김효중

김효중

Frontend Developer

3개 조직이 사용하는 토스증권 고객상담 시스템을 개발, 출시, 운영까지 전 사이클에 걸쳐 참여하며 하루 2,000건의 실시간 상담을 처리하는 서비스를 구축했어요.

개인의 목표보다 팀의 목표를 우선시해요.
팀의 방향에 맞춰 우선순위를 조절하고 빠르게 실행하는 것에 익숙해요.

제가 만든 것이 실제로 쓰일 때 가장 큰 동기를 얻어요.

서로 다른 세 조직의 구성원이 실제로 사용하는 서비스를 직접 개발·운영하며 고객의 목소리를 듣고 필요한 기능을 구현해 문제를 해결한 경험에서 가장 큰 보람을 느꼈어요.

경력

(주)토스증권

2025.03 — 현재

Operations Optimization Team · Frontend Ops Developer · 계약직 (2025.12 — 현재)

Product Stability Team · Frontend Developer Assistant · 인턴 (2025.03 — 2025.12)

전화·채팅 상담의 고객·계좌 조회부터 기록 생성, 화면 간 동기화까지 개발하고 출시 후 운영 문제를 개선했어요.

불필요한 SDK 로딩을 줄여 초기 JavaScript 71.6% 감소

영상통화 기능을 사용하지 않는 상담 팝업에도 SDK가 포함되어 있었어요. 의존성과 로딩 범위를 분리해 초기 JavaScript를 689kB에서 196kB로 줄였어요.

  • 빌드 산출물에서 전역 MeetingProvider의 정적 import를 따라 SDK가 포함되는 경로를 확인했어요. Provider를 실제 사용 라우트의 layout으로 옮기고, 헤더·드래그 패널은 next/dynamic으로 나눠 필요한 화면에서 불러오도록 했어요.
  • 데이터 조회 중에도 children은 렌더링하고 context만 null로 제공해 화면 표시와 데이터 대기를 분리했어요. 데이터 영역의 fallback을 유지한 상태에서 SDK 청크의 참조 경로와 First Load JS 변화를 빌드 산출물로 확인했어요.

네트워크가 끊긴 동안 누락된 대화 복원

재연결만으로는 돌아오지 않는 메시지를 다시 조회하고 병합해, 상담원이 누락된 대화를 이어서 확인할 수 있도록 개선했어요.

  • 마지막 메시지 시각을 기준으로 getTranscript를 순방향·오름차순 조회했어요. 이미 받은 메시지 ID를 Set에 담아 중복을 제거하고, 목록을 병합한 뒤 구독 화면을 갱신했어요. 세션이 사라지거나 조회 결과가 없으면 갱신을 중단했어요.
  • 적용 약 1개월 후 팀의 고객 만족도 집계에서 ‘매우 불만족’ 27.1% → 19.7%(-7.4%p), ‘매우 만족’ 40.6% → 48.2%(+7.6%p)가 관측됐어요. 팀 단위 개선 전후 관측값이에요.

외부 SDK 이벤트와 업무 처리의 결합 분리

외부 SDK의 이벤트 수신과 고객·계좌 조회, 기록 생성 로직을 분리하고 여러 탭 사이의 상담 상태를 동기화했어요.

  • Amazon Connect Streams/Chat 이벤트를 이벤트버스로 추상화하고, 구독하는 업무 로직을 분리해 테스트를 작성했어요.

CI 검사 시간 중앙값 약 24% 단축

  • 문제. 타입 검사·린트·테스트·빌드가 같은 의존성을 각각 설치해 PR 검증이 지연됐어요. lockfile 변경 직후에는 새 캐시를 채우는 시간도 필요했어요.
  • 해결. lockfile 기반 pnpm 캐시를 4개 작업에 적용하고, develop 반영 직후 pnpm fetch로 미리 채웠어요. Next.js 빌드 캐시도 복원·저장하도록 바꾼 뒤, 검사 완료 시간 중앙값이 5분 35초에서 4분 13초로 줄었어요.

SSE 전환 과정에서 발생한 프록시·연결 종료 문제 해결

