Intersection Observer API을 도입하려 했던 이유
- 프로젝트중 특정 요소가 보인 뒤에 다음 요소가 화면에 렌더링 되게끔 구성을 하고 싶었습니다.
- 이를 구현하기 위해서 여러 방법이 있겠지만 Intersection Observer API가 가장 좋은 방법이지 않을까 해서 위의 API를 사용해 보았습니다.
- 코드하기에 앞서, 공식문서에서 Intersection Observer API의 스펙을 살펴보았습니다.
간추린 공식문서 설명
- intersection observer API는 부모 엘리먼트나 탑 레벨 문서(document)의 뷰포트를 이용하여 타겟팅한 엘리먼트의 변화를 감지하는 API 입니다.
- 즉, 타겟팅 엘리먼트의 감지 여부에 조건을 걸어 어떤 역할을 부여할 수 있다는 의미입니다.
Intersection Observer 생성
let options = {
root: document.querySelector("#scrollArea"),
rootMargin: "0px",
threshold: 1.0,
};
let observer = new IntersectionObserver(callback, options);
let callback = (entries, observer) => {
entries.forEach((entry) => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
Observer를 생성하려면 IntersectionObserver 생성자 함수를 이용하시면 됩니다.
생성자 함수는 callback 함수와 options 객체를 받습니다.
options 객체는 root와 rootMargin, threshold를 프로퍼티로 가지고 있습니다.
- root는 타겟 엘리먼트의 뷰포트 대상으로 사용되는 엘리먼트입니다. 반드시 타겟의 조상요소여야 합니다.
디폴트값(입력하지 않았거나 null로 둘 경우)은 document의 뷰포트입니다. - rootMargin은 root를 둘러싸는 마진입니다. intersection(교차도)이 브라우저에 의해 계산되기 전에 루트 엘리먼트의 바운딩 박스를 줄이거나 늘립니다. 마진값에 대한 문자열을 값으로 가집니다.
기본값은 0px이고 "10px 20px 30px 40px"와 같이 설정할 수 있습니다. - threshold는 넘버나 넘버 배열을 값으로 받습니다. 이 숫자들은 콜백함수의 실행 조건이 됩니다. 만약 타겟이 root에 50% 이상 노출될 시에 callback이 실행되게 하려면 0.5를 기입하면 됩니다.
매번 25%마다 콜백을 실행시키려면 [0, 0.25, 0.5, 0.75, 1.0] 을 대입하면 됩니다.
callback 함수는 threshold 조건에 의해 실행되는 함수입니다. entries와 observer를 인수로 가집니다.
- entries는 관찰중인 요소들이 담긴 배열입니다. (forEach로 실행되는 entry가 담겨있는 게 맞습니다.)
- entry는 intersection의 상태를 가지고 있는 객체입니다.
root와 타겟이 교차(intersection)되었다면 isIntersecting은 true가 됩니다.
- observer는 Intersection Observer 객체 자체를 나타냅니다.
- 따라서 콜백함수 실행 도중에 특정 조건에 따라 observer를 컨트롤 할 수 있습니다.
- 만약 특정 조건이 갖춰진다면 Intersection의 구독을 끊는 형식으로 말입니다.
도입 사례
- 뷰포트에 이곳은~ 연구합니다 부분이 나온 뒤에 카드가 보이게 만들고 싶었습니다.
- 여러 방법이 있겠지만, 브라우저의 부하가 적은 Intersection Observer API를 사용하는 것이 좋다고 판단했고, 실행에 옮겼습니다. 웹 API는 렌더링 이후에 실행이 되어야 하므로 useEffect를 사용했고, 훅으로 분리했습니다.
초기 useMonitortElement 훅
- Element를 감지하는 훅이라서 useMonitorElement라고 네이밍 했습니다.
// useDetectElement.ts
import { MutableRefObject, useEffect } from "react";
export default function useMonitorElement(
callback: IntersectionObserverCallback,
monitoredElementRef: MutableRefObject<null>
) {
useEffect(() => {
if (!monitoredElementRef.current) return;
let options = {
root: null,
rootMargin: "0px",
threshold: 0.5,
};
let MonitoredElement = new IntersectionObserver(callback, options);
MonitoredElement.observe(monitoredElementRef.current);
return () => MonitoredElement.disconnect();
}, []);
}
- 페이지 컴포넌트(app router 사용 중)에서는 아래와 같이 useDetectElement를 호출합니다.
// page.tsx
// ...
const displayRef = useRef(null);
const showCardCallback: IntersectionObserverCallback = (
entries,
observer
) => {
entries.forEach((entry) => {
console.log("entries", entries);
console.log("entry", entry);
if (entry.isIntersecting) {
setIsShowCard(true); // 사실은 이것만 page.tsx의 것이 아닐까?
observer.unobserve(entry.target);
}
});
};
useMonitorElement(showCardCallback, displayRef); // 이것도 상당히 못생긴 것 같다.
// ...
- 동작은 잘 합니다.
- 하지만 코드가 굉장히 번잡해보였습니다. showCardCallback의 로직 대부분이 page.tsx에 없어도 될 것 같았습니다.
- 리팩토링이 필요한 시점입니다.
리팩토링 useMonitorElement
https://dev.rase.blog/21-12-07-intersection-observer/
위 블로그의 코드를 참조해서 리팩토링을 실시했습니다.
구현하고 싶은 상황이 비슷해서 코드가 비슷하게 나왔습니다.
// useMonotorElement.ts
import { useEffect, useRef } from "react";
let options = {
root: null,
rootMargin: "0px",
threshold: 0.5,
};
export default function useMonitorElement(callback: () => void) {
const monitoredElement = useRef(null);
useEffect(() => {
if (!monitoredElement.current) return;
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
observer.unobserve(entry.target);
callback();
}
});
}, options);
observer.observe(monitoredElement.current);
return () => observer.disconnect();
}, [callback, monitoredElement.current]);
return monitoredElement;
}
- useRef를 훅 내부에서 선언하고 리턴합니다. 이 훅을 사용할 페이지에서
const element = useMonitorElement(callback) 처럼 활용한 뒤, element를 태그의 ref에 넣어주면 동작합니다. - 저는 isIntersecting이 한번만 작동하게 만들고 싶었습니다. 그래서 isIntersecting이 한번 동작하면 observer.unobserve(entry.target)을 사용해 동작한 타겟의 관찰을 취소 해주었습니다.
- 컴포넌트가 언마운트되었을 때 구독이 반드시 취소되어야 하므로 클린업 함수도 제대로 달아주었습니다.
// page.tsx
// ...
const showCardCallback = () => {
setIsShowCard(true);
};
const monitoredRef = useMonitorElement(showCardCallback);
return (
<>
<div className={styles.welcome_message}>
<p ref={monitoredRef}>
// ...
- 리팩토링으로 인해 page.tsx 내부의 코드가 확 깔끔해 졌습니다!!!!!!!
결론
- Intersection Opserver API 사용법이 엄청나게 어려운 것으로만 알고 있었는데, 막상 공부해보니 그렇게 어려운 기능은 아니었습니다. 역시 공식문서를 읽어야 답이 나옵니다.
- 리팩토링 후 가독성과 코드의 응집율이 훨씬 좋아진 것 같습니다. 행복합니다.
- 구글링과 공식문서링(?)을 통해 꾸준히 성장해야 겠습니다.
'개발 일지' 카테고리의 다른 글
[클린코드] Header 코드 정리 (0) | 2023.07.14 |
---|---|
실무에서 바로 쓰는 프론트엔드 클린코드 감상 (0) | 2023.07.14 |
position sticky 헤더의 flickering 문제 (0) | 2023.07.10 |
[Next.js] 앱 라우터에 scss 스타일 폴더 세팅하기 (0) | 2023.07.06 |
Peer Dependencies 문제 인지 및 해결 (0) | 2023.07.04 |