LabHub
배우기 러닝패스 코스

고래를 찾는데 고양이가 나타났다 · 검색창에는 세 개의 시간이 흐른다 · 이론

고래를 찾는데 고양이가 나타났다

LabHub 에서 이어서 보기

한 줄 요약

검색창의 입력, 서버에 보낸 요청, 화면에 표시한 결과는 서로 다른 시점의 정보다. 세 가지를 하나의 문자열로 생각하면 느린 과거 응답이 최신 화면을 차지한다.

왜 이게 필요했나

달 기지 도서관에서 고양이를 검색했다가 곧바로 고래로 바꿨다. 관측소 두 곳에 요청이 갔고 고래 응답이 먼저 왔다. 화면에 고래가 나타난 직후, 오래 기다린 고양이 응답이 도착했다. 오류 창도 없고 HTTP 응답도 성공인데 화면에는 틀린 자료가 남는다. 이것은 검색 알고리즘보다 누가 지금 화면을 바꿀 자격이 있는가의 문제다.

여기서 검색 단추를 비활성화하면 현상은 숨길 수 있다. 하지만 사용자는 오타를 고치거나 생각을 바꿀 수 있어야 한다. 기다리게 만드는 것이 제품 요구에 맞는지 먼저 판단해야 한다. 이 실험에서는 새 검색을 허용하고, 더 이상 유효하지 않은 응답을 버리는 정책을 택한다.

어떻게 동작하나

입력 중인 draft는 아직 제출되지 않은 문장이다. 제출된 요청에는 검색어와 제출 번호를 둔다. 같은 검색어를 다시 제출해도 새 의도가 될 수 있기 때문이다. 화면 상태는 다음처럼 구별한다.

| 상태 | 화면이 알고 있는 것 | 사용자에게 필요한 안내 |
| --- | --- | --- |
| idle | 아직 제출된 검색이 없음 | 검색어 입력 |
| loading | 특정 요청의 응답 대기 | 무엇을 기다리는지 |
| success | 요청에 맞는 결과 목록 | 어떤 검색의 결과인지 |
| empty | 응답은 성공했지만 결과 없음 | 검색어 변경 |
| error | 요청 실패와 원인 | 다시 시도할 방법 |

loading: boolean, error: boolean, items: Result[]를 따로 관리하면 로딩과 오류가 동시에 켜진 조합도 만들 수 있다. kind를 기준으로 나뉜 유니온 타입은 각 상태가 가져야 하는 정보를 묶는다. success에서만 items를 읽고 error에서만 message를 읽게 하면 렌더 함수에서도 모순을 발견하기 쉽다. 다만 타입 검사는 응답 순서를 보장하지 않는다. 고양이와 고래는 둘 다 올바른 타입의 결과이기 때문이다.

사건 표를 먼저 써 보자. 번호는 밀리초가 아니라 순서다.

| 사건 | 내용 | 표시할 검색 |
| --- | --- | --- |
| 1 | 고양이 요청 | 고양이 대기 |
| 2 | 고래 요청 | 고래 대기 |
| 3 | 고래 응답 | 고래 결과 |
| 4 | 고양이 응답 | 여전히 고래 결과 |

마지막 행이 구현해야 할 제품 정책이다. 응답이 성공했다는 사실과 지금도 유효하다는 사실을 분리해서 설명할 수 있어야 한다.

데이터가 아니라 가능한 상태를 설계하기

다음 두 상태를 비교해 보자. 예시의 문자열은 형식을 설명하기 위한 표본이며 실제 응답 값은 요청에 따라 달라진다.

type View =  | { kind: 'idle' }  | { kind: 'loading'; query: string }  | { kind: 'success'; query: string; titles: string[] }  | { kind: 'error'; query: string; message: string };const waiting: View = { kind: 'loading', query: '별 지도' };const failed: View = {  kind: 'error', query: '별 지도', message: '자료실에 연결할 수 없습니다'};

두 상태를 동시에 켜는 대신 현재 상태 하나를 고른다. error를 선택했다면 어떤 요청이 실패했는지와 안내 문장이 함께 있어야 한다. 실제 실습은 여기에 empty를 더한다. 새 상태를 추가할 때는 타입 정의만 바꾸는 것으로 끝내지 않고, 화면 문장과 전이 검사도 함께 찾아야 한다.

실패 뒤 재시도할 때 loading으로 이동했는데 옛 오류 문장이 계속 보인다면 상태와 화면의 연결이 어긋난 것이다. 새 요청을 만드는 함수만 보지 말고 렌더 분기가 어떤 필드를 읽는지 확인하자. 반대로 결과가 먼저 사라지는 것이 언제나 정답은 아니다. 이전 결과를 남긴 채 새 결과를 기다리는 제품도 있다. 그 정책을 택한다면 이전 결과라는 표시와 현재 요청 상태를 표현하는 타입이 추가로 필요하다. 이 실험은 이전 결과를 비우는 단순한 정책으로 경합에 집중한다.

현장에서 만나는 모습

검색뿐 아니라 주소 자동완성, 상품 옵션별 재고, 지도 위치 변경, 고객별 상세 패널에서도 같은 문제가 생긴다. FDE가 고객 시연 중 화면이 가끔 과거 값으로 바뀌는 현상을 만나면, 먼저 입력 시각과 응답 시각을 함께 기록해야 한다. 실패 로그만 찾으면 성공한 오래된 응답을 놓친다.

2026-09-11 확인한 [Canonical Web Developer 공고](https://canonical.com/careers/2804965)는 TypeScript·반응형 UI·접근성·복잡한 UI 성능을 요구하고, 대규모 React+TypeScript 경험은 우대로 구분한다. 이 과제는 그 요구를 작은 관찰 가능한 작업으로 옮긴 것이다. EMEA 공고 하나가 전체 시장의 채용 빈도나 합격을 보장하지는 않는다.

직접 확인할 것

실험실의 요청 대기열에서 고래를 먼저, 고양이를 나중에 배달한다. 입력창과 결과 제목을 각각 읽고 어느 시점에서 둘이 어긋났는지 기록한다. 시작 코드의 타입 검사가 통과하는데도 왜 화면은 잘못될 수 있는지 한 문장으로 설명해 보자.

타입 표현은 [TypeScript의 discriminated unions 설명](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions)을 참고한다.