주기적으로 조회하던 상담 지표를 SSE로 전환하면서, 브라우저·Next.js 프록시·서버 사이에서 발생한 데이터 미수신과 연결 종료 문제를 추적했어요.

  • 연결됐지만 이벤트가 오지 않는 문제. HTTP 200 응답 뒤에도 onmessage가 호출되지 않았어요. curl로 gzip과 identity 요청을 비교해 Next.js rewrite 경유 시 압축 버퍼링이 이벤트 전달을 막는 것을 확인했어요. SSE만 Route Handler로 분리하고 Accept-Encoding: identity와 버퍼링 방지 헤더를 적용해, 앱 전체의 압축 설정을 유지하면서 데이터를 전달하도록 했어요.
  • 운영 환경에서 발생한 403·404 오류. 프록시가 요청 헤더를 다시 구성하면서 X-Forwarded-For를 누락해, 접근 제어에서 클라이언트 대신 프록시의 내부 IP를 검사하고 있었어요. 수신 요청에 해당 헤더가 있을 때만 서버 요청에 전달하도록 수정했어요.
  • 브라우저에서 끊어도 남아 있는 서버 연결. 클라이언트는 150초마다 SSE 연결을 교체했지만 프록시의 fetch에는 취소 신호가 연결돼 있지 않았어요. request.signal을 서버 요청에도 전달하고, 클라이언트 취소 시 같은 신호가 중단되는지 테스트했어요.
  • 서버의 연결 종료가 프록시 오류로 번지는 문제. 서버 스트림이 먼저 닫힐 때 Next.js의 응답 전달 오류로 기록됐어요. reader를 감싼 ReadableStream에서 요청 취소와 terminated·other side closed 오류는 close로 처리하고, 나머지 오류는 error로 전달했어요. 종료 오류를 재현하는 회귀 테스트도 추가했어요.

복잡한 폼에서 검증·저장·단계 이동의 책임 분리

복합 입력과 동적 필드를 가진 상담 폼, 여러 단계를 거치는 문서 등록 화면의 상태를 역할별로 분리했어요.

  • 상담 폼은 Zod 스키마와 React Hook Form을 연결하고 Controller·useFieldArray로 입력을 관리했어요. 저장 응답을 해당 상담의 Query cache에 반영한 뒤 목록·상세를 갱신하고, 상담 ID·서버 데이터 변경에 맞춰 폼을 reset했어요.
  • 문서 등록 화면은 단계 구성·순서·초기 단계·이동 조건을 별도 컴포넌트와 훅으로 분리했어요. 페이지에는 입력 상태와 자동 저장·최종 저장·취소 처리를 남겨, 단계 변경과 저장 로직을 따로 수정할 수 있도록 했어요.

개인정보 조회 조건을 요청과 캐시에 일관되게 적용

마스킹 여부와 조회 사유를 화면마다 따로 처리하지 않도록 공통 조회 훅에 연결했어요.

  • useQuery·useSuspenseQuery를 확장해 queryFn 실행 시 마스킹 여부를 Query context의 헤더 메타데이터에 전달했어요. 고객 조회의 queryKey와 함수 인자에도 검색 조건·마스킹 여부·사유를 포함해 서로 다른 조건의 결과를 구분했어요.
  • 기존 MaskProvider·useMaskContext의 상태를 조회 입력에 연결했어요. 검색어가 없을 때는 enabled로 요청을 막고 자동 재조회 여부를 명시해, 화면과 조회 동작에 같은 정책을 적용했어요.

접근 권한과 화면 선택 조건을 한곳에서 관리

메뉴별 접근 권한과 고객 유형별 화면 분기를 상위에서 처리하도록 정리했어요.

  • route-permissions에 메뉴별 허용 조직을 매핑하고 공통 훅·가드와 403 안내 화면을 연결했어요. 회원 상태·계좌 유형에 따른 화면 선택은 상위 SwitchCase로 옮겨 하위 페이지에 흩어진 조건문을 정리했어요.

기존 동작을 유지하면서 공통 테이블과 렌더링 구조 확장

