블로그의 기능 중 목차 기능을 꼭 만들어 보고 싶었다. 🚀
목차 기능 설명 ✨
- 1.페이지 내에 있는
h2와h3를 찾고, 스크롤 위치에 따라 현재 보이는 섹션의 제목을 강조하는 기능을 구현하였다. - 2.이때 Intersection Observer API를 사용하였다.
Intersection Observer?
사용자 화면에 지금 보이는 요소인지 아닌지를 구별하는 기능을 제공한다.
이때, Intersection Observer는 scroll 이벤트가 아니다.
scroll 이벤트는 단시간에 수백번, 수천번 호출될 수 있고 동기적으로 실행되기 때문에 메인 스레드(Main Thread) 영향을 준다. 따라서 디바운싱(Debouncing)과 쓰로틀링(Throttling)을 통해 이러한 문제를 개선시켜줘야한다.
하지만 Intersection Observer는 비동기적으로 실행되기 때문에 메인 스레드에 영향을 주지 않으면서 변경 사항을 관찰할 수 있다.
1. 생성자 초기화
new IntersectionObserver()를 통해 관찰자(Observer)를 초기화하고 관찰할 대상Element을 지정한다.
생성자는 2개의 인수(`callback`, `options`)를 가진다.
const io = new IntersectionObserver(callback, options); // 관찰자 초기화
io.observe(element); // 관찰하고자 하는 HTML 요소2. callback
관찰할 대상(Target)이 등록되거나 가시성(Visibility, 보이는지 보이지 않는지)에 변화가 생기면 관찰자는 콜백(Callback)을 실행한다.
콜백은 2개의 인수(`entries`, `observer`)를 가집니다.
const io = new IntersectionObserver((entries, observer) => {}, options);
io.observe(element);entriesIntersectionObserverEntry객체의 리스트이다. 배열 형식으로 반환하기 때문에 forEach를 사용해서 처리를 하거나, 단일 타겟의 경우 배열인 점을 고려해서 코드를 작성해야 한다.
3. options
rootMargin
바깥 여백(Margin)을 이용해 Root 범위를 확장하거나 축소할 수 있다. (px과 % 표현 가능)
ex) TOP, RIGHT, BOTTOM, LEFT ( 10px 0px 30px 0px )
threshold
옵저버가 실행되기 위해 타겟의 가시성이 얼마나 필요한지 백분율로 표시한다.
구현하기
getIntersectionObserver 함수
import { Dispatch, SetStateAction } from "react";
const observerOptions = {
threshold: 0.4,
rootMargin: "0px 0px -70% 0px",
};
export const getIntersectionObserver = (
setState: Dispatch<SetStateAction<string>>
) => {
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.intersectionRect.top !== 0) {
setState(entry.target.id);
}
}
}, observerOptions);
return observer;
};Toc 컴포넌트
"use client";
import { getIntersectionObserver } from "@/utils/observer";
import { useEffect, useState } from "react";
export const Toc = () => {
const [currentId, setCurrentId] = useState<string>("");
const [headingEls, setHeadingEls] = useState<Element[]>([]);
useEffect(() => {
const observer = getIntersectionObserver(setCurrentId);
const headings = document.querySelectorAll("h2, h3");
const headingElements = Array.from(headings);
setHeadingEls(headingElements);
headingElements.map((header) => {
const id = header.textContent!;
header.id = id;
observer.observe(header);
});
}, []);
return (
<>
<div className="absolute ml-5 left-full">
<div className="fixed hidden text-xs xl:flex xl:flex-col max-w-[220px] gap-3 text-[#999]">
{headingEls.map((header, i) =>
header.nodeName === "H2" ? (
<div
className={`text-[12px] ${
currentId === header.textContent &&
"text-white text-[13px] transition-all duration-125 ease-in delay-0"
}`}
key={i}
>
<a href={`#${header.id}`}>{header.textContent}</a>
</div>
) : (
<div
className={`ml-5 text-[12px] ${
currentId === header.textContent &&
"text-white text-[13px] transition-all duration-125 ease-in delay-0"
}`}
key={i}
>
<a href={`#${header.id}`}>{header.textContent}</a>
</div>
)
)}
</div>
</div>
</>
);
};Reference
https://heropy.blog/2019/10/27/intersection-observer/
https://blog.hyeyoonjung.com/2019/01/09/intersectionobserver-tutorial/
