본문으로 건너뛰기
면접 학습
복습세션 →

과제 기반 — 기차예약

토픽 2 · Phase 1

상태관리 의사결정 — Jotai + React Query 역할 분담

질문 5개
  1. 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 가능 (테스트·외부 통합 시 유리).
  2. Q2-2

    atom을 `departureStation`, `arrivalStation`, `tripType`, `departureDate`, `returnDate`, `passengerCount` 처럼 필드별로 분리한 이유는요? 하나의 큰 객체 atom으로 묶으면 안 되나요?

    핵심 포인트

    • selective re-render가 핵심 — 인원 한 번 바꿔도 출발역 컴포넌트는 리렌더되지 않게
    • 큰 객체 atom으로 묶으면 한 필드 변경마다 모든 구독자가 리렌더됨 (객체 참조 새로 생기니)
    • atom 분리는 사실상 store normalization과 같은 효과
    • 관련 변경(출발역 ↔ 도착역, 가는날 ↔ 오는날)은 writable atom의 부수효과로 묶어둠 — 분리하되 일관성은 보존
    모범 답안먼저 답해보고 펼치기

    atom을 필드별로 쪼갠 가장 큰 이유는 selective re-render입니다. 만약 searchConditionAtom이라는 큰 객체 atom 하나를 두고 그 안에 { departureStation, arrivalStation, tripType, ... }을 다 넣으면, 인원 한 번 바꾸는 것만으로도 객체 참조가 새로 생기니 그 atom을 구독하는 모든 컴포넌트가 다 리렌더됩니다. 출발역 ListRow가 인원 변경 때마다 리렌더되는 건 명백한 낭비고요.

    필드별로 atom을 두면 passengerCountAtom을 구독한 CountField만 리렌더되고, 출발역을 구독한 StationSection은 그대로 있습니다. 이건 jotai의 atom 모델이 자연스럽게 만들어주는 효과라 별도 메모이제이션 코드 없이 무료로 따라옵니다.

    다만 필드를 분리한다고 일관성을 잃으면 안 되는 케이스가 있습니다. 예를 들어 출발역과 도착역이 같으면 안 되고, 가는날을 변경해서 오는날이 그것보다 이전이 되면 안 되고, 가는날·편도왕복이 바뀌면 이미 선택한 티켓은 더 이상 의미가 없어서 reset돼야 합니다. 이런 종속성은 writable atom의 부수효과로 묶어서 처리했습니다 — setDepartureDateAtom은 가는날을 setting하면서 (1) 다른 날짜면 티켓 reset, (2) returnDate가 더 이전이면 undefined로 정리합니다. atom을 쪼갰지만 변경 흐름은 한 곳에 모인 셈입니다.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. 그럼 큰 객체 atom + selectAtom 같은 방식도 가능하지 않나요? jotai에 그런 헬퍼가 있는데요.selectAtom을 쓰면 큰 atom에서 일부분만 구독해 selective re-render를 흉내 낼 수는 있습니다. 다만 그 경우 setter는 여전히 큰 atom을 통째로 갈아끼우는 식이라 편하지 않고, equality 비교를 selectAtom 쓰는 곳마다 신경 써야 합니다. 처음부터 atom을 쪼개는 게 더 단순하고 의도가 명확하다고 봤습니다.
    • Q-b. atom이 너무 많으면 메모리 문제 같은 건 없나요? → atom 자체는 일종의 key + reducer 정의라 객체 하나 정도의 비용입니다. 본 과제에서 client state atom이 6개 + writable 6개 + derived 1개 정도라 메모리 부담은 무시할 수준입니다. 정작 부담스러운 건 너무 많아져서 개념적으로 추적이 어려워지는 경우인데, 그래서 한 도메인의 atom들은 한 파일(searchConditionAtom.ts, ticketConditionAtom.ts)에 모아 응집도를 유지했습니다.
    • Q-c. setDepartureDateAtom처럼 부수효과를 atom 안에 묻으면 트레이싱이 어렵지 않나요? → 트레이드오프가 있습니다. 호출처가 단순해지는 대신 atom 정의를 봐야 부수효과를 알 수 있습니다. 다만 본 과제에서 그 부수효과들은 도메인 룰(같은 역 자동 해제, 가는날 변경 시 종속 데이터 정리)이라 호출처마다 매번 적는 것보다 atom에 묶는 게 일관성 측면에서 안전하다고 봤습니다 — 한 군데서 빠뜨릴 위험이 사라집니다.
    CS · 이론
    • Normalization: 큰 state를 작은 단위로 쪼개서 관리하는 방식. redux toolkit의 createEntityAdapter도 같은 사상. atom 분리는 사실상 컴포넌트 단위 normalization.
    • Writable derived atom (jotai): atom(read, write) 형태로 setter에 부수효과를 묶는 패턴. 본 과제의 setDepartureDateAtom, setDepartureStationAtom, setTripTypeAtom이 그 예. 단순 setter가 아니라 도메인 룰을 보장하는 mini-reducer.
    • Reference equality: 객체 atom이 한 글자만 바뀌어도 새 참조 → React가 리렌더. selective subscription을 결정하는 가장 기본 메커니즘.
  3. Q2-3

    `isSearchValidAtom`은 derived atom으로 만들었는데, 그냥 컴포넌트의 `useMemo`로 한 번 계산해도 충분하지 않았나요?

    핵심 포인트

    • 검증 규칙의 단일 정의 위치(SSOT) — 검색 가능 여부의 정의가 atom 한 곳에 모임
    • SearchPage의 "기차 보기" 버튼 비활성화에 쓰지만, 향후 TicketsPage 같은 데서도 같은 기준으로 가드를 걸 수 있음
    • useMemo는 컴포넌트 안에서만 살아 있고, 값 자체를 다른 곳에서 재구독할 수 없음 — atom은 어디서든 useAtomValue 한 줄로 접근
    • 검증 룰이 늘어나면 atom 안에서만 수정하면 되고 호출처 코드는 그대로 — 변경 폭발 반경 감소
    모범 답안먼저 답해보고 펼치기

    useMemo와 derived atom 둘 다 같은 결과를 낼 수 있는 자리지만, 이번 케이스는 atom으로 둔 게 더 자연스럽다고 봤습니다. 이유는 두 가지입니다.

    첫째, 검증 규칙이 한 곳에 모이는 효과입니다. 출발역·도착역이 둘 다 있어야 하고, 둘이 같으면 안 되고, 가는날이 있어야 하고, 왕복이면 오는날도 있어야 하고, 인원이 0명이면 안 되고, 유아 동반 시 어른이 1명 이상이어야 한다 — 이 룰들이 isSearchValidAtom 한 군데에 모입니다. 룰이 늘어나면 atom만 고치면 되고 호출처는 그대로입니다. useMemo로 페이지 안에 박아두면 이 검증 코드가 여러 페이지에 흩어질 위험이 있는데, atom으로 두면 그런 분산이 원천 차단됩니다.

    둘째, 재사용성입니다. 지금은 SearchPage의 "기차 보기" 버튼 비활성화에만 쓰이지만, TicketsPageConfirmPage에서 직접 URL로 진입했을 때 검색 조건이 valid한지 검사해서 잘못된 진입을 막는 가드로도 자연스럽게 활용할 수 있습니다 — useAtomValue(isSearchValidAtom) 한 줄로요. useMemo였으면 그 페이지마다 같은 계산을 다시 짜야 합니다.

    물론 비용도 있습니다. derived atom은 그 atom이 의존하는 atom들이 바뀔 때마다 재계산되니, 검색 조건이 자주 바뀌는 SearchPage에서 매 입력마다 7~8개 atom을 다 읽고 boolean 하나를 내놓습니다. 다만 이건 실측해도 무시할 수준의 계산량이고, jotai가 의존성 추적을 alquantitatively 잘 해주니 의도와 다른 재계산이 일어날 가능성은 거의 없습니다.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. atom 안에 console.warn이 들어 있는데 production에서도 그대로 두는 게 맞나요? → 좋은 지적입니다. 본 과제에서는 디버깅 흔적으로 남아 있는데, 실제 production이라면 console.warn을 빼거나 dev 모드에서만 찍히도록 막는 게 맞습니다. 면접에서 직접 짚으시면 인정하고, 정리되지 않은 흔적이라고 말씀드릴 부분입니다.
    • Q-b. derived atom 대신 selector function을 atom에 직접 노출시키는 zustand 패턴이 더 가볍지 않나요? → zustand의 selector는 store 함수 호출 시점에 selector를 부르는 구조라 의존성 트래킹을 useMemo+useStore에 의존합니다. jotai의 derived atom은 의존성 그래프가 자동 구성돼서 selector 코드를 호출처에 적을 필요가 없는 게 차이입니다. 본 과제 규모에선 둘 다 충분하지만 jotai를 골랐기 때문에 그 결을 따라간 결정입니다.
    • Q-c. derived atom이 너무 많아지면 의존성 그래프가 복잡해지지 않나요? → 맞습니다. 본 과제는 derived atom 1개라 부담이 없지만, 큰 앱에서 derived atom이 derived atom을 의존하는 식으로 누적되면 의도와 다른 리렌더가 일어나거나 디버깅이 어려워질 수 있습니다. jotai에서는 useAtomDevtools나 React DevTools jotai extension으로 그래프를 시각화하는 게 일반적입니다.
    CS · 이론
    • Single Source of Truth (SSOT): 같은 진실을 여러 곳에서 정의하지 않는다. 검증 룰처럼 도메인 결정이 들어 있는 코드는 atom으로 끌어올릴 가치가 크다.
    • Derived state: 다른 state로부터 계산되는 state. recoil의 selector, jotai의 atom(get => ...), redux의 reselect가 모두 같은 사상.
    • Memoization 비용 vs 가치: useMemo는 의존성 배열을 직접 관리해야 하고 컴포넌트 안에 갇힌다. derived atom은 의존성이 자동으로 추적되고 어디서든 재구독 가능 — 더 비싸 보이지만 분산 비용까지 더하면 보통 같다.
  4. Q2-4

    `searchConditionAtom`과 `ticketConditionAtom`을 두 파일로 나눈 이유가 있나요?

    핵심 포인트

    • "다시 선택" 흐름을 단순하게 만들기 위한 분리 — PDF 명시 요구사항(검색 조건 유지 + 티켓만 reset)을 atom 경계로 직접 표현
    • 검색 조건은 사용자 입력의 상위 도메인, 티켓은 그 입력에 종속된 하위 도메인
    • resetSelectedTicketsAtomsearchConditionAtom에서 import해서 부수효과 묶음 — 가는날·tripType이 바뀌면 티켓 자동 reset
    • 분리하지 않았으면 "다시 선택" 시 어떤 필드는 유지하고 어떤 건 reset할지 일일이 처리해야 했음
    모범 답안먼저 답해보고 펼치기

    이건 PDF의 한 문장에서 출발한 결정이었습니다. ConfirmPage 명세에 "기차표를 제외하고, 사용자가 입력한 출발역, 도착역, 여행 방식, 인원수는 유지되어야 해요"라고 명시돼 있어서, 이걸 atom 구조에 직접 반영하기로 했습니다.

    검색 조건(출발역·도착역·tripType·날짜·인원)은 사용자가 직접 만드는 상위 도메인이고, 선택한 티켓(가는편·오는편)은 그 검색 조건에 종속된 하위 도메인입니다. 두 도메인의 라이프사이클이 다르니 파일도 따로 둔 게 자연스럽습니다.

    이 분리가 빛나는 곳이 두 군데 있습니다. 첫째, ConfirmPage의 "다시 선택하기" 버튼 — resetSelectedTicketsAtom만 호출하고 navigate(SEARCH) 하면 검색 조건은 그대로, 티켓만 비워진 상태로 SearchPage로 돌아갑니다. 이게 atom 한 줄로 끝납니다. 둘째, 가는날을 바꾸거나 편도/왕복을 바꿀 때 — setDepartureDateAtomsetTripTypeAtom이 부수효과로 resetSelectedTicketsAtom을 호출합니다. 검색 조건이 바뀌면 이미 선택해둔 티켓은 더 이상 의미가 없으니까요. 이 부수효과를 위해서 searchConditionAtomticketConditionAtom에서 export한 reset atom을 import하는 구조입니다.

    만약 두 파일을 합쳤으면 같은 동작은 가능했겠지만, "다시 선택" 시 어떤 필드는 유지하고 어떤 건 reset할지 호출처에서 일일이 처리해야 했을 겁니다. 그러면 한 군데서만 빼먹어도 일관성이 깨집니다. 파일 분리 + reset atom 패턴이 그 위험을 구조적으로 막아줍니다.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. 두 atom 파일이 서로 import하면 순환 의존성 위험이 있지 않나요? → 단방향이라 순환은 아닙니다 — searchConditionAtomticketConditionAtom을 import할 뿐, 반대 방향은 없습니다. 도메인 의존성으로 보면 검색 조건이 상위, 티켓이 하위니 자연스러운 방향입니다.
    • Q-b. 그럼 resetAllAtom은 왜 또 따로 있나요? 같은 일을 atom 단위로 다 reset하면 되지 않나요?resetAllAtom은 CompletePage에서 "완료" 버튼을 눌렀을 때, 새 예약 흐름을 깨끗하게 시작하기 위해 모든 검색 조건 + 티켓 + 인원을 초기값으로 되돌리는 트리거입니다. 호출처에서 6개 setter를 일일이 호출하지 않게 하나의 atom으로 묶었습니다. "다시 선택"과는 의도가 달라서 별도 atom으로 두는 게 명료합니다.
    • Q-c. 하나 더 — 출발역/도착역까지 ticketConditionAtom 쪽으로 가져가는 게 도메인적으로 더 맞지 않나요? 티켓 검색의 입력값이니까요. → 흥미로운 지적입니다. 다만 PDF가 "검색 조건"으로 묶어 표현했고, "다시 선택" 시 유지 대상에 출발역/도착역도 포함돼 있어서 검색 조건 쪽에 두는 게 명세와 정렬됩니다. 도메인 모델이 명세를 따라간 케이스라고 보시면 됩니다.
    CS · 이론
    • Domain boundary as file boundary: 같은 라이프사이클·같은 변경 이유로 묶이는 atom은 한 파일에. 라이프사이클이 다르면 파일 분리.
    • Cross-atom side effects: jotai writable atom의 setter에서 다른 atom을 set하는 패턴은 일반적이고 권장됨. 도메인 룰을 한 곳에 묶는 효과.
    • Reset patterns: redux에서는 RESET action, jotai에서는 reset 전용 writable atom. 부분 reset(resetSelectedTickets) vs 전체 reset(resetAll)을 분리해두는 게 깨끗.
  5. Q2-5

    React Query에서 `staleTime: 60s`, `keepPreviousData`, `enabled: params !== null` 같은 옵션을 어떻게 결정하셨나요?

    핵심 포인트

    • staleTime: 60s — 역 목록·티켓 목록 같은 데이터가 1분 안에 자주 바뀔 가능성 낮음. refetch 빈도 낮춰 깜빡임/낭비 방지
    • retry: 1 — 네트워크 일시 오류 한 번은 재시도, 그 이상은 사용자 피드백으로
    • refetchOnWindowFocus: false — 모바일 흐름이라 탭 포커스 변경이 거의 없음. 켜두면 BottomSheet 닫힐 때마다 재요청 트리거 위험
    • keepPreviousData — 검색 키워드 입력 시 이전 결과를 유지해 깜빡임 방지 (UX 개선)
    • enabled: params !== null — 출발역·도착역·날짜가 다 갖춰지지 않은 상태에서는 호출 자체를 막아야 한다 — 옵셔널 chaining으로 풀 수도 있지만 enabled가 더 명확
    모범 답안먼저 답해보고 펼치기

    React Query의 옵션은 두 군데에서 정했습니다. 글로벌 기본값(queryClient.ts)과 각 query 훅에서의 override.

    글로벌 기본값에서 staleTime: 60s를 줬습니다. 본 과제의 데이터는 역 목록·티켓 목록·예약 정보인데, 이것들이 1분 안에 자주 바뀔 가능성이 낮습니다. staleTime을 0으로 두면 컴포넌트가 마운트될 때마다 백그라운드 refetch가 일어나 불필요한 요청이 늘어나니, 60초 정도면 합리적인 기본값이라고 봤습니다. retry: 1은 일시적 네트워크 오류에 한 번은 재시도하고 그 이상은 사용자에게 에러로 노출하는 균형점이고요. refetchOnWindowFocus: false는 본 과제가 모바일 흐름 중심이라 탭 포커스 변경이 거의 없고, BottomSheet가 열렸다 닫힐 때마다 윈도우 포커스 이벤트가 트리거될 수 있어서 끄는 쪽이 안전했습니다.

    훅 레벨 override는 두 가지가 의미 있는데요.

    첫째 useStationsQueryplaceholderData: keepPreviousData. 사용자가 역명을 한 글자씩 입력할 때마다 디바운스된 keyword가 바뀌고 query key가 바뀝니다. 기본 동작이면 키 변경 시 데이터가 잠깐 undefined가 됐다가 새로 채워지는 깜빡임이 생기는데, keepPreviousData를 주면 이전 결과를 그대로 보여주면서 새 결과를 백그라운드에서 받아옵니다. 검색 UX의 표준 처리고요.

    둘째 useTicketsQueryenabled: params !== null. 티켓을 검색하려면 출발역·도착역·날짜가 다 있어야 하는데, 그게 갖춰지기 전에 호출이 나가면 안 됩니다. enabled 플래그로 호출 자체를 막아두는 게 깔끔합니다. 같은 패턴이 useReservationQuery에도 들어 있습니다 — id가 null이면 호출 안 함.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. mutation 후 invalidation은 안 하시네요? 예약 후 reservations 캐시 갱신 같은 거요. → 본 과제 흐름상 예약 후 바로 CompletePage로 이동해서 id로 다시 GET하는 구조라, list 캐시를 invalidate할 일이 없었습니다. 만약 예약 목록 페이지 같은 게 추가된다면 mutation의 onSuccess에서 queryClient.invalidateQueries(['reservations'])를 호출하는 게 표준 패턴이고요.
    • Q-b. queryKey를 [QUERY_KEYS.STATIONS, params]로 했는데 params 객체가 매번 새로 만들어지면 캐시 키가 어긋나지 않나요? → React Query는 query key를 deep equality로 비교하기 때문에 객체 참조가 바뀌어도 내용이 같으면 같은 키로 인식합니다. 그래서 params를 매번 새 객체로 만들어도 캐시는 정상 동작합니다.
    • Q-c. tosslib의 isHttpError로 에러 처리하는 부분은 어디서 했나요? → 이번 과제에선 mutate의 onError에서 명시적으로 핸들링하지 않고 isPending으로 버튼만 막아두는 정도까지만 했습니다. 본격적인 에러 처리는 우선순위에서 빼고 README에 to-do로 남겼는데, 실제로 했다면 mutation의 onError에서 isHttpError로 분기해 사용자에게 토스트로 메시지를 노출하는 게 표준 흐름입니다. 지금은 미구현이라 솔직히 인정하고 답합니다.
    CS · 이론
    • staleTime vs cacheTime: staleTime은 "fresh로 간주하는 기간"(이 안에는 background refetch 안 함), cacheTime(v5에서는 gcTime)은 "캐시 메모리에 보관하는 기간"(없는 동안은 GC). 둘은 다른 차원.
    • Query key as cache identity: deep equality로 비교. 객체·배열을 그대로 키로 써도 안전. 다만 DateMap 같은 비구조화 객체는 직렬화에 유의.
    • enabled flag: useQuery의 lazy 실행 패턴. dependent query(다른 데이터에 의존하는 query)에서 표준.
    • placeholderData vs initialData: initialData는 캐시에 진짜로 박히는 값(시간 측정 시작), placeholderData는 보여주기만 하는 임시 값(stale 시간 영향 없음). 검색 UX엔 placeholder가 적절.
    • refetchOnWindowFocus: 데스크톱 기반 SPA에선 true가 기본값으로 좋지만, 모바일/오버레이가 많은 흐름에선 의도와 다른 트리거 위험.