화면마다 다른 헤더를 지원하면서 기존 테이블 사용처의 동작을 유지하고, 브라우저 전용 UI의 렌더링 시점을 구분했어요.

  • 공통 ListTable에 renderHeader 확장 지점을 추가하고, 지정하지 않으면 기존 columns 헤더를 사용하도록 했어요. 커스텀 헤더가 우선하는 동작을 테스트해 하위 호환성을 확인했어요.
  • ClientGate에는 useSyncExternalStore의 서버·브라우저 snapshot을 적용했어요. 서버와 초기 hydration에서는 같은 null을 반환하고 이후 children을 렌더링해, 브라우저 전용 UI의 표시 시점을 공통으로 처리했어요.

기간·태그별 분석에서 실제 상담 이력까지 연결

요약 지표를 확인한 뒤 같은 조건의 상담 원본까지 살펴볼 수 있도록 분석 화면과 이력 조회를 연결했어요.

  • 날짜 구간·전화/채팅 집계·이전 기간 증감률·차트 데이터 변환을 함수로 분리했어요. visx로 기간별 건수와 태그의 14일 추이를 표시하고, 상담·음성 인식(STT) 영역은 Error Boundary로 나눠 오류를 격리했어요.
  • 함께 나타난 태그의 건수·비율을 조회하고, 선택한 태그 조합으로 필터링된 상담 이력으로 이동하도록 구현했어요. 로딩·빈 결과·날짜 미선택 상태도 구분했어요.

(주)업사이트

2024.08 — 2024.12

Frontend Developer · 인턴 · 서울 청년 예비인턴

2개 프로젝트가 공유하는 UI 패키지와 배포 흐름 구축

프로젝트마다 중복 구현하던 UI를 비공개 npm 패키지로 분리하고, 공통 컴포넌트의 버전 관리와 배포를 자동화했어요.

  • GitHub Packages로 디자인 시스템을 배포하고 Changesets로 변경 사항과 버전을 관리했어요. Storybook을 AWS S3에 배포해 디자이너와 실제 컴포넌트 단위로 QA했어요.

번역 문구 수정에 필요한 개발자 의존성 축소

개발자가 번역 JSON을 수정하던 흐름을 바꿔, 비개발자가 공통 시트에서 서비스 문구를 직접 관리하도록 개선했어요.

  • Google Spreadsheet API로 시트의 번역 데이터를 서비스에 반영했어요. 별도로 작업일보의 가변 입력은 React Hook Form·useFieldArray로 구현해 필드 추가·삭제와 입력 상태를 함께 관리했어요.

개인 프로젝트

HJ-Devlog

2023.06 — 현재

개인 블로그 · Next.js · React · TypeScript

필요한 언어만 불러와 문법 강조 번들 크기 축소

코드블록에 쓰이는 언어만 동적으로 불러오도록 바꿔 문법 강조 번들을 666.9KB에서 105.5KB로 줄였어요.

  • Lighthouse에서 Total Blocking Time 문제를 확인하고 next/bundle-analyzer로 react-syntax-highlighter의 번들 비용을 추적했어요. 언어별로 dynamic import하도록 로딩 단위를 나눴어요.

블로그 운영에 오류 관측과 성능 검사 연결

직접 만든 블로그에 오류 추적과 자동 성능 검사를 연결하고, 검색·공유와 방문 지표까지 관리했어요.

  • axios 응답 interceptor에서 수집한 오류 정보를 Sentry로 전송했어요. 전체 페이지의 Lighthouse 검사를 CI에서 자동 실행하도록 연결했어요.
  • 직접 만든 UI를 Storybook으로 문서화하고, 자동 목차는 Intersection Observer로 구현했어요. Next.js metadata 기반 sitemap을 Search Console에 등록하고 RSS·동적 OpenGraph 이미지를 제공했어요.
  • GA의 pageLocation·totalUsers로 페이지별 사용자 수를 표시하고, Recharts로 방문자 수·참여 시간·세션당 페이지 수를 시각화했어요.

학력 · 활동

프로그래머스 데브코스

2023.06 — 2023.12

팀 프로젝트 2회, 멘토·동료 코드 리뷰에 참여했어요.

← 블로그로 돌아가기