메일로 주고받던 상품 견적을 한 화면에서

2025.07 ~ 2025.12

장외 파생상품 견적 요청을 조회·발송·회신 비교까지 처리하는 웹 콘솔 프론트엔드입니다.

과제

  • 견적 요청이 이메일로 오가 상품 조건을 사람이 옮겨 적음
  • 진행 상태가 메일함에 흩어져 요청 조회·호가 비교가 어려움

담당 범위

  • 프론트엔드 전 범위: React 19, TanStack Router / Query (단독 개발)
  • 가상화 무한 스크롤 공용 테이블, 서버 메타데이터 기반 동적 폼
  • 인증 가드·세션 만료 처리, SSE 실시간 통계, 품질 게이트

설계 판단

  • 검색 필드·템플릿 폼을 서버 메타데이터로 렌더링 → 필드 추가 시 프론트 재배포 불필요
  • 무한 스크롤 + 행 가상화 공용 테이블로 통일 → 화면별 코드를 컬럼 정의만 남김
  • 세션 만료를 HTTP 클라이언트 훅 한 곳에서 판정 → 엔드포인트별 중복 분기 제거

결과 지표

  • 커밋 283건 중 282건이 본인 계정
  • TypeScript/TSX 411개 파일, 24,498줄
  • 연동한 API 엔드포인트 경로 51개
원문 자세히 보기

과제

장외 파생상품 견적 요청이 이메일로 오가면서, 상품 조건을 사람이 옮겨 적고 진행 상태는 메일함에 흩어져 있었다. 수신 메일에서 뽑아낸 요청·주문을 한 화면에서 조회·검색하고, 템플릿으로 조건 조합을 만들어 다수 상대방에게 한 번에 발송하고, 회신된 호가를 비교하고, 운영자가 사용자·접속·발신 제어와 시스템 정지까지 관리할 수 있는 웹 콘솔을 만드는 것이 목표였다.

담당 범위

소스 24,498줄 · 파일 411개 · 라우트 29개 · 연동 API 51개

프론트엔드 단독 개발. 실제로 손댄 영역은 다음과 같다 — (1) TanStack Router 파일 기반 라우팅 구조 설계와 인증·메뉴 권한 이중 가드(beforeLoad에서 로그인 여부, loader에서 서버가 내려준 메뉴 목록으로 경로 접근 판정), (2) ky 기반 HTTP 클라이언트 3종(API/Auth/SSE) 구성과 전역 에러·세션 만료 처리, (3) 로그인·2FA·비밀번호 재설정 등 인증 플로우 UI와 요청 본문 AES-CBC 암호화 유틸, (4) TanStack Query 캐시 전략 설계(쿼리키 팩토리, 용도별 staleTime 프리셋 4종, 무한 쿼리 낙관적 업데이트/롤백), (5) 서버 메타데이터 응답으로 검색 필드와 RFQ 템플릿 폼을 그리는 동적 렌더링, (6) TanStack Table + Virtual 기반 가상화 무한 스크롤 테이블 공용 컴포넌트, (7) SSE 스트림을 직접 파싱하는 실시간 통계 바, (8) 데스크톱/모바일 분기 레이아웃과 모바일 허용 경로 제한, (9) Biome·Husky·lint-staged·commitlint 품질 게이트와 브랜치·커밋 컨벤션 문서화. 백엔드 API 스펙과 DB 스키마 설계, 배포·서버 인프라 구성에는 관여하지 않았다 — 전달받은 스펙에 맞춰 프론트엔드만 구현했다.

보기

선택안 · 대안 · 근거 · 비용

검색 가능 필드와 요청 템플릿 폼을 서버 메타데이터 응답으로 런타임에 렌더링하고, 테이블 컬럼 헤더는 프론트 상수로 남기는 혼합 방식

대안모든 필드를 프론트에 하드코딩하거나, 반대로 컬럼 헤더까지 전부 서버 메타데이터로 내리는 방식근거상품군마다 파라미터 구성이 다르고 운영 중에도 필드가 추가된다. 필드 하나 늘 때마다 프론트를 다시 배포하지 않으려고 폼과 검색 조건은 서버가 내려주는 필드 스펙으로 그렸다. 반대로 테이블은 표시 순서·정렬 허용 여부·셀 렌더러(뱃지 색, 퍼센트, 날짜 포맷)가 화면마다 달라서 서버 스펙으로 표현하기 어려웠고, 상수 파일로 두는 편이 읽기 쉬웠다.비용메타 스키마가 바뀌면 타입 체커가 못 잡고 런타임에 조용히 빈 폼이 된다. 라우트 loader에서 Promise.allSettled로 메타 실패를 격리하고 빈 배열 기본값을 두어 화면이 죽지는 않게 했지만, 실패해도 사용자에게는 '검색 조건이 없는 화면'으로만 보인다. 그리고 컬럼 상수(header-display-name.ts 460줄)와 서버 메타가 이원화되어, 필드 하나를 추가할 때 서버만 고치면 되는 경우와 양쪽을 같이 고쳐야 하는 경우가 섞여 있다.

페이지네이션 대신 무한 스크롤 + 행 가상화를 적용한 공용 테이블 컴포넌트 하나로 통일 (TanStack Table + TanStack Virtual + IntersectionObserver)

대안서버 페이지네이션 + 페이지 번호 UI, 또는 화면마다 각자 테이블을 구현근거RFQ·오더·메일·My RFQ·Price Discovery·설정 화면들이 전부 '컬럼 30여 개짜리 목록 + 검색 + 탭 + 상세 시트'라는 같은 형태였고, 트레이딩 화면 특성상 페이지를 끊지 않고 위아래로 훑는 사용 패턴이었다. 공용 컴포넌트(InfiniteDataTable)와 페이지 훅(useDataTablePage)으로 뽑아 화면별 코드를 컬럼 정의와 핸들러만 남겼다.비용가상화 때문에 행 높이를 고정값으로 못 박아야 했고(가변 높이 실험 후 되돌림), IntersectionObserver와 스크롤 핸들러 두 경로가 동시에 fetchNextPage를 부르는 문제를 300ms 디바운스 + 500ms 재진입 플래그로 막았다. 타이밍에 의존하는 방어라 근본 해법은 아니다. 브라우저 Ctrl+F 검색, 특정 페이지 북마크, 인쇄는 포기했다.

세션 만료·인증 실패를 각 호출부가 아니라 ky 훅(afterResponse/beforeError)에서 비즈니스 에러코드로 판정해 강제 로그아웃까지 한 곳에서 처리

대안각 쿼리/뮤테이션의 onError에서 개별 처리하거나, HTTP 401/403만 보고 판단근거백엔드가 HTTP 200에 응답 본문 code 필드로 인증 실패를 내려주는 경로가 있어서 상태 코드만으로는 못 잡는다. 엔드포인트 51개에 같은 분기를 복붙하는 대신 클라이언트 공통 옵션 한 곳에 모아, 성공 응답과 에러 응답 양쪽에서 같은 코드 집합을 검사하도록 했다.비용shared/lib/api/client.ts가 features/auth의 Zustand 스토어를 직접 import해서 의존 방향이 뒤집혔다(별도 문서에 참조 지점 15곳을 채증해 두었다). 로그아웃 후 이동을 라우터가 아니라 window.location.href로 하기 때문에 SPA 상태를 통째로 버린다 — 확실하다는 장점과 느리다는 단점을 맞바꾼 선택이다. 그리고 성공 응답마다 response.clone() 후 JSON을 한 번 더 파싱하는 비용이 붙는다.

사용 기술

ReactTypeScriptViteTanStack RouterTanStack QueryTanStack Table