ComponentsP3 본문

SearchCommand

검색 입력, 결과 리스트, 키보드 탐색, 데스크톱 모달/모바일 전체 화면 검색 표면을 하나로 묶은 재사용 검색 컴포넌트입니다. 데이터 조회·랭킹·라우팅은 소비자가 소유합니다.

마지막 업데이트 2026-07-02

한눈에#

검색 결과 표면 — 데스크톱에서는 모달 팔레트, 모바일에서는 전체 화면 검색으로 전환됩니다. 검색 데이터는 외부에서 items로 주입합니다.

검색 결과 표면을 열어 키보드 탐색을 확인하세요.

  • controlled query
  • result snippets
  • keyboard navigation
  • mobile full screen

검색어와 결과는 소비자가 소유하고, SearchCommand는 표면·목록·키보드 계약을 책임집니다

사용 시점#

검색 결과를 즉시 보여주고 선택하면 문서·페이지·엔티티로 이동하는 표면이면 SearchCommand입니다.

권장 — 이렇게 쓰세요

지양 — 이러지 마세요

검색 결과 표면을 열어 키보드 탐색을 확인하세요.

쓴다문서 검색, 엔티티 검색, 설정 검색처럼 결과 스니펫과 위치 설명이 필요한 검색

대신 SearchField결과 화면 없이 검색어만 입력받아 폼 제출·필터링할 때

대신 Command검색 결과가 아니라 명령 실행 목록을 좁혀 선택할 때

기능 정리#

  • open, query, items를 제어 prop으로 받아 제품별 데이터 조회·랭킹·라우팅과 분리합니다.
  • SearchCommandItemtitle, href, subtitle, snippet, matchLabel, marker, badge, removeLabel을 지원합니다.
  • query와 일치하는 제목·스니펫 텍스트는 결과 안에서 강조되고, 소비자가 넘긴 matchLabel별칭·키워드·본문 같은 매칭 위치를 설명할 수 있습니다.
  • ArrowUp/ArrowDown으로 이동하고 Enter로 선택합니다.
  • 최근 항목처럼 removeLabel이 있는 결과는 활성 상태에서 Delete로 제거합니다. 검색어가 비어 있을 때는 Backspace도 제거 동작으로 처리합니다.
  • 비활성 결과는 hover·키보드 탐색·Enter 선택에서 제외되고, 키보드로 이동한 활성 결과는 보이는 영역 안으로 스크롤됩니다.
  • Escape, 배경 클릭, 닫기 버튼으로 닫습니다.
  • 767px 이하에서는 safe-area를 반영한 전체 화면 표면으로 전환하고, 닫기·삭제 컨트롤은 44px 터치 타깃을 유지하며 단축키 힌트는 숨깁니다.

조립 규칙#

SearchCommand는 검색 인덱스를 만들지 않습니다. 제품 코드는 아래 네 가지를 소유합니다.

  1. 인덱스 로드와 freshness 정책
  2. 쿼리 점수 계산과 결과 정렬
  3. 선택 후 라우팅 또는 액션 실행
  4. 검색 이벤트 수집과 운영 리포트
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의 aliaseskeywords까지 인덱싱하고, portal/lib/search/snippets.ts가 실제 매칭된 단어를 스니펫으로 돌려줍니다. 운영 관측은 SearchCommand 안에 넣지 않고 셸(SearchDialog)에서 /api/search-events로 전송합니다.

Props#

Prop타입기본값설명
openbooleanControls whether the search surface is open.
querystringControlled search query.
itemsreadonly SearchCommandItem[]Ranked result items supplied by the consuming app.
onQueryChange(query: string) => voidCalled whenever the search input changes.
onSelect(item: SearchCommandItem) => voidCalled when the active/selectable item is chosen.
onClose() => voidCalled by Escape, backdrop press, or close button.
onRemoveItem(item: SearchCommandItem) => voidCalled when a removable active/recent item is deleted.
labelstring검색Dialog label.
inputLabelstring검색어Search input label.
placeholderstring검색어 입력Search input placeholder.
listLabelstring검색 결과Result list label.
closeLabelstring검색 닫기Close button label.
statusTextReactNode결과가 없습니다.Empty-state content.
groupHeadingstringOptional heading displayed above grouped results.
classNamestringAdditional panel class name.
refRef<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 복원