Skip to content

현재 사용법 (2026-09-10, 6fbaee7f): FE 성능·측정·기존 동작 영상 · 기능 마스터. 아래는 작성 당시의 변경 설명이며 현재 전체 동작·재측정 결과를 뜻하지 않습니다.

저사양 CPU 에서의 급여 시트 성능 최적화 (4x 스로틀 실측 기반) ​

CPU 가 느린 PC 에서 급여 시트가 전반적으로 버벅이던 것을 세 축에서 고칩니다: 첫 화면이 뜨기까지 15.7초 걸리던 것이 2.6초가 되고, 셀을 마우스로 클릭할 때 키보드 이동보다 5배 느리던 반응이 같은 수준이 되며, 스크롤·선택 이동 중의 멈칫거림이 줄어듭니다. 화면과 기능은 바뀌지 않습니다.

모든 수정은 Chrome DevTools 의 CPU 4x 스로틀 환경에서 CDP 프로파일러로 병목을 실측해 확인한 뒤 적용했고, 같은 환경에서 전후를 재측정했습니다.

배경 / 지금 동작 ​

급여 워크북(hr-system 시트 탭)은 세 층으로 그려집니다. 엔진(src/sheet/engine.ts)이 가상화된 행 윈도우(약 35행 × 16열 = 560개 <td>)를 replaceChildren 으로 직접 그리고, Alpine 이 툴바·패널·모달 등 나머지 UI 를 반응형으로 묶으며, 워크북 컴포넌트의 engineTick(정수 카운터)이 "다시 그려야 한다"는 신호로 쓰입니다 — engineTick 을 읽는 모든 getter 와 바인딩은 tick 이 오를 때마다 재평가됩니다.

측정에 쓴 시트는 계약급여내역(1,010행 × 16열)입니다.

1. 초기 로드 15.7초 → 2.6초 — 숨은 달력 4만 개 바인딩 ​

변경 전

변경 후

프로파일 최상위가 전부 Alpine 표현식 초기 컴파일이었고, DOM 센서스로 추적하니 페이지 전체 Alpine 바인딩 43,911개 중 약 4만 개가 닫혀 있는 DS 데이트피커 팝오버 내부였습니다. 두 결함의 곱입니다. ① 피커 호스트들이 max: '9999-12-31' 을 "사실상 무제한" 의미로 넘기는데, 연도 드롭다운은 [min, max] 구간의 연도를 전량 노출하는 규칙이라 피커당 연도 버튼 약 7,974개가 생깁니다. ② 팝오버 루트가 x-show 라서 닫혀 있어도 부팅 시 전부 스탬프·컴파일됩니다.

수정(packages/components — 전 앱 공통 혜택): 팝오버를 template x-if 로 바꿔 열 때만 달력을 만들고, 연도 리스트를 보기 연도 기준 과거 150·미래 50으로 상한합니다. 생년월일(1900~현재 ≈ 127개)은 전량 유지되고, 날짜 유효성 검증(isOutOfRange)은 원래 min/max 를 그대로 쓰므로 상한 밖 날짜도 유효합니다. 상한 단위 테스트 3건을 추가했습니다.

2. 마우스 클릭이 키보드 이동보다 5배 느림 — 클릭 하나에 리페인트 3회 ​

변경 전

변경 후

플레인 클릭 하나가 mousedown·mouseup·click 세 단계에서 선택 리페인트를 3회, engineTick 을 2회 올리고 있었습니다. mouseup 의 flush 는 dragSelecting 만 보고 발동하는데, 이 플래그는 드래그를 시작만 해도(=모든 클릭에서) 참이 됩니다. tick 이 오를 때마다 하단 합계 재집계·대기/승인 행 전체 스캔·Alpine 의존 그래프 재추적이 돌아 클릭당 longtask 가 약 280ms(키보드는 0개)였습니다.

수정: mousedown 의 tick 제거(반응형 전파는 엔진의 renderTick 과 Alpine 프록시 추적이 이미 담당 — 키보드와 같은 메커니즘), 셀 경계를 실제로 넘었는지를 기록하는 dragMoved 플래그를 신설해 mouseup flush 를 진짜 드래그로 한정, click 의 재선택은 이미 그 셀만 선택된 상태면 건너뜁니다. 진짜 드래그는 조용한(raw) 확장을 쓰므로 드래그 끝의 tick 은 유일한 반응형 동기화라 유지합니다. 결과: 클릭당 longtask 280ms 상당 → 77ms 1개.

3. 선택 이동·스크롤 중 상시 비용 5건 ​

