Q2-1
`package.json`에 jotai도 react-query도 없었는데, 둘을 직접 골라서 도입했네요. zustand·redux·Context는 왜 후보에서 빠졌나요?
핵심 포인트
- 시작점 라이브러리가 비어 있다는 건 토스가 "라이브러리 선택 자체"를 평가 포인트로 본다는 시그널
- jotai 선택 이유: atom 단위 selective subscription, 보일러플레이트 최소, Provider 1줄, recoil 후속·메인테이너 안정성
- react-query 선택 이유: client state와 server state는 다루는 도구가 다르다 — 캐싱·중복 요청 제거·invalidate·로딩/에러 상태 관리를 직접 짜는 건 6시간 안에 비용 대비 가치 낮음
- Context 배제: provider value 변경 시 모든 consumer 리렌더 → 검색 필드 4~5개를 atom처럼 selective하게 끊어 쓰기 어려움
- redux 배제: 보일러플레이트(action·reducer·dispatcher)가 6시간 제약·"화려한 방법보다 익숙한 방법" 가이드와 안 맞음
- zustand 배제: jotai와 비교했을 때 selective subscription이 selector 함수에 의존(equality 직접 관리), atom 단위 분리만큼 자연스럽지 않음
모범 답안먼저 답해보고 펼치기
시작점에서 가장 먼저 본 신호는 package.json에 jotai도 react-query도 없다는 점이었습니다. 마크업·스타일은 거의 다 깔려 있는데 라이브러리만 비어 있다는 건, 토스가 상태 관리 도구를 직접 골라서 그 선택을 설명할 수 있는가를 본다는 의도라고 읽었습니다.
저는 client state는 jotai, server state는 react-query로 가르고 시작했습니다. 두 가지가 풀어야 할 문제 자체가 다르니까요. 사용자가 입력한 출발역·가는날·인원 같은 값은 클라이언트가 주인이고 페이지를 옮겨다녀도 유지돼야 하는 데이터고, /api/stations나 /api/tickets 응답은 서버가 주인인 캐시 같은 데이터입니다. 한 도구로 둘 다 다루면 staleTime이나 invalidate 같은 개념이 client state까지 침범해서 모델이 흐려진다고 봤습니다.
jotai를 고른 이유는 세 가지입니다. 첫째, atom 단위로 selective subscription이 자연스럽게 됩니다 — passengerCountAtom을 구독한 컴포넌트는 다른 atom이 바뀌어도 리렌더되지 않습니다. 둘째, 보일러플레이트가 거의 없습니다. atom 한 줄로 끝나니 6시간 제약 안에서 행정 비용이 작습니다. 셋째, atom이 모듈 스코프 변수라 "다시 선택" 같은 페이지 간 영속화 요구가 자연스럽게 풀립니다.
다른 후보를 배제한 이유도 명확합니다. Context API는 provider value가 바뀌면 모든 consumer가 리렌더되기 때문에 검색 필드 4~5개를 별도로 끊어서 다루기에는 적합하지 않습니다. 별도 provider를 5개 만들면 트리가 지저분해지고요. redux는 action·reducer·dispatcher 보일러플레이트가 본 과제 규모에 과합니다. 토스 가이드라인의 "화려한 방법보다 익숙한 방법"과도 충돌합니다. zustand는 좋은 후보였지만, selective subscription을 위해서 selector 함수와 equality 비교를 직접 신경 써야 하는 반면 jotai는 atom을 따로 만드는 것만으로 그게 자연스럽게 됩니다. 본 과제처럼 필드 4~5개를 작게 쪼개는 패턴엔 jotai가 더 잘 맞는다고 봤습니다.
react-query는 server state 표준이라 별 고민 없이 골랐습니다. 캐싱, 중복 요청 제거, mutation 후 invalidate, isPending/isError 같은 상태 관리를 직접 짜면 6시간이 다 갑니다.
이 답변, 어땠나요?
꼬리 질문
- Q-a. recoil이 jotai 자리를 대신할 수도 있었을 텐데 안 보신 이유는요? → recoil은 페이스북에서 만들었지만 메인테이너 활동이 사실상 멈췄고 GitHub 이슈가 누적되고 있는 상황이라 신규 도입은 권장하지 않습니다. jotai 메인테이너인 Daishi Kato가 React 생태계에서 활발하고 유지보수가 안정적이라 그쪽으로 갔습니다.
- Q-b. 그럼 server state도 그냥 jotai로 다룰 수 있지 않나요? jotai에 비동기 atom도 있고요.
→ 가능은 합니다 — jotai의
atom(async ...)또는loadable헬퍼로요. 다만 그렇게 쓰는 순간 캐시 키 관리, stale 정책, mutation invalidate, optimistic update 같은 server state 고유 문제를 직접 풀어야 합니다. react-query는 그걸 위해 만든 도구니 그쪽으로 가는 게 표준이라고 봤습니다. jotai와 react-query를 결합하는jotai-tanstack-query같은 어댑터도 있지만 그건 두 모델을 섞는 거라 취향 영역이고요. - Q-c. useState만으로도 풀 수 있는 작은 과제인데 jotai가 오버킬 아닌가요? → 한 화면이라면 그렇습니다. 다만 본 과제는 검색→역선택→티켓→예약확인→완료까지 5개 페이지에 걸쳐 사용자 입력값이 살아남아야 하고, 특히 PDF에 "기차표를 제외하고 출발역/도착역/여행 방식/인원수는 유지되어야 해요"라고 명시돼 있습니다. useState였다면 페이지 간 props 드릴링이나 라우트 state로 들고 다녀야 하는데 그게 더 부자연스럽다고 봤습니다.
CS · 이론
- Atom-based state model (jotai): state를 작은 unit(atom)로 쪼개고 컴포넌트는 자기가 구독한 atom의 변경에만 반응. recoil의 핵심 모델이기도 함. selective subscription이 무료로 따라옴.
- Client state vs Server state (Tanner Linsley): 두 상태는 본질이 다르다 — server state는 타인의 데이터의 캐시(stale·refetch·invalidate가 필수), client state는 자기 데이터(영속화·검증). 도구를 분리하는 게 자연스럽다.
- Context propagation 비용: React Context는 value 참조가 바뀌면 모든 consumer가 리렌더. 필드별 selective 구독이 필요하면 atom 모델이 우월. (
useContextSelector같은 우회책은 표준이 아님) - Provider 트리 깊이 vs 모듈 스코프: redux/zustand/jotai 모두 Provider 한 번이면 됨. atom은 모듈 스코프 변수이므로 React 트리 밖에서도 read·write 가능 (테스트·외부 통합 시 유리).