정의
스크롤 복원은 뒤로가기·새로고침 후 리스트가 보던 위치를 되찾는 UX다. **가상 리스트(window virtualizer)**는 DOM에 보이는 행만 두기 때문에, 복원 시 추정 높이와 실측 높이 차이로 문서 높이가 변하고 scrollY가 클램프될 수 있다.
단일 scroll authority란 bootstrap script·React effect·virtualizer seed 중 한 경로만 최종 scroll 위치를 정한다는 규칙이다. 여러 곳이 scrollTo·6프레임 reassert를 동시에 하면 번쩍임·드르륵 떨림이 난다.
왜 필요한가
무한 스크롤 리스트는 sessionStorage에 anchor id·scrollY·row 높이를 저장해 cold reload를 견딘다. 그런데 (1) throttle save 리스너가 매 렌더마다 재구독되면 저장이 0회이고, (2) reveal 후 min-height placeholder를 너무 일찍 제거하면 totalSize가 줄어 scrollY가 잘리며, (3) bootstrap rAF×3 + inner seed + 6프레임 scrollBy 루프가 서로 덮어쓴다.
E2E가 Chromium 네이티브 scroll restoration만 검증하면 스냅샷 미저장 false-green이 된다.
동작 원리
- 저장: passive scroll listener를 한 번만 부착(
[]deps). capture 함수는 latest-ref로 읽는다. 250ms throttle로 sessionStoragesetItem. - bootstrap: inline script tail은
window.scrollTo(0, y)1회(rAF 체인·load리스너 제거). - React 복원:
visibility:hidden유지 → anchordata-row-id의 viewport offset과 저장값 비교 →scrollBy(delta)1회. anchor 없으면 픽셀scrollYfallback(인덱스 추정 금지). - 높이 seed: 측정된 row 높이를 sessionStorage
:h키로 지속 → reload 첫 프레임부터getTotalSize()안정. - min-height floor: reveal 후에도 placeholder 유지, 실제 콘텐츠가 현재 scrollY를 덮을 때만 제거.
- failsafe: unreachable anchor + cold-fetch 소진 후 reveal — 영구
hidden백지 방지.
실무 적용
@tanstack/react-virtualmeasureElement는 commit 후 rAF 1회면 텍스트 row 보정에 충분한 경우가 많다.- e2e: reload ±16px·무지터, deep depth 드리프트 ≤1px, sessionStorage 저장 gate, anchorless·unreachable 경로.
history.scrollRestoration = "manual"은 Next 네이티브 back 복원과 충돌 — 프로젝트 정책 확인.- ListRestoreBootstrap을 외부 script 파일로 분리해 CSP·캐시 정책 명확화.
트레이드오프
| 선택 | 장점 | 단점 |
|---|---|---|
| 단일 corrective scroll | 예측 가능, 떨림 제거 | 가변 이미지 행은 1회로 부족할 수 있음 |
| N-frame reassert | 드리프트 chase | 눈에 보이는 지터·authority 경쟁 |
| 인덱스 fallback | 구현 쉬움 | 커서 페이지 경계 스냅 |
| 픽셀 scrollY fallback | anchor 없을 때 안정 | row 삭제 시 오차 |
| 측정 높이 지속 | deep reload 정확 | sessionStorage 용량·마이그레이션 |
사용하면 안 되는 경우
- 항목 수십 개·가상화 없는 짧은 리스트(브라우저 기본 restoration으로 충분).
- 행 높이가 비동기 이미지로 크게 변하는데 1회 보정만 고집하는 경우.
- 스냅샷 저장 없이 e2e만 "복원됨"으로 통과시키는 경우.
흔한 실수
- throttle save effect deps에 매 렌더 새
captureSnapshot넣어 pending timer clear → 저장 안 됨. - estimateSize만 쓰고 deep reload 후 totalSize 감소 → scrollY 클램프 수백 px.
- bootstrap + React + inner seed가 동시 scrollTo.
- e2e가 네이티브 restoration만 검증해 스냅샷 미저장 버그를 놓침.
reassertScrollForFrames·estimateScrollYForRowIndex같은 dead helper 잔존.
관련 개념
- list-virtualization-windowing — virtualizer·measureElement 기본
- ssr-hydration-mismatch — bootstrap script vs React 경계