Q3-1
`TicketsPage`를 가는편/오는편 별도 라우트로 안 만들고 한 라우트에서 모드 전환으로 처리한 이유는요?
핵심 포인트
- 한 라우트 +
isReturnPhase플래그 —tripType === ROUND && departureTicket !== null이면 오는편 모드 - 이 플래그 기준으로
origin/destination/date를 swap해 같은 useTicketsQuery 훅을 재사용 - 라우트가 같으면 사용자가 뒤로가기 했을 때 가는편 후보 화면으로 자연스럽게 돌아감
- 라우트를 둘로 나눴다면
/tickets/outbound↔/tickets/inbound동기화·뒤로가기 정책을 추가로 설계해야 했음 - 모드 전환의 근거가 atom 상태에서 자연스럽게 나옴 — URL이 아닌 atom으로 phase를 추적
모범 답안먼저 답해보고 펼치기
TicketsPage를 한 라우트로 묶은 가장 큰 이유는 두 화면이 본질적으로 같은 화면이기 때문입니다. 둘 다 출발역·도착역·날짜 조합으로 /api/tickets를 호출해서 그 결과를 리스트로 보여주고 사용자가 한 장을 고르는 흐름이고, 차이는 origin/destination/date를 어디서 가져오느냐 정도입니다. 화면 구조도 헤더 텍스트("가는 기차를 선택해주세요" vs "오는 기차를 선택해주세요")만 다르지 거의 같습니다.
이 본질이 같다는 판단을 코드에 표현한 게 TicketList와 TicketHeader의 isReturnPhase 플래그입니다 — tripType === ROUND && departureTicket !== null이면 오는편 모드, 아니면 가는편 모드. 이 플래그 하나로 origin/destination을 swap하고(가는편: 출발→도착, 오는편: 도착→출발), 사용할 날짜를 departureDate ↔ returnDate로 바꾸기만 하면 같은 useTicketsQuery 훅이 두 케이스를 다 처리합니다.
라우트를 /tickets/outbound와 /tickets/inbound로 나누는 안도 검토했는데, 이렇게 하면 (1) 어떤 시점에 어느 라우트로 navigate할지, (2) 오는편 화면에서 뒤로가기를 누르면 가는편 화면으로 돌아갈지 혹은 검색 페이지로 돌아갈지, (3) 둘 사이의 atom 동기화는 어떻게 할지 같은 추가 결정이 따라옵니다. 한 라우트로 두면 그런 결정이 사라지고, 사용자가 뒤로가기를 눌렀을 때 atom의 departureTicket이 살아 있으니 자연스럽게 가는편 후보 화면이 다시 보이는 효과가 무료로 따라옵니다.
핵심은 모드 전환 근거가 URL이 아니라 atom 상태에서 나온다는 점입니다. URL은 의도적으로 단순하게 유지하고, "현재 어느 phase냐"는 검색 조건과 선택한 티켓 atom들의 조합에서 derive한다 — 이 결정이 두 화면을 한 라우트로 묶을 수 있게 만든 토대입니다.
이 답변, 어땠나요?
꼬리 질문
- Q-a. 그럼 사용자가 새로고침을 하면 atom이 날아가는데, 오는편 모드에서 새로고침하면 어떻게 되나요?
→ 좋은 지적입니다. atom은 메모리에만 있으니 새로고침하면 검색 조건도 선택한 티켓도 다 날아갑니다. 그러면
useTicketsQuery의enabled가 false가 되고isPending이 false인 채로tickets.length === 0이 돼서EmptyResult가 보일 가능성이 높습니다. 이게 깔끔한 처리는 아닙니다 — 본 과제에서는 SearchPage로 redirect하는 page-level 가드를 README to-do로 남겨두고 우선순위에서 뺐습니다. 시간이 더 있다면<RequireSearchCondition>같은 가드 컴포넌트로 감싸서, atom이 비어 있으면 SearchPage로 replace navigate하는 게 표준 처리입니다. - Q-b. 두 화면이 본질이 같다고 하셨는데, 실제로 두 케이스에서 디자이너가 다른 UI를 요구하면 한 컴포넌트로 두는 게 부담 아닌가요? → 그 시점이 오면 분리합니다. 본 과제 명세 기준으로는 헤더 텍스트와 표시 날짜만 차이가 있어서 한 컴포넌트로 두는 게 효율적이고, 만약 오는편에만 추가 옵션(예: "당일 왕복 할인" 같은)이 들어온다면 그때 라우트 분리든 컴포넌트 분기든 검토하면 됩니다. 변경의 비용이 실제로 발생할 때 분리하는 게 YAGNI 측면에서 안전합니다.
- Q-c. 가는편 선택 직후 오는편 화면으로 모드만 바뀌면 사용자에게 시각적 변화가 적어서 헷갈리지 않을까요? → 충분히 합리적인 우려입니다. 본 과제에서는 헤더 텍스트가 "가는 기차를 선택해주세요" → "오는 기차를 선택해주세요"로, 표시 날짜가 가는날 → 오는날로 바뀌어서 시각적 신호는 유지됩니다. 추가로 transition 효과를 주거나 짧은 토스트("가는편이 선택됐어요. 이제 오는편을 골라주세요" 같은)를 띄우는 것도 UX 개선 포인트로 README에 기록해둘 만하다고 봅니다.
CS · 이론
- State machine으로서의 phase: "어느 phase냐"는 atom 상태의 조합에서 derive 가능. 명시적으로
phase: 'outbound' | 'inbound'atom을 두지 않아도tripType + departureTicket이 그 역할을 함. xstate 같은 라이브러리 없이도 derived state로 충분. - URL as source of truth vs atom as source of truth: URL을 SSOT로 두면 새로고침 안전·공유 가능 / atom을 SSOT로 두면 코드가 단순. 본 과제는 6시간 제약과 모바일 흐름 특성을 고려해 atom 쪽으로 기울임.
- Single Component, Multiple Modes: 본질이 같으면 한 컴포넌트, 시각이 같으면 한 라우트. 분리는 본질·시각이 갈라질 때.