긴 법률 문서 목차 스크롤스파이, 깜빡임 없이 하이라이트하기
조항이 수십 개인 긴 문서에서 목차 활성 항목이 튀는 깜빡임을 IntersectionObserver의 rootMargin 밴드로 잡는 프론트엔드 구현 방법을 정리했어요.
조항이 40개 넘는 긴 문서를 화면에 띄우고, 옆에 목차를 붙여 지금 읽는 위치를 표시하려 한 적 있나요? 스크롤을 조금만 굴려도 목차의 활성 항목이 위아래로 튀면서 깜빡이는 경험, 프론트엔드 화면을 만들다 보면 한 번쯤 만나게 돼요.
왜 목차가 깜빡일까요
가장 흔한 방식은 스크롤 이벤트에서 각 섹션의 위치를 재는 거예요. 문제는 스크롤 이벤트가 프레임마다 여러 번 튀고, 그때마다 활성 섹션을 다시 계산하니 상태가 쉴 새 없이 바뀐다는 점이에요. 직접 붙여 보니 40개짜리 목차에선 손가락을 살짝만 움직여도 하이라이트가 두세 칸씩 널뛰더라고요.
계산 위치도 애매해요. 섹션 경계가 화면 어디를 지날 때 "활성"으로 볼지 기준이 없으면, 두 섹션이 동시에 화면에 걸친 순간 둘 사이를 오가며 깜빡여요.
IntersectionObserver로 바꾸기
여기서 스크롤 이벤트 대신 IntersectionObserver를 쓰면 절반은 해결돼요. 이 API는 요소가 화면에 들어오고 나가는 그 순간에만 콜백을 부르거든요. 매 프레임 위치를 재던 걸, 섹션이 실제로 교차할 때 몇 번만 부르는 구조로 바꾸는 셈이에요.
깜빡임을 없애는 핵심은 rootMargin
관찰만 붙인다고 깜빡임이 사라지진 않아요. 여러 섹션이 동시에 화면에 보이면 어느 걸 활성으로 볼지 여전히 흔들리거든요. 여기서 rootMargin으로 감지 영역을 화면 상단의 얇은 띠로 좁히는 게 핵심이에요.
const observer = new IntersectionObserver(onEntry, {
rootMargin: "0px 0px -80% 0px", // 화면 상단 20% 구간만 감지
threshold: 0,
});위처럼 아래쪽 여백을 -80%로 주면, 섹션 제목이 화면 위쪽 20% 구간에 들어올 때만 활성으로 잡혀요. 값을 바꿔 가며 테스트해 보면 이 띠가 좁을수록 활성 항목이 한 번에 하나로 딱 떨어져요.
여기에 한 가지를 더했어요. 새로 감지한 id가 지금 활성 id와 같으면 상태 갱신을 건너뛰는 거예요. 리액트라면 불필요한 리렌더가 사라지니, 매 프레임 수십 번 바뀌던 게 섹션이 실제로 넘어갈 때 몇 번으로 줄어요.
- rootMargin 상단 밴드로 좁히기
- 같은 id면 상태 갱신 스킵
- threshold는 0
마지막 섹션 처리도 잊지 마세요
짧은 마지막 조항은 화면 상단 띠까지 못 올라와서 영영 활성이 안 되는 경우가 있어요. 돌려 보니 문서 끝에 여백 블록을 하나 두거나, 스크롤이 바닥에 닿으면 마지막 항목을 강제로 켜는 처리로 자연스러워졌어요.
플랜엘(plan-l)에서 법령과 판례를 검색하다 보면 조문이 수십 개인 문서를 자주 다루게 되는데, 이런 목차 하이라이트 하나만 매끄러워도 읽는 흐름이 확 안정돼요. 만들면서 느낀 건, 화려한 애니메이션보다 "지금 여기를 읽고 있다"는 표시가 흔들리지 않는 게 더 중요하다는 점이었어요. 긴 문서를 다루는 화면이라면 한번 손봐 두면 두고두고 편해요.
관련 글
댓글
아직 댓글이 없어요. 첫 댓글을 남겨주세요.