프로파일에서 상호작용마다 반복되던 비용을 제거했습니다.

  • 선택 집계 배지 위치 계산이 배지가 보이지 않는 단일 선택 상태에서도 스크롤 틱당 2회 rect 실측과 스타일 쓰기를 하고 있었습니다 — 2셀 미만이면 측정 전에 반환합니다.
  • 필 핸들 재배치가 렌더 직후 동기 getBoundingClientRect 로 강제 레이아웃을 만들었습니다(이동 1회 최대 143ms) — rAF 로 프레임당 1회로 병합하고, 드래그 중에는 기존 paint rAF 안에서 즉시 실행을 유지합니다.
  • 하단 합계(summaryFor) 가 engineTick 을 읽는 탓에 모든 선택 이동마다 16열 × 1,010행을 재합산했습니다 — 같은 tick 안에서는 값이 변할 수 없으므로 (tick, colKey) 결과를 캐시합니다.
  • 금액 포맷(formatAmount) 의 toLocaleString(ICU) 호출이 가상화 재렌더마다 수백 셀 반복됐습니다(렌더 시간의 약 1/3) — 결과를 메모합니다.
  • Alpine MutationObserver 가 엔진의 tbody 교체마다 추가/제거된 560개 노드를 디렉티브 탐색·cleanup 스캔했습니다 — 엔진이 그리는 노드에는 Alpine 디렉티브가 없어 순수 낭비입니다. 렌더를 Alpine.mutateDom 으로 감싸 감시를 멈추되, 엔진은 래퍼를 주입(setDomMutationWrapper)받기만 해 프레임워크 독립을 유지합니다.

남은 한계 (후속 과제) ​

이제 상호작용당 비용의 대부분은 브라우저 몫입니다 — 가상화 윈도우 재렌더(560개 td 교체)가 유발하는 네이티브 스타일 recalc·레이아웃·페인트. 이를 더 줄이려면 행 재사용(DOM 패칭) 같은 엔진 구조 변경이 필요해 이번 범위에서 제외했습니다. 선택 스타일시트를 전체 스왑 대신 증분 갱신하는 것도 같은 계열의 후속 후보입니다.

실측 요약 (4x CPU 스로틀 · 계약급여내역 1,010행) ​

시나리오수정 전수정 후
초기 로드 DCL15.7s2.6s
첫 그리드 셀 표시19.3s4.6s
Alpine 바인딩 요소43,9113,246
마우스 클릭 longtask~280ms (2개)77ms (1개)
키보드 이동 longtask00 (불변)
스크롤 틱 longtask85~130ms70~106ms

회귀 확인: 데이트피커 열림/연도 드롭다운/닫힘 언마운트, 클릭 선택·셀 상세 패널 동기화, 드래그 다중 선택(카운터 포함), 다중 선택 집계 배지 표시·위치, 필 핸들 드래그. hr-system vitest 406건 · components 19건 그린.

후속 설계 메모 — 선택 스타일시트 증분 갱신 (미착수, 2026-08-11 기록) ​

가상화 재렌더 0~2단계(#547) 이후 남은 상호작용 비용의 정체입니다. 화살표 창 경계 이동 ~68ms · 클릭 ~77ms(4x 스로틀)의 대부분이 여기서 나옵니다.

배경. 선택 하이라이트는 셀 클래스가 아니라 임페러티브 스타일시트(src/sheet/styles.ts)로 그립니다 — <style> 태그 하나에 td[data-row-key=…][data-col-key=…] { … } 규칙을 선택 셀 수만큼 써넣고, 선택이 바뀔 때마다 textContent 로 전체 교체합니다. 대량 선택(수천 셀)에서 DOM 변이 없이 1회 교체로 끝나고, 가상화로 입장한 셀이 자동으로 하이라이트를 받는 것이 이 설계의 장점입니다.

문제. textContent 교체는 브라우저에 "스타일시트 변경"으로 잡혀 문서 전체 스타일 재계산을 유발합니다. 화살표 1회 = 규칙 한 줄 삭제 + 한 줄 추가인데 ~7천 요소를 재평가합니다.

방안 A — CSSOM 증분 (권장). styleEl.sheet.deleteRule/insertRule 로 바뀐 규칙만 넣고 뺍니다. 기존 장점(입장 셀 자동 하이라이트, 대량 선택 1회 비용)을 유지하고 재계산 범위만 줄입니다. 규칙 인덱스 추적 코드가 필요합니다.

방안 B — 하이브리드 클래스 토글. 선택이 소규모(수십 셀 이하)일 때만 이전/새 셀에 클래스 remove/add(DOM 변이 2번). 구현은 단순하지만 가상화 자동 하이라이트가 깨져 패치 입장 행에 선택 클래스를 보정하는 데코 단계가 추가로 필요합니다.

착수 시 순서. 방안 A 프로토타입으로 먼저 실측합니다 — attribute 기반 셀렉터라 브라우저의 규칙 무효화 최적화가 덜 먹을 수 있어, 효과가 작으면 방안 B 로 전환합니다. 측정 시나리오: 4x 스로틀에서 화살표 연속 이동·클릭 연속의 longtask (기준선: 화살표 ~68ms · 클릭 ~77ms).