Skip to content

메인 검색·종목 상세 복귀 렉 수리 — 950행 통마운트에서 검색 표면 엔진 통일·선택 상태 분리까지 (2026-09-01)

  • 기간: 2026-09-01 (1세션, 오너 보고: "종목 검색해서 종목 상세페이지로 들어갔다가 다시 뒤로 돌아가면 엄청 렉이 걸리는데 왜그럴까")
  • 랜딩: PR #1077(Phase 1~3, c627aba2) — 마이그레이션 없음, Vercel 자동 배포
  • 설계서: 없음(수리 건) — Phase 계획·예상 효과는 이슈 #1076 본문
  • 정본: 종목 검색 UI 규칙 = exerciseSynonymSearch 엔진 + useListWindow 재사용(별도 구현 금지) · 선택 종목 상세 경량 경로 = buildPrSelectedExercise(barbelicMappers.ts)
  • 도구: 계산량 벤치마크 스크립트(세션 스크래치패드, gitignored — 수치는 본문 5절)
  • 게이트: prSelectedExerciseSplit.test.mjs(동등성 5종) · exerciseSearchListWindow.test.mjs(홈 드로어 배선 회귀 그물)
  • 버그리포트: bug-054-20260901.md

Phase 현황

Phase내용상태
Phase 1홈 검색 드로어 — 엔진 교체 + 60행 창 렌더✅ PR #1077 (ba481489)
Phase 2선택 종목 상세 분리 — 뒤로가기 전량 재계산 차단✅ PR #1077 (f8d6a4c1)
Phase 3실측 검증(계산량 벤치마크·구조 테스트)✅ 본 문서 5절

1. 배경

홈의 종목 검색 드로어는 2026-08-11 기록 탭 코드를 이식하며 만들어졌고, 그 뒤 BUG-021(2026-08-25)이 검색 표면들을 synonym 엔진+창 렌더로 정비할 때 이 드로어만 빠졌다. 한편 앱은 탭을 keep-alive(숨김 유지)로 운영해 방문 화면이 모두 마운트된 채 남는다. 오너가 홈 검색 → 상세 → 뒤로가기 경로의 심한 렉을 보고했다.

2. 문제 제기

홈 검색 창이 전체 종목 ~950행을 통째로 그렸다

검색어가 없으면 950행 전부를 DOM에 마운트하고, 필터는 useMemo 없는 includes 스캔 — 검색 표면 5곳 중 유일하게 엔진도 창 렌더도 안 쓰는 곳이었다(BUG-021이 실측했던 950행 12ms 통커밋 패턴 그대로).

뒤로가기 한 번이 전체 카탈로그 재계산을 유발했다

"지금 보고 있는 종목"(prExerciseId)이 PR 화면 전체 데이터 memo의 의존성이라, 선택 해제만으로 950종목 병합·정렬·보드/카드 생성이 재실행됐다(실측 12.32ms/회). 이 값은 계산 안에서 "선택 종목 상세" 한 조각에만 쓰인다.

그 여파로 검색 색인까지 매번 재구축됐다

prProps 정체성이 바뀌면 검색 탭 카탈로그 파생이 새 배열을 만들어 950종목 자모 분해 색인(WeakMap 캐시)이 무효화됐다(BUG-021 실측 13.7ms/회). keep-alive 탭 구조라 방문 화면이 많을수록 연쇄가 커져 "오래 쓸수록 심해지는" 체감을 만들었다.

3. 해결 방안

원칙 (오너 go, 2026-09-01)

원인 분석과 Phase 계획을 보고한 뒤 오너 "아하 오케이 진행해줘"로 착수. 별도 결정 항목 없음.

접근

판단
홈 드로어에 창 렌더만 추가기각 — 뒤로가기 재계산이 그대로 남는다
엔진+창 렌더 통일 + 선택 상세 계산 분리채택 — 원인별 최소 수리, 동등성 테스트로 회귀 차단
화면 컴포넌트 전면 React.memo보류(범위 밖) — 재계산 제거로 비용이 크게 줄어, 잔여 렉 보고 시 별도 상신

