SearchCommand
검색 입력, 결과 리스트, 키보드 탐색, 데스크톱 모달/모바일 전체 화면 검색 표면을 하나로 묶은 재사용 검색 컴포넌트입니다. 데이터 조회·랭킹·라우팅은 소비자가 소유합니다.
마지막 업데이트 2026-07-02
한눈에#
검색 결과 표면 — 데스크톱에서는 모달 팔레트, 모바일에서는 전체 화면 검색으로 전환됩니다. 검색 데이터는 외부에서 items로 주입합니다.
검색 결과 표면을 열어 키보드 탐색을 확인하세요.
- controlled query
- result snippets
- keyboard navigation
- mobile full screen
검색어와 결과는 소비자가 소유하고, SearchCommand는 표면·목록·키보드 계약을 책임집니다
사용 시점#
검색 결과를 즉시 보여주고 선택하면 문서·페이지·엔티티로 이동하는 표면이면 SearchCommand입니다.
권장 — 이렇게 쓰세요
지양 — 이러지 마세요
검색 결과 표면을 열어 키보드 탐색을 확인하세요.
쓴다 — 문서 검색, 엔티티 검색, 설정 검색처럼 결과 스니펫과 위치 설명이 필요한 검색
대신 SearchField — 결과 화면 없이 검색어만 입력받아 폼 제출·필터링할 때
대신 Command — 검색 결과가 아니라 명령 실행 목록을 좁혀 선택할 때
기능 정리#
open,query,items를 제어 prop으로 받아 제품별 데이터 조회·랭킹·라우팅과 분리합니다.SearchCommandItem은title,href,subtitle,snippet,matchLabel,marker,badge,removeLabel을 지원합니다.query와 일치하는 제목·스니펫 텍스트는 결과 안에서 강조되고, 소비자가 넘긴matchLabel로별칭·키워드·본문같은 매칭 위치를 설명할 수 있습니다.ArrowUp/ArrowDown으로 이동하고Enter로 선택합니다.- 최근 항목처럼
removeLabel이 있는 결과는 활성 상태에서Delete로 제거합니다. 검색어가 비어 있을 때는Backspace도 제거 동작으로 처리합니다. - 비활성 결과는 hover·키보드 탐색·Enter 선택에서 제외되고, 키보드로 이동한 활성 결과는 보이는 영역 안으로 스크롤됩니다.
Escape, 배경 클릭, 닫기 버튼으로 닫습니다.- 767px 이하에서는 safe-area를 반영한 전체 화면 표면으로 전환하고, 닫기·삭제 컨트롤은 44px 터치 타깃을 유지하며 단축키 힌트는 숨깁니다.
조립 규칙#
SearchCommand는 검색 인덱스를 만들지 않습니다. 제품 코드는 아래 네 가지를 소유합니다.
- 인덱스 로드와 freshness 정책
- 쿼리 점수 계산과 결과 정렬
- 선택 후 라우팅 또는 액션 실행
- 검색 이벤트 수집과 운영 리포트
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
<SearchCommand
open={open}
query={query}
items={rankedItems}
onQueryChange={setQuery}
onSelect={(item) => {
if (item.href !== undefined) router.push(item.href);
}}
onClose={() => setOpen(false)}
/>;
포털 문서 검색은 이 조립 규칙을 따르는 레퍼런스 구현입니다. portal/lib/search/index-builder.ts가
MDX frontmatter의 aliases와 keywords까지 인덱싱하고, portal/lib/search/snippets.ts가
실제 매칭된 단어를 스니펫으로 돌려줍니다. 운영 관측은 SearchCommand 안에 넣지 않고
셸(SearchDialog)에서 /api/search-events로 전송합니다.
Props#
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
open | boolean | — | Controls whether the search surface is open. |
query | string | — | Controlled search query. |
items | readonly SearchCommandItem[] | — | Ranked result items supplied by the consuming app. |
onQueryChange | (query: string) => void | — | Called whenever the search input changes. |
onSelect | (item: SearchCommandItem) => void | — | Called when the active/selectable item is chosen. |
onClose | () => void | — | Called by Escape, backdrop press, or close button. |
onRemoveItem | (item: SearchCommandItem) => void | — | Called when a removable active/recent item is deleted. |
label | string | 검색 | Dialog label. |
inputLabel | string | 검색어 | Search input label. |
placeholder | string | 검색어 입력 | Search input placeholder. |
listLabel | string | 검색 결과 | Result list label. |
closeLabel | string | 검색 닫기 | Close button label. |
statusText | ReactNode | 결과가 없습니다. | Empty-state content. |
groupHeading | string | — | Optional heading displayed above grouped results. |
className | string | — | Additional panel class name. |
ref | Ref<HTMLDivElement> | — | Ref for the dialog panel. |
접근성#
- 루트는
role="dialog"+aria-modal="true"를 사용합니다. - 입력은
role="combobox"이고 결과 목록은role="listbox"/role="option"입니다. - 활성 결과는
aria-activedescendant로 입력과 연결됩니다. - 포커스는 열릴 때 입력으로 이동하고 닫힐 때 이전 포커스로 돌아갑니다.
- 최근 항목의 시각적 삭제 아이콘은 보조 액션으로만 쓰고, 키보드 사용자는 활성 결과에서
Delete또는 빈 검색어의Backspace로 제거합니다. - 모바일에서도 같은 키보드 계약을 유지하되, 화면에는 단축키 힌트를 노출하지 않습니다.
prefers-reduced-motion: reduce에서는 exit timer를 기다리지 않고 즉시 unmount해 focus trap·scroll lock을 바로 해제합니다.
토큰#
component 토큰 없이 surface/input/listbox semantic 토큰을 직접 소비합니다.
| 속성 | 토큰 |
|---|---|
| 표면 | color.surface-raised · color.border · radius.lg · shadow.xl |
| 모바일 표면 | 100dvh 전체 화면 · safe-area-inset-top/bottom · radius: 0 · 그림자 없음 |
| 입력 | font.size-body-1 · color.text · color.text-placeholder |
| 활성 결과 | color.surface-selected · color.primary-text · active option scrollIntoView(nearest) |
| 터치 타깃 | 닫기 버튼/모바일 삭제 컨트롤/결과 행 control.height-md |
| 스니펫 | font.size-caption · color.text-subtle |
| 매치 강조 | color.primary-text · underline |
| 등장/퇴장 모션 | panel spring.spatial.fast · overlay/exit spring.effect.default · closing 상태 후 focus 복원 |