Skip to content

검색 입력 반응성 — 입력 불가 버벅임에서 자모 단위 안정 검색까지 (2026-08-25)

  • 기간: 2026-08-25 하루 (1세션, 오너 실기기 확인 루프 — "검색창이 갑자기 너무 엄청나게 버벅여서 아무것도 입력할 수가 없어"에서 시작해 보고 6건 연쇄)
  • 랜딩: PR #717 · #723 · #746 · #752 · #762 · #764 · #766 · #768 · #770 — 마이그레이션 0(전부 클라), 매 PR Vercel Production 배포 확인
  • 설계서: 없음(수리 건 — 오너 보고 즉응 루프)
  • 정본: exercise-search.md "문자열 매칭" 절 + 엔진 src/react/exerciseSynonymSearch.ts + UI 훅 src/react/ui/shared/useDebouncedSearchQuery.ts·useListWindow.ts
  • 도구: 단독 마운트 지연 하네스(worktree 임시 vite 엔트리, 950종목 픽스처 + 자모 시퀀스 주입 + Profiler 커밋 계측 + 강제 레이아웃 실측 — gitignored, 레포 밖)
  • 게이트: tests/react/exerciseSynonymSearch.test.mjs(자모·토큰·prefix 회귀 그물) · exerciseSearchListWindow.test.mjs(창 렌더 단독 마운트 단언 3표면)
  • 버그리포트: bug-021-20260825.md (부록 4-1~4-6 = 수리 연쇄 전체)
  • 계약: exercise-search.md 규칙 6(접기·자모·토큰·alias prefix) 개정, "문자열 매칭" 절 신설
  • 기록 이슈: #771 (원 트랙 #673에서 분리, 오너 지시)

Phase 현황

Phase내용상태
Phase 1매칭 단순화·사전 계산 (키 입력 경로에서 로케일 정규화 제거)✅ PR #717
Phase 2입력/리스트 렌더 분리 (useDeferredValue) + alias prefix✅ PR #723 · #746
Phase 3리스트 렌더 게이트 — 오너 기준(스페이스/엔터 즉시 + 100ms 무입력) + 결과 element 고정✅ PR #752
Phase 460행 창 렌더 (useListWindow — 첫 2~3글자·브라우즈 통째 커밋 제거)✅ PR #762
Phase 5한글 자모 매칭 (IME 조합 중간 상태 포용, 낱자 NFKC 3계 합류)✅ PR #764 · #766
Phase 6공백 토큰 AND (어순 자유·다단어 중간 상태 안정)✅ PR #768
Phase 7게이트 전 표면 확산 (나머지 검색창 6곳 + 리포트 필터 매처 승격)✅ PR #770

1. 배경