4. 적용한 내용

Phase 1 — 홈 검색 드로어 수리 (#1077)

HomeScreen.tsx 드로어를 검색 탭(SetSearch)과 동일 구조로 교체: buildExerciseSynonymRows+filterExerciseSynonymRows(띄어쓰기·자모) + 입력 디바운스 + useListWindow 60행 창 렌더. 색인은 드로어가 열려 있을 때만 생성(부팅 중 카탈로그 교체마다 접기를 물지 않게). 마커(pr.search·prSearchInput·pr.open) 변경 0.

Phase 2 — 선택 종목 상세 분리 (#1077)

buildPrSelectedExercise 신설(barbelicMappers.ts) — 선택 id에 해당하는 행만 걸러 동일 병합(병합은 id별 독립이라 결과 동등, 테스트로 고정). prController.ts의 본체 memo에서 prExerciseId 의존 제거, 선택이 없으면 본체 객체를 그대로 반환해 prProps 정체성 유지. mobileApp.tsx의 검색 카탈로그 memo는 종목 목록 배열에만 의존.

Phase 3 — 실측 검증

950종목 합성 데이터 벤치마크 + 구조 테스트(5절).

주요 결정과 그 근거

  • 홈 드로어 색인은 드로어 열림에 게이트 — 닫힌 드로어 몫의 자모 접기(~14ms)를 부팅 경로에서 제거.
  • dual-key 게이트(래칫 260) 준수를 위해 원 id 읽기를 prSourceRawExerciseId 헬퍼로 공유(사이트 수 불변).

작업 중 드러난 것

  • 홈 드로어는 내부 상태(클릭)로만 열려 SSR 마운트 검증 불가 — 배선을 소스 구조 단언으로 고정(리포지토리 기존 관용구).
  • 새 함수에 snake∥camel 이중 키를 쓰면 dual-key 래칫이 즉시 잡는다 — 경계 헬퍼 공유가 정답.
  • 한글 문서를 PowerShell로 치환 수정하면 인코딩이 깨진다(기존 함정 재확인) — 문서 수정은 Write/Edit 도구로만.

5. 적용 결과

항목결과
검색 창 최초 마운트 행 수950 → 60 (+ 스크롤 확장 센티널)
홈 검색 방식includes 스캔 → 제품 엔진(띄어쓰기·자모·별칭) — 검색 표면 5곳 엔진 통일 완성
상세 진입/이탈 1회당 카탈로그 재계산12.32ms → 진입 0.14ms · 이탈(뒤로가기) ~0ms (950종목 벤치마크, node 실측)
뒤로가기당 검색 색인 재구축13.7ms(BUG-021 실측) × 매회 → 0회 (prProps 정체성 유지)
게이트로컬 게이트 전체(npm run check, 2,290 tests) Phase마다 통과 · CI 1회 통과
미검증실기기 프레임 단위 체감(로그인 세션 필요) — 계산량 실측·구조 테스트로 대체(§14 자동 증거)

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

홈 검색이 즉각 반응한다

유저가 홈 돋보기를 누르면 60행만 그려져 창이 바로 뜨고, "데드", "ㄷㄷ" 같은 부분 입력·별칭으로도 찾아진다.

상세에서 뒤로 나와도 멈추지 않는다

뒤로가기는 이제 선택 값 하나를 지우는 일만 한다 — 전체 카탈로그 재계산·색인 재구축이 0회가 되어, 여러 탭을 돌아다닌 뒤에도 비용이 일정하다.

구조적으로 남는 것

  • 종목 검색 UI = 엔진+창 렌더 재사용 원칙(세 번째 재발 끝에 회귀 그물로 고정).
  • "선택 상태는 전량 파생 memo의 의존성에 넣지 않는다" — 분리 패턴과 동등성 게이트.

남은 것

  • keep-alive 탭의 전 화면 동시 재렌더 구조(원인 ③의 토대)는 범위 밖 — 잔여 렉 보고 시 별도 트랙 상신.