본문 바로가기
궁금한 것

해당 섹션에 도착 시 nav에 이벤트 주기 scroll spy

by 쪼꼬에몽 2025. 12. 25.

Intersection Observer API - Web API | MDN

 

IntersectionObserver - Web API | MDN

교차 영역을 계산할 때 루트의 바운딩 박스에 적용할 오프셋입니다. 즉, 필요에 따라 계산 용도로 루트 영역을 늘리거나 줄일 수 있습니다. 생성자 설정에 지정한 값은 생성자 내에서 내부 조건

developer.mozilla.org

스크롤을 내릴 때마다 섹션에 도착하게 된다.

그럴 때마다 해당하는 섹션의 nav에 색을 주려고 한다.

이때 이용하는 것이 IntersectionObserver이다.

  • 스크롤 위치를 감지해서
  • 현재 화면에 보이는 섹션의 id를 추적
  • 네비게이션 하이라이트 같은 곳에 사용한다. 

탄생 배경

과거: scroll 이벤트 기반

window.addEventListener("scroll", () => {
  const top = window.scrollY;
  // 각 섹션 offsetTop 계산
});

 

이 방식은 

스크롤 이벤트가 너무 자주 발생, 직적 계산, 성능 문제, 모바일 문제 등의 문제점이 발생했다.

 

브라우저가 효율적으로 요소가 화면에 보이는지 알려주기 위해 IntersectionObserver API가 탄생.

스크롤을 직접 감지하는 것이 아닌

요소와 viewport의 교차 상태만 관찰하여

브라우저 레벨에서 최적화하고 이벤트 루프 부담 감소시킨다.

 

IntersectionObserver가 나오면서 생긴 ScrollSpy.

부라우저 내부에서 batching 처리,

스크롤 이벤트 없음,

디바운스 필요 없음,

모바일에서도 안정적

이어서 성능이 매우 좋다.

React 철학인 상태 기반 UI, 관심사 분리, 반응형 레이아웃에 잘 어울린다.

 

import { useEffect, useState } from "react";

export default function useScrollSpy(
  selector = "main[id]",
  threshold = 0.6
) {
  const [activeSection, setActiveSection] = useState<string>('');

  useEffect(() => {
    const sections = document.querySelectorAll(selector);

    const observer = new IntersectionObserver(
      (entries) => {
        let found = false;

        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            found = true;
            setActiveSection(entry.target.id);
          }
        });

        if (!found) setActiveSection("");
      },
      { threshold }
    );

    sections.forEach((section) => observer.observe(section));

    return () => observer.disconnect();
  }, [selector, threshold]);

  return activeSection;
}

 

export default function useScrollSpy(
  selector = "main[id]",
  threshold = 0.6
)

 

selector는 감시할 DOM 요소 선택자이다.

main의 id를 감시한다. 

thresold는 요소가 얼마나 보여야 보인다고 판단할지 정해준다.

0.6이면 60% 이상 화면에 들어왔을 때 보인다고 설정한다.

 

const [activeSection, setActiveSection] = useState<string>('');

 

상태는 현재 활성화된 섹션 id이다.

화면에 보여지는 id를 문자열로 상태 관리한다.

 

useEffect 안이 핵심이다.

const sections = document.querySelectorAll(selector);

 

selector에 해당하는 모든 요소를 수집한다.

NodeListOf<Element>를 반환한다.

NodeList[
  <main id="AboutMe" />,
  <main id="Skills />,
]

 

const observer = new IntersectionObserver(
  (entries) => {

 

IntersectionObserver란

특정 요소가 viewport 안에 들어왔는지를

스크롤 이벤트 없이 감지하는 브라우저 API이다.

 

let found = false;

 

이번 콜백에서 하나라도 보이는 섹션이 있는지 확인한다.

 

entries.forEach((entry) => {
  if (entry.isIntersecting) {
    found = true;
    setActiveSection(entry.target.id);
  }
});

 

entry.isIntersecting가

true이면 threshold 이상 화면에 들어왔고,

false이면 화면 밖이라는 의미이다.

 

entry.target은

실제 DOM 요쇼로,

.id를 붙여 main의 id를 반환해준다.

 

보이는 섹션을 발견하면 activeSection을 해당 id로 변경한다.

if (!found) setActiveSection("");

 

모든 섹션이 viewport 밖일 때

이전 섹션 id가 계속 남아있는 현상을 방지한다.

지금은 어떤 섹션도 활성 상태가 아니라는 것을 만들어 준다.

sections.forEach((section) => observer.observe(section));

 

각각의 섹션을 감시 대상으로 추가하여

브라우저가 자동 추적할 수 있도록 한다.

return () => observer.disconnect();

 

컴포넌트 언마운트 시

Observer를 완전히 제거하여 메모리 누수를 방지한다.

}, [selector, threshold]);

 

selector나 threshold가 바뀌면

observer를 새로 생성한다.

 

<li className={activeSection === "about" ? "active" : ""}>

 

이런 식으로 activeSection이 현재 id랑 일치하는 비교하고

일치 유무에 따라 스타일을 부여한다.

'궁금한 것' 카테고리의 다른 글

Framer Motion  (2) 2025.12.28
import { } 유무 차이  (0) 2025.12.26
nav 클릭 시 원하는 섹션으로 스크롤 이동 Anchor Scroll  (0) 2025.12.25
::after  (0) 2025.12.23
group-hover  (0) 2025.12.23