밝을희 클태

[NextJS] 스크롤 애니메이션 구현하기 IntersectionObserver API 본문

NextJS

[NextJS] 스크롤 애니메이션 구현하기 IntersectionObserver API

huipark 2023. 12. 3. 01:43

 

구현 화면

 

 

IntersectionObserver API

는 특정요소가 뷰포트와 교차하는지 비동기적으로 관찰하는 데 사용된다. 이걸로 스크롤 애니메이션을 구현할 수 있다.

 

코드 :

function Item({ children }: { children: React.ReactNode }) {
	const ref = useRef<HTMLDivElement | null>(null);
	const [visible, setVisible] = useState(false);

	useEffect(() => {
		const observer = new IntersectionObserver(
			(entries) => {
				entries.forEach(({ target, isIntersecting }) => {
					if (target === ref.current) setVisible(isIntersecting);
				});
			},
			{
				threshold: 1,
			}
		);
		if (ref.current) observer.observe(ref.current);
		return () => observer.disconnect();
	}, []);

	return (
		<div ref={ref}>
			{children}
			<style jsx>{`
				div {
					opacity: ${visible ? 1 : 0};
					transform: ${visible ? "translateY(0)" : "translateY(30px)"};
					transition: opacity 0.5s ease, transform 1s ease;
				}
			`}</style>
		</div>
	);
}

 

 

코드 설명 :

 

외부 요소와 연결하기 위해 ref 를 초기화해준다. typescript를 사용해서 참조하려는 타입을 명시해 준다.

	const ref = useRef<HTMLDivElement | null>(null);

 

요소가 뷰포트와 만났을 때 확인하기 위한 useState

	const [visible, setVisible] = useState(false);

 

 

IntersectionObserver 초기화

 

entries는 감시되고 있는 요소의 정보를 담고 있는 배열이다. entriestarget(감시되고 있는 요소 자체), isIntersecting(요소가 뷰포트에 들어왔으면(true), 아니면(false) ) 

targetrefDOM을 비교하면서 같으면 visible의 상태를 업데이트, 이때 target이 뷰포트와 교차한 상태면 (true) 아니면 (false)

그리고 threshold는 요소가 뷰포트에 얼마나 보이면 isIntersectingtrue로 바꿀지에 대한 값이다.

 

IntersectionObserver을 초기화해주고 ref.current가 있으면

if (ref.current) observer.observe(ref.current);

observer에 감시 대상을 추가해 준다.

 

 

그리고 컴포넌트가 언마운트 되면 아래 코드로 감시를 중단한다 메모리 누수를 방지하는데 필요하다.

return () => observer.disconnect();

 

 

new IntersectionObserver(callback, options)

	useEffect(() => {
		const observer = new IntersectionObserver(
			(entries) => {
				entries.forEach(({ target, isIntersecting }) => {
					if (target === ref.current) setVisible(isIntersecting);
				});
			},
			{
				threshold: 1,
			}
		);
		if (ref.current) observer.observe(ref.current);
		return () => observer.disconnect();
	}, []);

 

 

처음엔 이렇게 했는데 이렇게 하면 Item 컴포넌트 하나당 각자의 독립적인 IntersectionObserver를 가지게 된다 복잡한 로직이나 개별적인 애니메이션을 사용할 때는 이렇게 해야 되겠지만 위 방법을 사용하면 성능이 저하된다.

 

로직이 같은 컴포넌트를 공유할 때는 부모 컴포넌트에서 하나의 IntersectionObserver를 사용하는 게 성능에 좋다.

 


 

하나의 IntersectionObserver 사용

코드 :

function Item({
	children,
	visible,
}: {
	children: React.ReactNode;
	visible: boolean;
}) {
	return (
		<div>
			<div
				style={{
					opacity: visible ? 1 : 0,
					transform: visible ? "translateY(0)" : "translateY(30px)",
					transition: "opacity 0.5s ease, transform 1s ease",
				}}
			>
				{children}
			</div>
		</div>
	);
}
export default function AboutScreen() {
	const [visibleItems, setVisibleItems] = useState(new Set());

	useEffect(() => {
		// IntersectionObserver 인스턴스 생성
		const observer = new IntersectionObserver(
			(entries) => {
				// 각 관찰 대상의 변화에 대한 콜백 함수
				entries.forEach(({ target, isIntersecting }) => {
					// 각 관찰 대상에 대한 처리
					const id = target.getAttribute("data-id"); // 각 대상의 data-id 속성 값 추출

					if (isIntersecting) {
						// 요소가 뷰포트에 들어왔는지 확인
						// visibleItems 상태를 업데이트하여 요소의 id 추가
						setVisibleItems((prevItems) => new Set(prevItems).add(id));
					} else {
						// visibleItems 상태를 업데이트하여 요소의 id 제거
						setVisibleItems((prevItems) => {
							const newItems = new Set(prevItems);
							newItems.delete(id);
							return newItems;
						});
					}
				});
			},
			{
				threshold: 0.9, // 관찰 기준점 설정 (90% 요소가 보여야 감지)
			}
		);

		// 모든 .item 요소를 관찰 대상으로 추가
		document.querySelectorAll(".item").forEach((item) => {
			observer.observe(item);
		});

		// 컴포넌트가 언마운트 될 때 관찰을 중단
		return () => observer.disconnect();
	}, []);

	return (
		<div className="container">
			<div className="image-container">
				<Image
					src={starBack}
					alt="starBackground"
					quality={100}
					layout="fill"
				/>
			</div>
			{Array.from({ length: 4 }, (_, index) => index).map((index) => (
				<Item key={index} visible={visibleItems.has(`item-${index}`)}>
					<div
						className="item"
						data-id={`item-${index}`}
						style={{
							width: 200,
							height: 200,
							background: index % 2 === 0 ? "red" : "blue",
						}}
					></div>
				</Item>
			))}
		</div>