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

과제 기반 — 기차예약

토픽 3 · Phase 1

편도/왕복 분기 전략

질문 5개
  1. 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 "오는 기차를 선택해주세요")만 다르지 거의 같습니다.

    이 본질이 같다는 판단을 코드에 표현한 게 TicketListTicketHeaderisReturnPhase 플래그입니다 — tripType === ROUND && departureTicket !== null이면 오는편 모드, 아니면 가는편 모드. 이 플래그 하나로 origin/destination을 swap하고(가는편: 출발→도착, 오는편: 도착→출발), 사용할 날짜를 departureDatereturnDate로 바꾸기만 하면 같은 useTicketsQuery 훅이 두 케이스를 다 처리합니다.

    라우트를 /tickets/outbound/tickets/inbound로 나누는 안도 검토했는데, 이렇게 하면 (1) 어떤 시점에 어느 라우트로 navigate할지, (2) 오는편 화면에서 뒤로가기를 누르면 가는편 화면으로 돌아갈지 혹은 검색 페이지로 돌아갈지, (3) 둘 사이의 atom 동기화는 어떻게 할지 같은 추가 결정이 따라옵니다. 한 라우트로 두면 그런 결정이 사라지고, 사용자가 뒤로가기를 눌렀을 때 atom의 departureTicket이 살아 있으니 자연스럽게 가는편 후보 화면이 다시 보이는 효과가 무료로 따라옵니다.

    핵심은 모드 전환 근거가 URL이 아니라 atom 상태에서 나온다는 점입니다. URL은 의도적으로 단순하게 유지하고, "현재 어느 phase냐"는 검색 조건과 선택한 티켓 atom들의 조합에서 derive한다 — 이 결정이 두 화면을 한 라우트로 묶을 수 있게 만든 토대입니다.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. 그럼 사용자가 새로고침을 하면 atom이 날아가는데, 오는편 모드에서 새로고침하면 어떻게 되나요? → 좋은 지적입니다. atom은 메모리에만 있으니 새로고침하면 검색 조건도 선택한 티켓도 다 날아갑니다. 그러면 useTicketsQueryenabled가 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: 본질이 같으면 한 컴포넌트, 시각이 같으면 한 라우트. 분리는 본질·시각이 갈라질 때.
  2. Q3-2

    POST `/api/reservations` body가 편도(`ticketId`)와 왕복(`outboundTicketId` + `inboundTicketId`)에서 다른 모양인데, 이걸 어떻게 풀었나요?

    핵심 포인트

    • Discriminated uniontype: TRIP_TYPE.ONE_WAYtype: TRIP_TYPE.ROUND를 식별자로
    • 두 body 인터페이스를 각각 정의 후 PostReservationApiBody = OneWay | Round로 합치면 TypeScript가 분기를 강제
    • ConfirmBottomCTA.handleReserve에서 tripType === ONE_WAY로 분기 → 각 분기 안에서는 그 타입에 맞는 필드명만 허용됨 (잘못된 필드 컴파일 에러)
    • Reservation 응답도 같은 패턴ReservationOneWay(ticket) ↔ ReservationRound(outboundTicket/inboundTicket) discriminated union
    모범 답안먼저 답해보고 펼치기

    편도와 왕복은 API body 모양이 다릅니다. 편도는 { type: 'ONE_WAY', ticketId, count }, 왕복은 { type: 'ROUND', outboundTicketId, inboundTicketId, count }. 한 인터페이스에 모든 필드를 옵셔널로 넣으면 컴파일러가 어떤 조합이 유효한지 검증해주지 못하니, discriminated union으로 두 모양을 따로 정의하고 type 필드를 식별자로 잡았습니다.

    useReservationMutation.tsPostReservationApiBodyOneWayPostReservationApiBodyRound를 각각 정의하고 PostReservationApiBody = OneWay | Round로 합쳤습니다. 이 타입을 받는 mutate 호출은 자동으로 type 값에 따라 분기되고, type === 'ONE_WAY' 분기 안에서 outboundTicketId 같은 잘못된 필드를 쓰면 컴파일 에러가 납니다. TypeScript의 narrowing이 도메인 룰을 직접 강제해주는 셈입니다.

    ConfirmBottomCTA.handleReserve에서 이 타입을 활용한 흐름은 이렇습니다 — tripType === ONE_WAY이면 { type: ONE_WAY, ticketId: departureTicket.id, count: passengerCount }, 왕복이고 arrivalTicket이 있으면 { type: ROUND, outboundTicketId, inboundTicketId, count }, 왕복인데 arrivalTicket이 null이면 body가 null이라 mutation을 호출하지 않습니다. 가드 로직과 타입이 한 곳에 정렬됩니다.

    같은 패턴이 GET /api/reservations/:id 응답에도 들어 있습니다. Reservation = ReservationOneWay | ReservationRound로 두고, OneWay는 ticket 필드, Round는 outboundTicket/inboundTicket 필드를 가집니다. 응답을 소비하는 컴포넌트에서 reservation.type === 'ONE_WAY'로 분기하면 그 분기 안에서만 ticket을 안전하게 접근할 수 있습니다.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. 옵셔널 필드로 풀면 안 되는 이유를 좀 더 설명해주세요. ticketId?: number; outboundTicketId?: number; inboundTicketId?: number 처럼요. → 컴파일러가 두 가지를 검증해주지 못해서 그렇습니다. 첫째, "type이 ONE_WAY일 때 outbound/inbound가 절대 없다"를 강제하지 못합니다 — 코드가 둘 다 채워서 보내도 컴파일은 통과합니다. 둘째, "type이 ROUND일 때 outbound와 inbound가 반드시 있다"도 강제하지 못합니다. discriminated union으로 두면 이 두 가지 invariant가 타입 시스템 차원에서 보장됩니다.
    • Q-b. 만약 향후 "왕복 + 다구간" 같은 새 type이 추가되면 어떻게 확장하나요? → discriminated union의 큰 장점이 그 시점에 드러납니다. PostReservationApiBodyMultiSegment를 추가하고 union에 합치면, 기존 코드의 if (type === ONE_WAY) / else (type === ROUND) 같은 switch가 exhaustive하지 않다는 컴파일 에러를 냅니다. 새 case를 빼먹은 곳을 컴파일러가 알려주는 거라, 안전한 확장이 가능합니다.
    • Q-c. 응답 처리할 때 Reservation 객체가 OneWay인지 Round인지 type으로 분기하는 게 번거롭지 않나요? 한 인터페이스로 통합해서 바로 쓰면 더 편하지 않나요? → 컴포넌트마다 분기 코드가 늘어나는 건 사실입니다. 다만 그 비용 대신 잘못된 접근을 컴파일 시점에 막아주는 안전장치를 얻습니다. 통합 인터페이스로 두면 outboundTicket? 같은 옵셔널을 !로 풀어서 쓰는 코드가 늘어날 텐데, 그 시점에 런타임 오류 위험이 생깁니다. 본 과제처럼 도메인 모양이 명확히 두 갈래라면 union이 더 안전합니다.
    CS · 이론
    • Discriminated Union (Tagged Union): TypeScript의 핵심 패턴. literal type을 식별자로 두고 narrowing을 강제. C 진영의 sum type, Rust의 enum, Haskell의 algebraic data type과 같은 사상.
    • Type narrowing: TypeScript가 if (x.type === 'ONE_WAY') 같은 체크를 만나면 그 분기 안에서 x의 타입을 좁힌다. discriminated union의 가장 자주 쓰이는 활용.
    • Exhaustiveness check: switch 끝에 const _: never = x;를 두면 새 case가 추가됐을 때 컴파일 에러로 알려줌. 본 과제에선 안 썼지만 union이 늘어나면 강력한 안전장치.
    • Make illegal states unrepresentable: Yaron Minsky의 격언. 옵셔널 조합으로 잘못된 상태를 표현 가능하게 두지 말고, 타입으로 막을 수 있는 건 막는다.
  3. Q3-3

    왕복 가는편 선택 후 오는편 후보에서 "가는편 도착보다 일찍 출발하는 티켓"을 disabled 하는데, **같은 날일 때만** 그 룰을 적용한 이유는요?

    핵심 포인트

    • 가는편이 9월 25일 09:50 도착, 오는편이 9월 26일 07:30 출발이면 다른 날이라 시간 비교가 의미 없음(이미 안전)
    • 가는편이 9월 25일 09:50 도착, 오는편 후보가 9월 25일 08:00 출발이면 물리적으로 불가능(같은 날, 도착보다 출발이 빠름) → disabled
    • isSameDay 체크 없이 시간만 비교하면 모든 다른 날 오는편이 잘못 disabled될 위험 있음
    • PDF에는 명시되지 않았지만 합리적 가설(가이드라인 명시 표현)을 세워서 추가한 도메인 룰
    모범 답안먼저 답해보고 펼치기

    이 룰은 PDF 명세에 명시돼 있지 않은 부분이라, 토스 가이드라인의 "스스로 합리적인 가설을 세우고 계속 진행해주세요"에 따라 제가 추가한 도메인 룰입니다.

    상황을 좀 풀어보면, 왕복 흐름에서 사용자가 가는편을 먼저 선택하고 오는편을 고르는 구조입니다. 이때 가는편이 9월 25일 09:50에 도착했는데, 오는편 후보가 9월 25일 08:00 출발이라면 사용자가 아직 도착도 못 한 상태에서 출발하는 셈이니 물리적으로 불가능합니다. 이런 티켓이 선택 가능하게 보이면 사용자가 선택했다가 예약 단계에서 거부당하는 등 혼란이 생길 수 있어서, 후보 단계에서 미리 disabled 처리하는 게 자연스럽다고 봤습니다.

    다만 이 비교를 무조건 시간만으로 하면 문제가 생깁니다. 가는편이 9월 25일 09:50 도착이고 오는편이 9월 26일 07:30 출발이면 시간 숫자만 보면 07:30 < 09:50이라 disabled로 잘못 처리됩니다. 실제로는 다른 날이니 안전한 케이스인데도요. 그래서 TicketItem 안에서 isSameDay를 먼저 체크하고, 같은 날일 때만 가는편 도착시각과 오는편 출발시각을 비교했습니다. 다른 날이면 시간 비교 자체를 건너뛰고 그냥 선택 가능하게 둡니다.

    이 룰이 적용되는 조건을 코드로 보면 tripType === ROUND && departureTicket !== null && isSameDay(가는편 도착, 후보 출발) && 후보 출발 < 가는편 도착 — 네 가지가 모두 만족할 때만 disabled입니다.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. 그럼 가는편 도착이 23:50인데 오는편 후보가 다음날 00:30 출발이면, 같은 날이 아니라 disabled가 안 걸리는데 사실상 위험한 케이스 아닌가요? 환승 시간이 너무 짧잖아요. → 정확합니다. 이 룰은 "물리적으로 불가능한 케이스만 막는다"가 목적이라, 짧지만 가능한 환승은 사용자에게 맡기는 보수적 정책입니다. 더 엄격하게 가려면 "최소 환승 시간 N분"이라는 도메인 정책이 있어야 하는데 그건 실제 KTX 운영 규칙을 알아야 하니 6시간 안에서는 가설로 박지 않고, 명백히 불가능한 케이스만 막는 쪽으로 갔습니다.
    • Q-b. 시간 비교를 Date 객체끼리 < 연산자로 하셨는데 getTime()을 명시적으로 호출하는 게 더 안전하지 않나요?Date < Date는 내부적으로 valueOf()를 호출하는데 결과적으로 getTime()과 같습니다. 다만 의도를 명확히 하기 위해 getTime()을 적는 스타일도 합리적이고, 본 과제에서는 짧은 표현을 우선했습니다. 코드 리뷰에서 합의된 컨벤션을 따르는 게 정답이라고 봅니다.
    • Q-c. 가는편 도착이 정확히 오는편 출발 시각과 같은 경우 — <로 비교하면 같은 시각은 허용되는데, 그것도 사실상 환승 0분이라 불가능 아닌가요? → 맞습니다. 본 과제에서는 strict less-than(<)으로 두어 같은 시각은 허용으로 처리했는데, 좀 더 엄격하게 가려면 <=가 더 안전합니다. 실제 운영 규칙을 알면 수정할 부분이고, README에 가설로 적어두는 게 토스 가이드라인의 "README 등에 문서로 남겨주세요"와 정렬됩니다.
    CS · 이론
    • Domain rule from rational hypothesis: 명세에 없지만 사용자 흐름상 합리적으로 도출되는 룰. 가설로 명시(코드 주석·README) 후 진행하는 게 협업 시 안전.
    • Date 비교의 함정: Date.getTime() vs Date.valueOf() vs 비교 연산자. JS 사양상 동일하게 동작하지만 의도 표현 측면에서 명시적 호출이 안전한 스타일이라는 의견도 있음.
    • Same-day vs strict-time check 분리: 시간 단위 비교는 항상 "같은 단위 안에서만 의미 있다"는 원칙. 다른 날의 시간 비교는 가짜 정렬을 만들 위험.
  4. Q3-4

    편도 ↔ 왕복 전환할 때 이미 선택한 티켓을 reset하는 정책의 근거는요?

    핵심 포인트

    • setTripTypeAtom이 부수효과로 resetSelectedTicketsAtom 호출
    • 편도에서 가는편 티켓을 골랐는데 왕복으로 바꾸면 → 가는편은 유지해도 되지만 오는편이 비어 있어야 함. 가는편을 유지하면 사용자가 다시 갔을 때 동기화 처리가 복잡해질 수 있음
    • 단순화·일관성 우선 — tripType이 바뀌면 티켓은 무효라는 원칙
    • 가는날/편도왕복 같은 검색 조건 변경 시 일관되게 reset (가는날 변경 → 티켓 reset과 같은 패턴)
    모범 답안먼저 답해보고 펼치기

    편도/왕복 전환 시 티켓을 reset하는 결정의 핵심은 단순함입니다. 룰을 단순하게 두면 사용자도 코드도 헷갈릴 일이 적습니다.

    논리적으로는 편도 → 왕복 전환 시 가는편 티켓을 유지해도 됩니다 — 같은 출발역·도착역·날짜로 가는 티켓이니까요. 하지만 그러면 두 가지 문제가 생깁니다. 첫째, 사용자 입장에서 "편도 → 왕복" 전환 후 티켓 페이지에 들어가면 가는편은 골라져 있고 오는편만 비어 있는 어색한 상태가 됩니다. 둘째, 코드 입장에서 "어떤 검색 조건 변경은 reset, 어떤 변경은 보존"이라는 분기를 일일이 관리해야 합니다.

    그래서 정책을 단순하게 통일했습니다 — 검색 조건의 핵심 필드(tripType, 가는날)가 바뀌면 선택한 티켓은 모두 무효. 이걸 atom 차원에서 부수효과로 묶어뒀습니다. setTripTypeAtom setter가 tripType을 갱신하면서 resetSelectedTicketsAtom을 호출하고, setDepartureDateAtom도 가는날이 실제로 바뀌었을 때 같은 reset을 호출합니다.

    이 통일된 룰의 장점은 호출처에서 신경 쓸 게 없다는 점입니다. TripTypeTab은 그냥 setTripType(next)만 호출하면 되고, DateFieldonConfirm={setDepartureDate}만 연결하면 됩니다. reset이 필요한지 호출처가 판단할 일이 없으니 한 군데서 빼먹을 위험도 사라집니다.

    다만 이 정책의 비용도 인정합니다. 사용자가 편도 → 왕복으로 바꾸려는 의도가 단순히 "오는편 추가"였다면 가는편을 다시 골라야 하는 작은 마찰이 생깁니다. 본 과제에서는 그 마찰보다 정책 단순함의 가치가 더 크다고 봤지만, 실제 production이라면 사용 패턴 데이터를 보고 결정할 부분이라고 생각합니다.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. 가는날만 살짝 바꿨을 때(예: 9월 25일 → 26일) 왕복의 오는날도 같이 reset되나요? → 가는날 변경 시 reset되는 건 선택한 티켓입니다. 오는날 자체는 별도 atom이고, 가는날이 변경됐을 때 오는날이 가는날보다 이전이 됐다면 그때만 자동으로 undefined로 만듭니다. 즉 가는날이 26일로 미뤄지고 오는날이 27일이라면 오는날은 그대로 유지됩니다.
    • Q-b. 만약 정반대 정책 — "전환 시 티켓 보존"이 요구사항이었다면 어떻게 구현했을까요? → 부수효과를 빼고 atom을 단순 setter로 두면 됩니다. 그리고 화면에서 "왕복으로 바꾸셨네요. 가는편은 유지되고 오는편만 새로 골라주세요" 같은 안내 토스트를 띄우는 게 사용자 혼란을 줄이는 표준 패턴입니다. 정책 자체가 코드보다 UX 가이드를 더 요구합니다.
    • Q-c. 출발역/도착역 변경 시에도 티켓 reset이 필요하지 않나요? 그건 안 하는 이유가 있나요? → 좋은 지적입니다. 현재 setDepartureStationAtom/setArrivalStationAtom은 reset을 호출하지 않는데, 사실 출발역·도착역이 바뀌면 기존 티켓도 더 이상 의미가 없으니 reset되는 게 일관됩니다. 본 과제에서는 station 선택 직후 SearchPage로 돌아가고 그 시점에 사용자가 다시 "기차 보기"를 눌러 TicketsPage로 진입하는 흐름이라 자연스럽게 새로 fetch되긴 하지만, 엄밀히는 station 변경 setter에도 reset을 묶는 게 정합성 측면에서 더 안전합니다. 이 부분은 인정하고 답변할 포인트입니다.
    CS · 이론
    • Cascading invalidation: 상위 데이터가 바뀌면 종속 데이터를 자동 무효화. SQL의 ON DELETE CASCADE, react-query의 invalidateQueries, jotai의 cross-atom side effect가 모두 같은 사상.
    • Policy uniformity: 비슷한 변경에 대해 같은 정책을 적용하면 호출처에서 신경 쓸 분기가 줄어든다. SRP의 한 응용.
    • Cost of policy choice: 단순함의 비용은 사용자 마찰. 어느 쪽을 우선할지는 도메인 컨텍스트와 사용 패턴에 따라.
  5. Q3-5

    `TicketsPage`를 한 라우트 + 모드 전환으로 처리한 게 사용자 입장에서 직관적인가요? 시각적으로 같은 화면이 두 번 등장하는 셈인데요.

    핵심 포인트

    • 시각 변화는 헤더 텍스트("가는 기차 → 오는 기차")와 표시 날짜(가는날 → 오는날)에서 명확
    • 토스의 일반적 모바일 흐름이 "한 화면에서 단계별로 진행"이라 흐름 자체는 익숙한 패턴
    • 더 명확하게 하려면 transition 효과 / 짧은 토스트 / 단계 인디케이터 같은 UX 보강 가능
    • README에 UX 개선 후보로 기록하는 게 토스 가이드라인의 "README 등에 문서로 남겨주세요"와 정렬
    모범 답안먼저 답해보고 펼치기

    이 부분은 트레이드오프를 인정하고 답변하는 게 맞다고 봅니다.

    먼저, 시각적 변화는 분명히 있습니다 — 헤더 텍스트가 "가는 기차를 선택해주세요"에서 "오는 기차를 선택해주세요"로, 표시 날짜가 가는날에서 오는날로, 그리고 출발/도착역 표기가 swap됩니다. 사용자가 아무 변화도 느끼지 못하는 건 아닙니다. 게다가 토스의 일반적 모바일 흐름은 한 화면에서 단계별로 진행되는 패턴이 익숙해서, "가는편 선택 → 오는편 선택"이 같은 화면에서 이어지는 건 사용 흐름상 자연스럽다고 봤습니다.

    다만 더 명확하게 만들 여지는 있습니다. 후보로는 (1) 가는편 선택 직후 짧은 토스트(가는편이 선택됐어요. 이제 오는편을 골라주세요)를 띄우거나, (2) 화면 상단에 "1/2 가는편" → "2/2 오는편" 같은 단계 인디케이터를 두거나, (3) phase 전환 시 화면을 살짝 슬라이드하는 transition을 넣는 방법이 있습니다. 6시간 제약 안에서는 핵심 기능을 우선했고 이 UX 개선은 README에 후보로 기록해두는 게 토스 가이드라인의 "사용자 경험을 개선하고 싶은 지점이 있다면 ... README 등에 문서로 남겨주세요"와 정렬된다고 봤습니다.

    또 하나 짚을 부분이 있는데, 라우트를 분리했을 때의 비용도 균형 있게 봐야 합니다. /tickets/outbound/tickets/inbound로 나누면 사용자 입장에서 URL이 달라지니 단계가 시각적으로 명확해지는 장점이 있는 반면, 뒤로가기 동작·딥링크 진입 가드·atom과 URL의 동기화 같은 결정이 추가로 따라옵니다. 본 과제 규모에서는 한 라우트 + UX 보강이 시간 대비 효율이 더 낫다고 판단했습니다.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. 만약 사용자가 가는편 선택 후 오는편 화면에서 뒤로가기를 누르면 어떻게 되나요? → 라우터 기본 동작으로 SearchPage로 돌아갑니다. 다만 departureTicket atom이 살아 있으니, 다시 SearchPage에서 "기차 보기"로 진입하면 atom 상태 조합상 isReturnPhase가 true로 평가돼서 오는편 화면이 그대로 보일 가능성이 있습니다. 이게 기대하는 동작인지(사용자는 가는편을 다시 고르려고 뒤로 누른 거라면 의도와 다름) 명확하게 하려면 뒤로가기 시 departureTicket을 reset하는 가드를 넣거나, 라우트를 분리해서 history를 더 정확히 만드는 게 안전한 처리입니다. 이 케이스는 명확한 결정을 내려두지 못한 영역이라 README to-do 후보로 적절합니다.
    • Q-b. 라우트가 같으면 분석/추적 측면에서 가는편/오는편 단계를 구분하기 어렵지 않나요? → 정확합니다. GA나 Amplitude 같은 분석 도구에서 URL 기반으로 funnel을 만들 때 한 URL이라 단계 구분이 약해집니다. 그럴 땐 page view를 URL 대신 명시적 이벤트(ticket_outbound_viewed, ticket_inbound_viewed)로 트래킹하거나, query string에 ?phase=outbound 같은 비식별 파라미터를 추가해 분석 보조용으로 쓰는 패턴이 일반적입니다. 본 과제에는 분석 요구가 없어서 다루지 않았지만, production이라면 짚을 부분입니다.
    CS · 이론
    • Mode-based UI vs Route-based UI: 같은 화면 구조에 모드만 바뀌는 흐름은 mode-based가 단순. 시각이 크게 다르거나 history·딥링크 의미가 강하면 route-based.
    • Stepper/Wizard pattern: 단계 진행 UI에서 단계 인디케이터로 사용자 인지를 보강하는 표준 패턴.
    • README as design log: 결정의 근거와 미구현 영역을 README에 기록하는 게 협업 시 가장 적은 비용으로 컨텍스트를 전하는 방법.