08-24 synonym 검색 계약 트랙(#673, 작업 기록)이 배포되면서 검색 모집단이 synonym 합집합 + alias(행당 최대 16개)로 커졌다. 배포 직후 오너 실사용에서 검색창이 입력 불가 수준으로 버벅였고, 이후 실기기 확인 루프에서 한글 IME 특유의 조합 중간 상태·낱자·어순 문제가 연쇄로 드러났다. 이 문서는 그 수리 연쇄(9 PR)를 한 트랙으로 묶는다.

2. 문제 제기

키 입력마다 전 행·전 용어를 다시 정규화했다

foldExerciseSearchText(NFKC + ko-KR lower + 유니코드 정규식)를 매 키 입력마다 ~1,100행 × 최대 18용어에 재적용 — 벤치 15ms+/타, 한글 IME는 자모마다 change를 발화해 글자당 여러 번.

질의가 반영되지 않는 키에서도 React가 큰 리스트를 재비교했다

디바운스·deferred를 넣어도 입력 커밋마다 브라우즈 전 행(~950행)의 element를 재생성·재diff — 하네스 실측 키당 10~15ms. Profiler는 React 렌더만 재서 이 비용과 브라우저 레이아웃(950행 12ms)을 처음엔 놓쳤다.

완성 음절 문자열 매칭이 한글 IME의 실제 입력 상태를 포용하지 못했다

"프레스"를 치는 중간의 "프렛"(다음 초성이 받침으로 선부착), 낱자 "오버헤ㄷ"(NFKC가 호환 자모를 조합형 U+1103으로 변환 — 실측 NFKC("오버헤ㄷ") = c624 bc84 d5e4 1103), 어순 반대 "저크 스플릿" — 모두 0건이 되어 타이핑 중 "결과 없음"이 번쩍였다.

검색창마다 동작이 달랐다

리포트 종목 필터는 synonym 매처도 없는 구형 plain includes, 데스크톱 드롭다운들은 게이트 없이 키마다 반영.

3. 해결 방안

원칙 (오너 결정, 08-25)

  • D1 — "사용자가 스페이스/엔터를 입력하거나, 혹은 100ms 동안 입력이 없거나 — 이 기준으로 렌더": 리스트 반영 시점의 정본이 됐다.
  • D2 — "이거때문에 검색 로직이 괜히 복잡해진거 아냐?": 키 입력 경로는 사전 계산 문자열 스캔 하나로 고정, "똑똑한 매칭" 금지. 역인덱스는 규모(~1,000행·0.3ms 스캔) 미달로 기각.
  • D3 — "다른 검색창에도 다 적용": 전 표면 동일 UX.
  • D4 — "이 검색창 개선 작업은 CI 없어도 될듯": 이 트랙 한정 CI 생략 admin 머지(로컬 tsc·전체 테스트·게이트 필수, 사후 CI 확인 — #764·#766·#768 전부 사후 pass).

접근

대안판단
접기·자모 분해를 행 생성 1회로 사전 계산, 키 입력은 includes채택 — 원인 제거, 규칙이 한 줄로 설명됨
역인덱스(인버티드 인덱스)기각 — 수십만 행부터 의미, 현 규모 스캔 0.3ms
결과 리스트 가상화 라이브러리기각 — 60행 창 + 센티널(레포 기존 관행)로 충분
조합 중간 상태에서 빈 화면 억제(UI 땜질)기각 — 자모·토큰 매칭이 원인을 제거

4. 적용한 내용

Phase 1~2 — 매칭 단순화·렌더 분리 (#717·#723·#746)

행 생성 시 nameTerms/aliasTerms 사전 접기(WeakMap 캐시), 필터 = 질의 1회 접기 + includes, 표기(이름) 매칭 우선 dedupe, alias는 prefix("ov"가 alias 중간에 걸려 스플릿 저크·바벨 로우가 뜨던 소음 7건 → 0건). 타이핑 표면 리스트를 useDeferredValue로 입력에서 분리.

Phase 3 — 렌더 게이트 (#752)

useDebouncedSearchQuery(ui/shared 신설): 입력창 값은 즉시, 리스트 질의는 ① 스페이스로 끝나면 즉시 ② 엔터 flush(IME 조합 중 제외) ③ 그 외 100ms 무입력 뒤. 하네스가 잔존 주범을 적발 — 질의 미반영 입력 커밋의 ~950행 재diff → 결과 리스트를 element 동일성(useMemo)으로 고정.

Phase 4 — 60행 창 렌더 (#762)

useListWindow(ui/shared 신설): 처음 60행만 마운트, 센티널이 보이면 60씩 확장(세션 검색 세트 피드의 무한 스크롤 관행). 질의가 바뀌면 창은 렌더 단계에서 즉시 60 복귀. 관찰은 callback ref — 창 확장 시 센티널 노드가 재마운트되어 effect 구독이 낡은 노드에 남는 결함(확장 1회 후 정지)을 하네스에서 적발.

Phase 5 — 자모 매칭 (#764·#766)

접기 마지막 단계에 한글 자모 분해: "프렛"=ㅍㅡㄹㅔㅅ ⊂ "프레스"=ㅍㅡㄹㅔㅅㅡ — IME 조합 중간 상태가 항상 목표 표기의 접두. 겹모음·겹받침은 두벌식 타건 순서(ㅘ=ㅗㅏ·ㄺ=ㄹㄱ). 낱자 입력은 NFKC가 조합형 자모로 변환하므로 조합형 초/중/종성 블록도 같은 호환 자모로 정규화(3계 합류).

Phase 6 — 공백 토큰 AND (#768)

질의를 공백으로만 나눠 각 토큰을 접고, 모든 토큰이 표기에 부분 매칭이면 통과 — 어순 자유("저크 스플릿" → 스플릿 저크), 둘째 단어의 조합 중간 상태("저크 스"~"저크 스프")도 전 구간 안정. 비문자 전체 분리는 "o.h.p"가 단문자 토큰으로 광역 매칭되는 회귀를 낳아 기각(테스트로 적발).

Phase 7 — 전 표면 확산 (#770)

게이트를 나머지 6곳(리포트 종목 필터·통합 검색·볼륨 선택·그룹 종목 추가·PR 상세 드롭다운·복합 빌더 슬롯)에 배선, 리포트 필터는 plain includes → synonym 매처 승격. PR 검색 계열은 검색 컨트롤러가 이미 디바운스 커밋·직전 결과 유지를 소유해 제외, 친구 검색은 서버 검색(별개 표면).

주요 결정과 그 근거

  • 변형 흡수는 데이터(alias), 포용은 접기(자모·토큰), 시점은 게이트 — 세 층을 분리해 각각 한 줄 규칙으로 유지.
  • 창 리셋은 effect가 아니라 렌더 단계 — effect 리셋이면 브라우즈에서 늘려둔 창이 첫 검색 커밋에 그대로 실린다.

작업 중 드러난 것

  • Profiler actual은 React 렌더만 잰다 — 커밋 후 스타일/레이아웃은 강제 레이아웃 읽기(offsetHeight)로 따로 실측해야 한다.
  • Browser 패널이 숨겨지면 rAF·IntersectionObserver가 얼고 setTimeout이 1s+로 스로틀 — 계측은 동기 dispatch·MessageChannel 대기로.
  • NFKC는 낱자 호환 자모(U+3131~)를 조합형(U+1100~)으로 바꾼다 — 자모 매칭은 음절·호환·조합형 3계를 한 코드포인트계로 합류시켜야 한다.

5. 적용 결과

항목전 → 후
매칭 비용(키당)15ms+ → 0.32ms (#717)
자모 연속 입력 시 입력 비용(키당)27ms → 10.4~15.6ms(#723) → 0.5~1.6ms(#752)
타이핑 중 5ms 초과 리스트 커밋11회 → 0회 (#752)
브라우즈 마운트 행 / 강제 레이아웃950행·12ms → 60행·≤2.3ms (#762)
"ov" 소음(엉뚱한 종목)7건 → 0건 (#746, Production 데이터 프로브)
IME 중간 상태("프렛"·"오버헤ㄷ"·"저크 스") "결과 없음" 번쩍임매 상태 → 0 (#764·#766·#768)
어순 반대("저크 스플릿")0건 → 스플릿 저크 (#768)
게이트 적용 표면4곳 → 검색창 전 표면(PR 계열·친구 검색은 자체 게이트) (#770)
오너 실기기 확인타이핑·"저크 스플릿" 구간 "아주 잘 동작"(08-25). 미검증 2건: #770 확산 6표면 실기기, 창 스크롤 확장(IO 발화 — 개발 환경 프레임 정지로 실브라우저 미검증)

6. 이번 개선으로 향상된 것

한글로 검색하는 실제 손놀림이 계약이 됐다

IME 조합 중간 상태·낱자·어순이 전부 매칭 모집단에 들어와, 타이핑 중 화면이 번쩍이지 않는다. 계약 문서 "문자열 매칭" 절이 정본.

키 입력 경로 성능 계약

접기·자모 분해 = 행 생성 1회 + 질의 1회, 키 입력 = 사전 계산 문자열 스캔, 리스트 반영 = 스페이스/엔터/100ms, 마운트 = 60행 창. 새 검색 표면은 useDebouncedSearchQuery + (큰 리스트면) useListWindow + 엔진/매처만 쓰면 같은 UX를 얻는다.

구조적으로 남는 것

  • 계약: docs/contracts/exercise-search.md 규칙 6 + "문자열 매칭" 절.
  • 훅: ui/shared/useDebouncedSearchQuery·useListWindow(ui/shared 첫 React 모듈들).
  • 게이트: 자모·토큰·prefix·창 렌더 회귀 테스트(엔진 + 단독 마운트 3표면).
  • 기법: 단독 마운트 지연 하네스(Profiler 커밋 + 강제 레이아웃 실측).

남은 것

  • 오너 실기기 확인 2건(#771 체크박스): #770 확산 6표면, 창 스크롤 확장.
  • 확인 후 #771·#673 종결.