Intersection Observer 는 무한스크롤을 구현할때 사용하는구나 정도로만 알고있었는데
이번에 포트폴리오에 기능을 추가하면서 사용해보게 되어 정리해본다.
내가 구현하고자 한 기능은
1. 스크롤을 내릴때 내용에 해당하는 해시(#hash) 가 url에 추가되고, 맨 위로 올렸을때는 해시가 사라지도록 하는 기능이다.
2. 헤더에 해당하는 메뉴 활성화 스타일이 추가된다
이를 구현하기 위해 IntersectionObserver 와 jotai를 사용하였다.
(jotai는 앵커 상태를 관리하기 위해!)
IntersectionObserver 공식문서를 보니
대상 요소와 상위 요소의 뷰포트가 서로 교차하는(겹치는) 영역이 달라지는(변하는) 경우 이를 비동기적으로 감지하는 수단 이라고 한다.
(서로 겹치는 영역이 변하는 경우 이를 비동기적으로 감지하는 수단)
간단히 말하면 감시하는 요소가 화면에 보이느냐 안보이느냐를 감지하는 수단이다.
비동기적으로 감지함으로써 교차상태가 변화될때마다 UI를 업데이트하는 것이 아니라
백그라운드에서 감지한 결과를 기반으로 나중에 한번에 UI를 업데이트하게 된다.
👩🏻💻코드 작성하기
생성자 IntersectionObserver 로 observer 객체를 생성해준다.
옵저버 객체는 1. 콜백 함수와 2. 옵션 객체를 인자로 받는다.
const observer = new IntersectionObserver(callback, options);
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const { id } = entry.target;
if (entry.isIntersecting) {
setCurrentAnchor(id); // 현재 화면에 보이는 섹션의 ID를 currentAnchor로 설정
window.history.replaceState(null, "", `#${id}`); // URL 변경
}
});
},
{
rootMargin: "-60% 0px", // 60% 화면 밖으로 나갈 때 트리거되도록 설정
}
);
1. 콜백함수 (entries)=>{ ... }
교차 상태가 변할 때마다 (감시하는 요소가 뷰포트에 보이거나 사라질때) 호출된다.
이 함수는 감지한 DOM 요소들의 상태 정보가 담긴 entry의 배열로 이루어진 IntersectionObserverEntry 객체 배열을 인자로 받는다. => 이 배열을 entries 로 전달
뷰포트에 DOM 요소가 감지되면 entry의 isIntersection 속성이 true가 되고, if문 안의 코드가 실행된다
(여기서는 url에 #해시를 추가하고, 헤더(GNB)의 글씨 색을 변경해주는 동작을 실행했다)
2. 옵션 객체
옵저버가 어떻게 동작할지를 설정한다. 말그대로 옵셔널하게 설정하면 된다
root, rootMargin, threshold 등의 속성이 있다.
.observe() 메서드는 옵저버에게 특정 DOM 요소를 관찰하도록 지시한다.
=> 관찰할 대상을 .observe(target) 타겟으로 전달
타겟 요소가 화면에 보이거나 사라질 때마다 ( = 뷰포트 혹은 root 요소와 교차할 때 마다) 콜백 함수가 실행된다
// 섹션 관찰 시작
navItems.forEach(({ anchor }: { anchor: string }) => {
const section = document.getElementById(anchor);
if (section) {
observer.observe(section); // 각 섹션을 관찰
}
});
// LandingImage의 ID도 감지
const landingImage = document.getElementById(" ");
if (landingImage) {
observer.observe(landingImage); // LandingImage도 관찰
}
관찰 대상이 많거나 observer가 계속 활성 상태로 남아 있으면 불필요한 리소스를 소비하며 메모리 누수가 발생할 수 있으므로
disconnect() 메서드로 컴포넌트가 언마운트 되면 관찰을 중지하도록 한다.
return () => {
observer.disconnect(); // 컴포넌트가 언마운트되거나 갱신될 때 관찰을 중지
};
🙆♀️ 위 코드들을 useEffect 안에 작성했다.
IntersectionObserver 는 DOM을 직접 다루는 API 이기 때문에, 모든 DOM이 렌더링 된 후에 실행해야 한다.
useEffect의 콜백함수는 렌더링이 끝난 후에 실행되므로 옵저버 실행이 컴포넌트의 라이프 사이클과 일치하게 된다.
또한 DOM 조작과 같은 부수효과를 다루기 위한 useEffect 내에서 처리하는 것이 적절하다.
document는 전역 객체이고, HTML 내의 id는 고유하므로 어디에서나 DOM에 접근할 수 있지만
DOM이 렌더링 되고나서 옵저버가 실행되어야하므로 이를 염두에 두고 코드 위치를 고민하면 되겠다
🙆♀️ 페이지 맨 위로 갔을때 해시를 없애고 싶어
처음에는 section 만 관찰하도록 했더니 맨 위로 올라갔을때 해시가 사라지지 않았다.
랜딩이미지 DOM도 관찰해주도록 추가했다.
랜딩 이미지에 해당하는 div에 id = "" 로 빈 문자열을 지정해주었지만 이 경우에는 id 가 지정이 되지 않고 앵커도 없어지지 않았다
id = " " 하고 공백을 추가해주었더니 내가 원하는대로 반영되었다
추가로, url에 해시가 있을때 새로고침하면 해시가 계속 남아있게 되어 그걸 없애주기 위해 아래 코드도 추가했다.
useEffect(() => {
const scrollToTopOnRefresh = () => {
// 해시가 있으면 새로고침 시 맨 위로 스크롤
if (window.location.hash) {
window.scrollTo(0, 0);
}
};
scrollToTopOnRefresh();
}, []);
페이지 구성에 대해 설명하자면
<>
<LandingImage />
<HeaderComponent>
<main>
{children}
</main>
</>
Landing Image 컴포넌트
<div id = " " > 를 가지고 있는 페이지 최상단에 위치하는 컴포넌트 이다.
{children}
옵저버 코드에 있는 navItems 는 헤더에 보여주고싶은 카테고리 이름이 들어가있는 배열이다.
해당 배열의 데이터를 가지고 페이지를 구성하는 <Section /> 컴포넌트가 들어가있다.
각각의 섹션 컴포넌트는 각각의 id = {anchor} 를 가지고 있다.
const navItems = [A, B, C]
return {
navItems.map((item)=> <Section id={anchor}/>)
}
대략 이런 구조로 이루어져 있다.
✅ 배운 것
옵저버는 지정한 id를 갖는 DOM에 접근하여 뷰포트 교차 여부를 감지하여 콜백 함수를 실행한다.
'개발 공부 일지 > JavaScript' 카테고리의 다른 글
입출력 - readline이 뭘까? (0) | 2025.01.22 |
---|---|
스크롤 이벤트를 지양하고 Intersection Observer 를 써야 하는 이유 (0) | 2025.01.16 |
File vs FileList (FormData에 문자열 배열을 추가하면 FileList가 된다?) (0) | 2025.01.08 |
Blob (블롭) 알아보기 (0) | 2025.01.08 |
자바스크립트 - 콜백 함수의 첫번째 인자는 언제나 이벤트 객체 (1) | 2024.09.12 |