지난 글에서 정리한 RADIO 프레임워크 적용사례로, 주제는 자동완성 검색창이다.
Input과 Dropdown만 있는 작은 UI지만, 요구사항을 제대로 짚어보면 생각보다 다룰 게 많다.
문제 정의
구글 검색창을 예로 든다. 사용자가 app을 입력하면 apple, apple store, apple watch, app store, apple stock 같은 추천어가 떠야 하고, 추천어를 클릭하거나 Enter를 누르면 해당 검색어로 검색 결과 페이지로 이동한다. 면접에서도 자주 나오고, 실무에서도 흔히 구현하게 되는 케이스다.
흔한 접근: Input 컴포넌트를 만들고, onChange에서 API를 호출하고, Dropdown에서 결과를 보여준다.
-> 틀린 방식은 아니지만, 이렇게만 설계하면 몇 가지가 빠진다.
- 사용자가 빠르게 타이핑할 때 API 호출을 어떻게 줄일 것인가
- 느린 네트워크와 빠른 타이핑이 겹치면서 생기는 race condition을 어떻게 처리할 것인가
- 추천어를 캐싱할 것인가
- 키보드 접근성을 어떻게 챙길 것인가
이 다섯 가지를 RADIO 순서로 짚어보자
- 입력 이벤트를 어떻게 제어할 것인가
- 서버 API 호출을 어떻게 줄일 것인가
- 느린 네트워크와 빠른 타이핑 사이의 race condition을 어떻게 처리할 것인가
- 캐싱과 stale data를 어떻게 다룰 것인가
- 키보드 접근성, 검색 지연 시간, 실패율을 어떻게 모니터링하고 설계할 것인가
*질문을 많이 던지는 것 자체가 목적이 아니라 질문 하나하나가 설계 선택으로 이어져야 의미가 있다.
R - Requirements
기능 요구사항(Functional Requirements)은 다음과 같이 정리된다.
- 사용자가 검색어를 입력하면 추천어 목록을 보여준다
- 추천어는 입력값이 바뀔 때마다 업데이트된다
- 추천어를 클릭하면 해당 검색어로 검색한다
- 키보드 위/아래로 추천어를 이동할 수 있다
- Enter를 누르면 현재 선택된 추천어 또는 입력값으로 검색한다
- Escape를 누르면 추천어 목록을 닫는다
- 검색어가 너무 짧으면 추천어를 보여주지 않는다
비기능 요구사항(Non-Functional Requirements)은 다음과 같다.
- 추천어는 빠르게 보여야 한다
- API 호출을 과도하게 보내면 안 된다
- 느린 네트워크에서도 동작해야 한다
- 모바일과 데스크톱을 모두 지원한다
- 접근성을 지원한다
- API가 실패해도 검색창 자체는 계속 사용할 수 있어야 한다
몇 글자부터 추천어를 보여줄지는 단순한 UX 질문처럼 보이지만 실제로는 서버 부하와 직결된다. 한 글자만 입력해도 API를 호출하면 a 같은 넓은 검색어에서 트래픽이 크게 늘어날 수 있어서, 보통 두 글자나 세 글자 이상부터 요청을 보내도록 설계한다.
개인화 여부도 마찬가지다. 개인화가 필요 없다면 CDN이나 Edge Cache를 적극적으로 쓸 수 있다. 개인화가 필요하면 사용자마다 결과가 달라지기 때문에 캐싱 전략이 훨씬 복잡해진다.
검색어 랭킹을 프론트엔드가 결정할지 서버가 결정할지도 이 단계에서 물어볼 만한 질문이다.
+ 근데 이 부분이 좀 의아해서 추가로 찾아보니 보통은 서버가 결정한다고 한다. 이유는 랭킹을 정하려면 전체 사용자의 검색 로그 (이 검색어 다음에 실제로 뭘 많이 클릭했는지), 인기 검색어 통계, 개인화 데이터 (이 사용자의 검색/구매 이력), 검색 인덱스, 상품 재고나 노출 우선순위 같은 비즈니스 로직이 필요한데 이걸 클라이언트에서 계산하기엔 연산이 많기도 하고 이 정보들은 전부 서버(혹은 서버가 접근하는 DB, 검색 엔진)에만 있어서. 프론트엔드로 다 내려보내려면 개인정보나 전체 로그 데이터를 클라이언트에 노출해야 해서 보안 문제도 생기고, 페이로드도 커진다. 그래서 서버가 이미 정렬까지 끝낸 상태로 suggestions 배열을 내려주고, 프론트엔드는 그 순서를 그대로 렌더링하기만 한다.
+ 랭킹 로직은 실무에서 꽤 자주 바뀐다. A/B 테스트로 알고리즘을 계속 튜닝하기 때문. 서버에 있으면 서버 코드/모델만 배포하면 끝나는데, 프론트엔드가 랭킹을 한다면 로직이 바뀔 때마다 클라이언트 배포까지 해야 해서 훨씬 번거로워진다.
+ 예외적으로 프론트엔드가 재정렬하는 경우는 있긴하다. 예를 들면 VS Code의 Quick Open처럼 데이터가 이미 로컬(파일 목록)에 있고 서버 왕복이 아예 없는 경우, 혹은 서버가 내려준 리스트 안에서 아주 가벼운 클라이언트 로컬 신호(방금 사용자가 클릭한 항목을 살짝 위로 올리는 정도)만 반영하는 경우 정도인데, 이건 "랭킹을 결정한다"기보다는 이미 정해진 순서에 미세 조정을 얹는 수준이라, 이 글에서 말하는 랭킹 결정권과는 다른 이야기
A - Architecture
전체 흐름은 아래와 같다.
User
│ typing
▼
Search Input
│ debounce
▼
Autocomplete Controller
│ check cache
▼
Suggestion API Client
│ HTTP GET /suggest?q=app
▼
ranking, search index, popular queries (서버)
│
▼
Suggestion Response
│ 장애 대응: timeout, fallback
▼
Dropdown UI
자동완성 검색창은 HTTP 요청-응답 방식으로, 사용자가 입력이라는 이벤트를 먼저 발생시키고, 그에 대한 응답을 한 번 받아오는 구조다.
호출 제어는 디바운스로, 부하 감소는 캐시로, 장애 대응은 타임아웃과 fallback으로 처리한다.
사용자가 a, ap, app, appl, apple을 순서대로 빠르게 입력할 때, 매 글자마다 API를 호출하면 낭비가 크기고 서버부하가 생기니 디바운스도 걸어준다.(보통 200ms~ 300ms)
D - Data / API
API는 다음과 같이 설계한다.
GET /suggest?q=app&limit=5
type Suggestion = {
id: string;
text: string;
type: "query" | "product" | "brand" | "history";
};
type SuggestionResponse = {
query: string;
suggestions: Suggestion[];
ttlms: number;
};
race condition
자동완성 검색창에서 race condition이 생기는 이유는 이렇다. 사용자가 app을 입력해서 요청을 보내고, 그 직후 apple을 입력해서 또 요청을 보낸다. 네트워크 상황에 따라 apple에 대한 응답이 app에 대한 응답보다 먼저 도착할 수 있다. 이때 마지막에 도착한 응답을 그대로 화면에 반영하면, 사용자는 apple을 입력했는데 추천어는 app 기준으로 보이는 상황이 생긴다.
이걸 막으려면 응답이 현재 입력값과 일치하는지 확인하고, 일치하지 않는 stale response는 무시해야 한다. 새 요청이 시작되면 이전 요청을 취소하는 것도 같은 목적이다.
state 설계
상태는 성격에 따라 나눈다.
| 상태 | 유형 정의 | 예시 |
| Local UI State | 클라이언트가 들고 있는 상태 | inputValue, isOpen, highlightedIndex |
| Server State | 서버 응답 기반 상태 | suggestions data, isLoading, error |
| Derived State | 계산 가능한 상태 | canShowDropdown, hasSuggestion |
| URL/Navigation State | 검색 결과 페이지에서 유지되어야 하는 상태 | 검색어, 필터 |
상태의 소유권과 수명 주기에 따라 상태 관리방법을 선택한다. 이렇게 나누면 어떤 상태를 어디에 저장할지가 자연스럽게 결정된다.
핵심장치만 다시 비교
셋 다 자동완성 검색에서 다루는 핵심 장치지만, 목적이 다르다.
- 디바운스: 서버 부하를 줄이기 위한 장치. a 입력 시에는 요청을 보내지 않고, ap는 300ms를 기다리고, app을 입력하면 이전 대기를 취소하고 새로 요청한다.
- 캐시: 중복 요청을 줄이기 위한 장치. app을 입력해서 API를 호출하고, apple로 바꿔서 또 호출한 뒤, 다시 app으로 돌아오면 캐시를 사용한다.
- race condition 방지: 정확성을 위한 장치. 이전 요청이 나중에 도착해도 현재 입력값과 맞지 않으면 무시한다.
I - Interface
컴포넌트는 책임 기준으로 나눈다.
- AutoComplete: 전체 상태를 연결
- SearchInput: 입력, 포커스, 키보드 이벤트 담당
- SuggestionDropdown: 추천어 목록 표시
- SuggestionItem: 개별 추천어 렌더링
- loading / empty / error 상태
- useAutoComplete: 디바운스, fetch, 캐시, 선택 로직을 담는 훅
단순히 인풋과 드롭다운으로 나누는 것보단 컴포넌트의 책임을 기준으로 나누는 쪽이 낫다고 한다. 로직이 컴포넌트 안에 다 들어가 있으면 테스트하기 어렵고 재사용하기도 어렵다. useAutoComplete처럼 훅으로 분리하면 UI가 바뀌어도 핵심 로직은 그대로 유지된다.
UI 상태는 다음과 같이 나뉜다.
- 기본값: 검색창만 표시
- 입력 중: loading indicator
- 결과 있음: dropdown 표시
- 결과 없음: "추천 검색어가 없습니다"
- 에러 발생: 조용히 fallback 처리 혹은 retry
- 선택: highlighted item 표시
키보드 인터랙션은 이렇게 정의한다.
- ArrowDown: 다음 추천어로 이동
- ArrowUp: 이전 추천어로 이동
- Enter: 선택된 추천어로 검색
- Escape: dropdown 닫기
- Tab: 자연스러운 focus 이동
- Input에는 ARIA 속성을 적용
자동완성 검색창은 마우스만으로 쓰는 UI가 아니기 때문에, 키보드만으로도 전체 흐름을 사용할 수 있어야 하고, 사용자가 지금 어떤 추천어가 선택되어 있는지 인지할 수 있어야 한다. 이 부분은 W3C의 Combobox 패턴 문서에 키보드 인터랙션과 ARIA 속성이 상세히 정리되어 있어서, 실제 구현할 때 참고할 만하다.
O - Optimization & Observability
성능 지표는 다음을 본다.
- time to first suggestion
- suggestion API latency
- suggestion API error rate
- cache hit rate
- drop off rate
- search conversion rate
운영 관점에서 고려할 것들은 다음과 같다.
- API가 실패해도 검색창 자체는 계속 동작해야 한다. 추천어가 안 뜨더라도 사용자가 직접 검색어를 입력하고 Enter를 눌러 검색할 수 있어야 한다
- 추천 API에는 타임아웃을 설정한다
- 디바운스 값은 A/B 테스트 대상이 될 수 있다. 100ms는 반응이 빠르지만 API 호출이 늘어나고, 300ms는 서버 부하는 줄지만 사용자는 느리다고 느낄 수 있다
- 추천어 클릭률을 측정할 수 있다
- 봇이나 어뷰징에 대한 방어, feature flag를 통한 점진적 롤아웃도 운영 전략에 들어간다
프론트엔드 시스템 디자인을 다룰 때 기능이 동작하는 시점에서 끝내는 경우가 많은데 배포 후에 어떻게 관리할지도 설계에 포함되어야 하는 부분이다.
흐름 정리
- 사용자가 검색어를 입력하면 로컬 input state가 먼저 업데이트된다
- 검색어가 너무 짧으면 API를 호출하지 않는다
- 입력이 멈춘 뒤 200~300ms가 지나면 디바운스된 요청이 나간다
- 요청을 보내기 전 클라이언트 캐시를 먼저 확인한다
- 새 요청이 시작되면 이전 요청을 취소하고, 응답이 도착하면 현재 입력값과 query가 일치하는지 확인해 stale response를 걸러낸다
- UI는 dropdown을 렌더링하고, 마우스 클릭과 키보드 이동, 스크린 리더 접근성을 모두 지원한다
- 운영 단계에서는 추천어 레이턴시, API 에러율, 캐시 히트율, 검색 전환율을 측정한다
위의 조건들을 모두 적용해서 위키피티아 api 로 한번 만들어봤는데 공부 많이 된다. 🧐
'Frontend' 카테고리의 다른 글
| 프론트엔드 시스템 디자인 (RADIO 접근법) (1) | 2026.07.15 |
|---|---|
| GraphQL Apollo Server/Client Cheat Sheet (0) | 2026.06.15 |
| 모두를 위한 웹을 만든다는 것, Accessibility (0) | 2026.05.13 |
| 메모이제이션(Memoization)의 원리와 함수 구현해보기, 적용 (2) | 2025.08.30 |
| CursorAI + MCP 로 생산성 100배 부스트하기 (Sequential Thinking과 TalkToFigma) (0) | 2025.04.07 |
댓글