-
[web] Intersection Observer API와 활용programing/Web 2021. 5. 16. 14:39
Intersection Observer API
🚨 글을 작성하는 현재 시점에선 해당 API는 실험적인 기능임을 명심하자!
특정 DOM 요소(이하 타겟 요소)와 그 상위 요소, 혹은 최상위 도큐먼트인 viewport(이하 루트 요소)와의 교챠영역에 대한 변화를 비동기적으로 감지할 수 있는 API.
보통 스크롤이 어느 위치에 도달했을 때, 애니메이션 혹은 특정 로직을 트리거하기 위한 장치로 많이 사용한다.
new IntersectionObserver
로 생성된IntersectionObserver
인스턴스 객체는 설정된 비율 만큼의 가시성을 계속 감시한다. 이 생성자 함수는 다음과 같은 세가지 옵션을 설정할 수 있다.- root
- 디폴트는 viewport. 교챠영역의 기준이 되는 요소.
- rootMargin
- 디폴트는 0, 0, 0, 0. 교챠영역을 계산할 때, 루트 요소에 더할 값.
- threshold
- 타겟 요소에 대한 루트 요소의 교챠 영역의 비율. 즉, 루트 요소 영역에 타겟 요소의 영역이 얼만큼 포함되어 있는지 나타내는 값. (배열도 가능하다.)
- 교차 영역 비율이 해당 값을 지나갈 때(이상, 이하 둘 다), 콜백이 호출된다.
- 0이라면 교차 영역이 1px이라도 넘으면 콜백을 호출한다는 뜻이며, 1이라면 교차 영역이 타겟 요소를 전부 포함해야 콜백을 호출한다는 뜻이다.
타입들
interface DomRectReadOnly { bottom: number, height: number, left: number, right: number, top: number, width: number, x: number, y: number, }
interface IntersectionObserverEntry { boundingClientRect: DomRectReadOnly, // observable의 전체 레이아웃 정보 intersectionRatio: number, // intersection root와 observable이 얼마나 교차하는지. (0 ~ 1) intersectionRect: DomRectReadOnly, // intersection root와 observable이 교차한 영역에 대한 레이아웃 정보 isIntersecting: boolean, // intersection root와 교차 여부 isVisible: boolean, rootBounds: DomRectReadOnly, // intersection root의 레이아웃 정보 target: HTMLElement, // observable 요소 time: number, // IntersectionObserver 인스턴스가 생성된 시점을 기준으로, 이벤트가 일어난 시간 }
각 인터페이스에 대한 자세한 정보는 하단의 참고 섹션을 참고해주세요.
전체적인 흐름
- 옵저버 객체를 생성한다.
const observer = new IntersectionObserver((entries) => { const target = entries[0]; });
- 옵저버 객체로 감시할 대상을 등록한다.
const target = documnet.querySelector('#target'); observer.observe(target);
- 콜백 함수를 로직에 맞게 수정한다.
일반적인 경우(대상 요소가 완전히 벗어나거나 완전히 들어온 경우 트리거)에는 다음과 같은 사항만 체크하면 된다.
intersectionRatio
가1
인지0
인지isIntersecting
이true
인지false
인지isVisible
이true
인지false
인지
Navigation Bar 숨기기, 보이기
💡 해당 섹션은 단일 요소를 관찰하는 방법에 대해 설명합니다.
스크롤의 위치에 따라 네비게이션 바가 보여지고 숨겨지는 로직을 구현해보도록 하겠습니다.
Next.js 에서는 컴포넌트 함수 내부에서는
document
객체에 접근할 수 없기 때문에,useEffect
내부에서 DOM 객체에 접근해야 합니다.useEffect(() => { // document 객체는 컴포넌트 함수 내에서 접근할 수 없다는 것을 명심! const preHeader = document.querySelector('#pre-header'); const observer = new IntersectionObserver((entries) => {}); return () => { observer.disconnect(); }; }, []);
감시할 대상을 얻은 뒤, observer를 만듭니다. 클린업 함수에서 감시를 해제하는 것도 잊지 마세요!
function isHTMLElement(target: any): target is HTMLElement { return target instanceof HTMLElement; } useEffect(() => { ... if (isHTMLElement(preHeader)) { observer.observe(preHeader); } ... }, []);
타입가드 함수를 이용해 타입체크를 한 뒤, 감시합니다.
const observer = new IntersectionObserver((entries) => { const target = entries[0]; if (isHTMLElement(headerRef.current)) { if (target.intersectionRatio === 0) { headerRef.current.classList.add('scroll-down'); } if (target.intersectionRatio > 0) { headerRef.current.classList.remove('scroll-down'); } } });
입맛에 맞게 콜백 함수를 수정합니다.
결과
스크롤을 내리면 경계선과 그림자가 생기며, 다시 스크롤이 맨 위로 붙으면 사라집니다.
One Page Scroll
💡 해당 섹션은 다중 요소를 관찰하는 방법에 대해 설명합니다.
💡 해당 기능은
scroll-snap-type
속성과scroll-snap-align
속성으로 쉽게 구현할 수 있습니다.이번에는 One Page Scroll (혹은 Full Page Scroll) 을 구현해보도록 하겠습니다.
먼저, Body 컴포넌트를 구현합니다.
import React, { useEffect } from "react"; export function Body() { return ( <div> <div className={"screen lightpink"}>first screen</div> <div className={"screen lightblue"}>second screen</div> <div className={"screen lightgreen"}>third screen</div> <div className={"screen lightcoral"}>fourth screen</div> <div className={"screen lightsteelblue"}>fifth screen</div> </div> ); }
여기서 각 스크린 요소가
100vh
를 가지도록 하는 것이 중요합니다..screen { display: flex; align-items: center; justify-content: center; width: 100vw; height: 100vh; font-size: 4rem; } .lightpink { background-color: lightpink; } .lightblue { background-color: lightblue; } .lightgreen { background-color: lightgreen; } .lightcoral { background-color: lightcoral; } .lightsteelblue { background-color: lightsteelblue; }
그리고 obserber 객체를 생성합니다.
useEffect(() => { const observer = new IntersectionObserver( (entries) => {}, { threshold: 0.1, } ); return () => { observer.disconnect(); }; });
이제 TS를 위한 유틸 함수를 정의합니다.
function isHTMLElement(target: any): target is HTMLElement { return target instanceof HTMLElement; } function isNodeList(target: any): target is NodeList { return target instanceof NodeList; }
useEffect
내에 관찰 로직을 구현합니다.const targets = document.querySelectorAll(".screen"); if (isNodeList(targets)) { const nodeArray = Array.from(targets); const isAllDiv = nodeArray.every((node) => node.tagName === "DIV"); if (isAllDiv) { nodeArray.forEach((node) => observer.observe(node)); } }
콜백 함수에 스크롤 기능을 구현합니다.
(entries) => { const [observable] = entries.filter( (observable) => observable.isIntersecting ); if (observable && isHTMLElement(observable.target)) { const offsetTop = observable.target.offsetTop; window.scroll({ left: 0, top: offsetTop, behavior: "smooth", }); } }
결과
번외! scroll-snap 이용해서 구현하기
번외로, CSS의 scroll-snap 을 이용해, 더 편리하게 구현할 수 있습니다.
먼저, 캐러셀처럼 크기가 동일한 자식 요소들과, 이것들을 담는 부모 요소를 정의합니다.
<div className={"screen-container"}> <div className={"screen lightpink"}>first screen</div> <div className={"screen lightblue"}>second screen</div> <div className={"screen lightgreen"}>third screen</div> <div className={"screen lightcoral"}>fourth screen</div> <div className={"screen lightsteelblue"}>fifth screen</div> </div>
여기서 중요한 점은, 부모 요소가 스크롤을 가질 수 있도록 하는 것입니다. 즉, 부모요소에 overflow 가 발생하면서, 해당 css 속성이 visible 같은 것이 아니여야 된다는 뜻이죠.
.screen-container { overflow: scroll; /* visible 같은 속성은 피해주세요 */ width: 100vw; height: 100vh; scroll-snap-type: y mandatory; /* y축, 맨데토리 */ } .screen { width: 100vw; /* 캐러셀처럼, 부모와 자식의 크기가 동일해야 보기 좋습니다. */ height: 100vh; scroll-snap-align: start; /* 스냅 위치를 지정합니다. 자세히는 모르겠어요.. */ }
이렇게 구성하면 구현 끝!
참고
'programing > Web' 카테고리의 다른 글
[CSS] column-count를 이용하여 masonry layout 구현하기 (2) 2021.02.28 [React] Styled-Components를 이용한 애니메이션 (0) 2021.01.30 [Web] Web Storage에 대하여 (2) 2020.12.26 [Web] JSON Web Token (0) 2020.12.03 [WEB] CORS에 대한 정리 (0) 2020.07.22 댓글
- root