Trouble Shooting
2025년 12월 03일

GNB 스크롤 성능 최적화

GNB 스크롤 성능 최적화 및 애니메이션 구현 트러블슈팅

1. 개요

  • Global Navigation Bar (GNB)가 스크롤 시 상단에 고정(Sticky)되면서 형태가 변하는 애니메이션을 구현하는 과정에서 발생한 성능 이슈와 이를 해결하기 위한 최적화 과정을 기록합니다.

2. 문제 인식 (Initial Challenge)

초기 접근 방식

처음에는 가장 직관적인 방법인 window.scroll 이벤트를 사용하여 스크롤 위치를 감지했습니다.

// ❌ 초기 접근 (의사 코드)useEffect(() => { const handleScroll = () => { // 1. 스크롤 이벤트는 픽셀 단위로 발생 (매우 빈번함)// 2. getBoundingClientRect() 호출은 강제 리플로우(Reflow)를 유발할 수 있음const introBottom = document.getElementById("intro")?.getBoundingClientRect().bottom; if (introBottom <= 0) setIsSticky(true); else setIsSticky(false); }; window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, []);

발견된 문제점

  1. 메인 스레드 부하: 스크롤 이벤트는 초당 수십~수백 번 발생합니다. 매번 DOM의 위치를 계산하는 로직이 메인 스레드를 점유하여, 복잡한 페이지에서는 스크롤 버벅임 현상을 유발할 수 있습니다.
  2. Layout Thrashing: 반복적인 DOM 읽기 작업은 브라우저가 스타일과 레이아웃을 계속 다시 계산하게 만듭니다.

3. 해결 과정 및 최적화 (Optimization)

1단계: Throttle/Debounce 고려

일반적으로 lodash/throttle을 사용하여 이벤트 발생 빈도를 제어(예: 100ms마다 실행)하는 방법이 있습니다.

  • 장점: 이벤트 실행 횟수가 현저히 줄어듦.
  • 한계: 여전히 스크롤을 "감시(Polling)"하는 방식이며, 정확한 타이밍에 UI를 변경하기 어려워 시각적인 딜레이가 발생할 수 있습니다.

2단계: IntersectionObserver 도입 (최종 해결책)

브라우저의 Native API인 IntersectionObserver를 사용하여 "위치 계산"이 아닌 "상태 변화"를 감지하도록 변경했습니다.

왜 더 좋은가?

  • 비동기 실행: 메인 스레드를 차단하지 않고 브라우저 내부(C++ 레벨)에서 교차 영역을 감시합니다.
  • Zero Polling: 스크롤할 때마다 계산하는 것이 아니라, 요소가 화면에서 사라지는 딱 그 순간 에만 콜백을 실행합니다.
// ✅ 최적화된 코드const stickyObserver = new IntersectionObserver( (entries) => { entries.forEach((entry) => { // !isIntersecting: 화면에서 사라짐// boundingClientRect.top < 0: 위쪽으로 사라짐 (스크롤 다운)if (!entry.isIntersecting && entry.boundingClientRect.top < 0) { setIsSticky(true); } else { setIsSticky(false); } }); }, { rootMargin: "-64px 0px 0px 0px", threshold: 0 } );

4. 성능 비교 (Performance Metrics)

사용자가 높이 2000px 페이지를 스크롤하여 GNB를 고정시키는 시나리오를 가정했을 때의 비교입니다.

결론: IntersectionObserver를 사용함으로써 이벤트 처리 비용을 약 99% 이상 절감했습니다.