Baseline: Widely available (널리 사용 가능) *
✅ Chrome ✅ Edge ✅ Firefox ✅ Safari
이 기능은 잘 확립되어 있고 많은 기기와 브라우저 버전에서 작동해요. 2019년 3월부터 브라우저 전반에서 사용할 수 있게 됐어요.
* 이 기능의 일부는 지원 수준이 다를 수 있어요.
Intersection Observer API는 타겟 요소와 조상 요소 또는 최상위 문서의 뷰포트와의 교차 변화를 비동기적으로 관찰하는 방법을 제공해요.
안녕하세요! 프론트엔드 성능 최적화와 모던 UI 구현에 있어 핵심적인 역할을 하는 Intersection Observer API (교차 관찰자 API) 문서를 가져오셨군요.
무한 스크롤(Infinite Scroll), 지연 로딩(Lazy Loading), 혹은 스크롤에 반응하는 화려한 애니메이션을 구현할 때 예전에는 scroll 이벤트를 사용해서 브라우저를 혹사시켰다면, 이제는 이 API가 그 모든 작업을 아주 우아하고 가볍게 처리해 줍니다.
실무에서 리액트(React)의 useRef나 useEffect와 함께 어떻게 쓰이는지, 강사의 꿀팁을 곁들여서 완벽하게 이해시켜 드릴게요!
역사적으로 웹에서 어떤 요소가 화면에 보이는지(visibility), 혹은 두 요소가 서로 겹치는지를 감지하는 것은 아주 까다로운 작업이었습니다. 기존의 해결책들은 신뢰하기 어려웠고, 브라우저와 사용자가 접속한 사이트 전체를 버벅거리게(sluggish) 만드는 주범이었습니다. 웹이 발전함에 따라, 이런 종류의 정보에 대한 필요성은 다음과 같은 이유로 점점 더 커졌습니다.
과거에 이런 교차(intersection) 감지를 구현하려면, 스크롤 이벤트 핸들러 안에 루프를 돌리면서 관련된 모든 요소에 대해 Element.getBoundingClientRect() 같은 메서드를 호출해 필요한 정보를 긁어모아야 했습니다. 이 모든 코드가 메인 스레드(main thread)에서 실행되기 때문에, 이런 검사 로직이 단 하나만 있어도 심각한 성능 문제를 일으킬 수 있습니다. 한 사이트에 이런 검사 로직이 여러 개 로드되어 있다면 상황은 끔찍해집니다.
무한 스크롤을 사용하는 웹페이지를 상상해 보세요. 페이지 중간중간 들어간 광고를 관리하기 위해 외부(vendor) 라이브러리를 쓰고, 곳곳에 화려한 애니메이션 그래픽이 있으며, 알림 상자 등을 띄우는 커스텀 라이브러리도 쓴다고 칩시다. 이 각각의 기능들이 각자 자신만의 교차 감지 루틴(scroll 이벤트 리스너 등)을 가지고 메인 스레드에서 동시에 윙윙 돌아갑니다. 웹사이트 개발자는 자기가 쓰는 외부 라이브러리 내부 사정까지는 잘 모르기 때문에 이런 사태가 벌어지고 있다는 사실조차 모를 수 있습니다. 사용자가 페이지를 스크롤할 때마다 이 수많은 교차 감지 로직들이 쉴 새 없이 실행되고, 결국 사용자는 뚝뚝 끊기는 브라우저와 웹사이트, 그리고 비명을 지르는 컴퓨터 팬 소리에 몹시 화가 나게 됩니다.
Intersection Observer API는 특정 요소가 다른 요소(또는 뷰포트(viewport))와 교차하는(겹치는) 상태로 진입하거나 빠져나갈 때, 혹은 두 요소 간의 교차 비율이 지정된 수치만큼 변할 때마다 실행되는 콜백 함수(callback function)를 등록할 수 있게 해 줍니다. 이 방식을 사용하면, 웹사이트는 더 이상 이런 교차 상태를 감시하기 위해 메인 스레드에서 무언가를 할 필요가 없으며, 브라우저가 알아서 내부적으로 최적화된 방식으로 교차 상태를 관리하게 됩니다.
단, Intersection Observer API가 할 수 없는 일이 하나 있습니다. 요소들이 정확히 몇 픽셀만큼 겹쳤는지, 혹은 정확히 어느 픽셀이 겹쳤는지에 기반하여 로직을 트리거할 수는 없습니다. 이 API는 오직 "이 요소들이 대략 N% 정도 겹치면, 무언가를 해야 해!"라는 아주 흔하고 일반적인 유스케이스(use case)만을 해결해 줍니다.
Intersection Observer API를 사용하면 다음 두 가지 상황 중 하나가 발생할 때 호출되는 콜백(callback)을 설정할 수 있습니다.
일반적으로 여러분은 타겟 요소에서 가장 가까운 스크롤 가능한 조상 요소를 기준으로 교차 여부를 감시하고 싶을 것입니다. 만약 타겟 요소가 스크롤 가능한 조상을 가지고 있지 않다면 디바이스의 뷰포트가 기준이 되겠죠. 디바이스 뷰포트를 기준으로 교차를 감시하려면 root 옵션에 null을 지정하면 됩니다.
뷰포트를 루트로 사용하든 다른 요소를 루트로 사용하든 API의 작동 방식은 동일합니다. 타겟 요소의 가시성(visibility)이 변하여, 루트 요소와 겹치는 비율이 여러분이 원하는 목표 수치를 넘어설 때마다 여러분이 제공한 콜백 함수를 실행합니다.
타겟 요소와 루트 요소 사이의 교차 정도를 교차 비율(intersection ratio)이라고 합니다. 이는 타겟 요소가 화면(루트)에 보이는 비율을 0.0과 1.0 사이의 값으로 나타낸 것입니다.
교차 관찰자를 생성하려면 생성자(constructor)를 호출하면서, 교차 임계값(threshold)을 특정 방향(위든 아래든)으로 넘어설 때마다 실행될 콜백 함수를 전달하면 됩니다.
const options = {
root: document.querySelector("#scrollArea"), // 스크롤이 발생하는 기준 박스
rootMargin: "0px", // 기준 박스의 여백
scrollMargin: "0px",
threshold: 1.0, // 타겟이 100% 보일 때 콜백 실행
};
const observer = new IntersectionObserver(callback, options);
임계값(threshold)이 1.0이라는 것은, root 옵션으로 지정된 요소 안에서 타겟 요소가 100% 완전히 보일 때 콜백이 호출된다는 뜻입니다.
IntersectionObserver() 생성자에 전달되는 options 객체는 관찰자의 콜백이 호출되는 구체적인 상황을 제어할 수 있게 해줍니다. 여기에는 다음과 같은 필드들이 있습니다.
root
타겟 요소의 가시성을 확인할 때 뷰포트(기준점)로 사용될 요소입니다. 반드시 타겟 요소의 조상(ancestor)이어야 합니다. 값을 지정하지 않거나 null로 설정하면 기본적으로 브라우저의 뷰포트가 사용됩니다.
rootMargin
루트 요소 주변의 여백(마진)입니다. CSS의 margin 속성과 비슷하게 1~4개의 값으로 된 문자열을 받습니다 (예: "10px 20px 30px 40px" - 상, 우, 하, 좌). 값은 오직 픽셀(px)이나 퍼센트(%)만 가능합니다. 이 값들은 교차 여부를 계산하기 전에 루트 요소의 바운딩 박스(bounding box, 경계 영역) 각 변을 늘리거나 줄이는 역할을 합니다. 음수 값을 넣으면 영역이 줄어들고, 양수 값을 넣으면 영역이 커집니다. 기본값은 "0px 0px 0px 0px"입니다.
scrollMargin
중첩된 스크롤 컨테이너 주변의 마진으로, rootMargin과 동일한 값과 기본값을 가집니다. (주로 여러 스크롤이 중첩된 복잡한 구조에서 미리 교차를 감지하기 위해 사용됩니다.)
threshold
타겟 요소가 얼마만큼 보였을 때(퍼센트) 관찰자의 콜백이 실행되어야 하는지를 나타내는 단일 숫자 또는 숫자 배열입니다. 만약 50%가 보이는 순간만 감지하고 싶다면 0.5를 넣으면 됩니다. 만약 25%씩 더 보일 때마다(25%, 50%, 75%, 100%) 콜백을 실행시키고 싶다면 [0, 0.25, 0.5, 0.75, 1] 배열을 넣으면 됩니다. 기본값은 0이며, 이는 타겟 요소가 루트의 경계선에 1픽셀이라도 닿는 순간(즉, 화면에 등장하기 시작하는 순간) 바로 콜백이 실행됨을 의미합니다. 1.0은 타겟 요소의 모든 픽셀이 완전히 화면에 들어오기 전까지는 교차한 것으로 치지 않겠다는 뜻입니다.
💡 강사의 실무 팁:
rootMargin은 무한 스크롤(Infinite Scroll)을 만들 때 아주 마법 같은 옵션입니다! 보통 맨 아래에 투명한 타겟 박스(<div id="bottom-trigger"></div>)를 하나 깔아두고 이걸 관찰합니다. 이때rootMargin: "0px 0px 500px 0px"로 주면, 사용자가 화면 맨 밑바닥에 도달하기 전(아직 500px 남았을 때) 미리 교차 이벤트가 발생합니다. 그러면 사용자가 바닥에 부딪히기 전에 새로운 데이터를 미리 Fetch해서 부드럽게 이어지는 무한 스크롤을 구현할 수 있죠!
IntersectionObserver() 생성자에 전달되는 콜백 함수는 발생한 이벤트들에 대한 IntersectionObserverEntry 객체들의 배열(list)과 관찰자 객체 자신을 인자로 받습니다.
const callback = (entries, observer) => {
entries.forEach((entry) => {
// 각각의 entry 객체는 관찰 중인 타겟 요소 하나의 교차 변화 상태를 설명합니다:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting (★★★★★ 가장 많이 씁니다!)
// entry.rootBounds
// entry.target
// entry.time
});
};
콜백이 받는 entries 배열에는 설정한 임계값(threshold)을 넘어선 이벤트 각각에 대한 IntersectionObserverEntry 객체가 담겨 있습니다. 짧은 시간 동안 여러 개의 타겟 요소가 동시에 화면에 나타나거나, 하나의 타겟이 순식간에 여러 임계값을 돌파하면 한 번에 여러 개의 entry 객체가 배열에 담겨 들어올 수 있습니다.
entry 객체에는 가장 중요한 속성인 isIntersecting이 있습니다. 이 불리언 값이 true라면 타겟 요소가 화면(루트)과 겹치기 시작했다는 뜻이고, false라면 화면 밖으로 빠져나갔다는 뜻입니다.
아래 코드 조각은 요소가 화면에 나타날 때(교차 비율 75% 이상) 카운터를 증가시키는 콜백 예제입니다.
const intersectionCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let elem = entry.target;
if (entry.intersectionRatio >= 0.75) {
intersectionCounter++;
}
}
});
};
관찰자를 만들고 콜백까지 정의했다면, 이제 관찰자에게 어떤 타겟 요소를 지켜봐야 할지 알려주어야 합니다.
const target = document.querySelector("#listItem");
observer.observe(target);
// 이제 타겟 요소를 지정했으므로 (비록 지금 당장 화면에 보이지 않더라도)
// 최초의 상태를 알려주기 위해 콜백 함수가 즉시 한 번 실행됩니다.
이제 이 타겟 요소가 관찰자를 만들 때 설정한 임계값(threshold) 조건을 만족할 때마다 콜백이 호출됩니다.
💡 강사의 실무 팁:
리액트(React)에서 이 로직을 쓸 때는 보통 커스텀 훅(useIntersectionObserver등)으로 만들어 사용합니다.useEffect안에서 관찰자를 생성하고observe(ref.current)로 감시를 시작한 뒤, 컴포넌트가 언마운트될 때 반드시observer.disconnect()를 호출해서 관찰자를 메모리에서 깔끔하게 해제(cleanup)해 주어야 메모리 누수를 막을 수 있습니다!
Intersection Observer API가 다루는 모든 영역은 '사각형(rectangles)'으로 간주됩니다. 불규칙한 모양의 요소라도 그 요소의 모든 부분을 감싸는 가장 작은 직사각형(바운딩 박스)을 기준으로 계산됩니다.
우리는 요소를 추적하기 위한 컨테이너가 필요합니다. 이 컨테이너를 교차 루트(intersection root) 또는 루트 요소(root element)라고 부릅니다. 보통은 타겟 요소의 스크롤 가능한 부모 요소이거나, null을 지정하여 브라우저의 전체 뷰포트를 루트로 사용합니다.
관찰자를 생성할 때 루트 마진(rootMargin)을 설정하면 이 루트의 경계 영역을 더 늘리거나 줄일 수 있습니다.
루트 마진을 사용해 상자를 늘리는(양수 값) 효과는, 타겟 요소가 실제로 화면에 보이기 '전에' 미리 교차 상태가 된 것으로 간주하게 만듭니다. 이를 활용하면 이미지가 사용자의 눈에 보이기 직전에 미리 네트워크에서 로딩을 시작하도록(Lazy-loading) 만들 수 있습니다.
아래 예제 컨트롤러를 조작해 보면 그 원리를 명확히 알 수 있습니다:
(MDN 문서 내 인터랙티브 예제 코드 부분 - 생략)
(루트 내부의 복잡한 중첩 스크롤 컨테이너들을 다루기 위해 scrollMargin을 사용하는 예제입니다. 기본 원리는 rootMargin과 거의 동일하므로 실무에서는 뷰포트 전체를 감지하는 rootMargin 위주로 사용하셔도 충분합니다.)
Intersection Observer API는 타겟 요소가 화면에 1픽셀, 2픽셀 보일 때마다 미친 듯이 콜백을 호출하는 대신 임계값(thresholds)이라는 똑똑한 방식을 사용합니다.
예를 들어, 어떤 요소가 화면에 진입하거나 빠져나갈 때 가시성이 25%씩 변할 때마다(0%, 25%, 50%, 75%, 100%) 알림을 받고 싶다면, 관찰자를 만들 때 임계값 배열로 [0, 0.25, 0.5, 0.75, 1]을 지정하면 됩니다.
콜백이 호출될 때 받는 IntersectionObserverEntry 객체의 속성 중 가장 많이 쓰이는 것은 단연 isIntersecting입니다. 이 값이 true라면 타겟이 루트에 조금이라도 걸쳐서 화면에 보이기 시작했다는 뜻입니다. 이를 통해 요소가 화면 밖에서 안으로 들어온 것인지(true), 아니면 안에서 밖으로 빠져나간 것인지(false)를 아주 쉽게 판별할 수 있습니다.
(MDN 문서 내 각기 다른 임계값을 가진 4개의 박스 예제 코드 부분 - 생략)
광고 노출수나 분석 데이터(애널리틱스)를 수집할 때는, 요소가 기술적으로 화면에 렌더링되긴 했지만 opacity: 0으로 숨겨져 있거나 다른 요소 뒤에 가려져 있어서 사용자 눈에 실질적으로 보이지 않는 상태(visually compromised)라면 교차 이벤트를 무시하고 싶을 수 있습니다.
이럴 때 trackVisibility 옵션을 true로 설정하면 브라우저가 요소의 진짜 가시성을 좀 더 엄격하게 계산합니다. 단, 이 계산은 컴퓨터 자원을 아주 많이 소모하기 때문에 반드시 필요한 경우에만 사용해야 하며, 이 옵션을 켤 때는 성능 저하를 막기 위해 delay 옵션(최소 100ms 이상 권장)을 함께 설정하여 너무 자주 계산이 일어나는 것을 막아야 합니다.
이 섹션에서는 Intersection Observer API를 구성하는 두 가지 핵심 인터페이스를 소개합니다.
IntersectionObserver
Intersection Observer API의 심장 역할을 하는 기본 인터페이스입니다. 이 인터페이스는 동일한 관찰 조건(configuration) 하에서 여러 개의 타겟 요소(target elements)를 감시할 수 있는 '관찰자(observer)'를 생성하고 관리하는 메서드들을 제공합니다. 각각의 관찰자는 하나 또는 그 이상의 타겟 요소들이 공통된 조상 요소(ancestor element) 또는 최상위 Document의 뷰포트(viewport)와 교차(intersection)하는 정도가 변할 때, 이를 비동기적으로(asynchronously) 감지할 수 있습니다. 여기서 교차 여부를 판별하는 기준이 되는 이 조상 요소나 뷰포트를 우리는 루트(root)라고 부릅니다.
IntersectionObserverEntry
특정 변화(transition)가 일어난 바로 그 순간, 타겟 요소와 그것의 루트 컨테이너 사이의 교차 상태를 아주 상세하게 설명해 주는 객체입니다. 이 객체는 오직 두 가지 방법으로만 얻을 수 있습니다. 첫 번째는 IntersectionObserver의 콜백 함수에 인자(input)로 전달받는 것이고, 두 번째는 IntersectionObserver.takeRecords() 메서드를 명시적으로 호출하여 빼내는 것입니다.
백문이 불여일견이죠! 이 간단한 예제는 화면을 스크롤할 때, 우리가 타겟으로 삼은 요소가 화면(뷰포트)에 더 많이 보일수록(가시성이 높아질수록) 요소의 색상과 투명도가 변하도록 만듭니다.
💡 강사의 팁: 이 예제보다 조금 더 실무에 가까운, 예를 들어 웹페이지에 떠 있는 광고(ads) 같은 요소들이 사용자의 눈에 실제로 얼마나 오랫동안 노출되었는지 그 시간을 정확히 측정하고 통계를 기록하는 심화 예제가 궁금하시다면, Intersection Observer API로 요소의 가시성 시간 측정하기 문서를 참고해 보세요!
이 예제의 HTML은 매우 직관적이고 짧습니다. 우리의 타겟이 될 커다란 기본 요소(아이디가 창의적이게도 "box"입니다)와 그 상자 안에 들어갈 약간의 텍스트 콘텐츠가 전부입니다.
<div id="box">
<div class="vertical">Welcome to <strong>The Box!</strong></div>
</div>
이 예제에서 CSS의 역할이 엄청나게 중요한 것은 아닙니다. 그저 요소를 화면 가운데 예쁘게 배치하고, 요소가 가려지거나 나타날 때마다 background-color와 border 속성이 부드럽게 변하도록 CSS 트랜지션(transitions)을 설정해 두었을 뿐입니다.
#box {
/* ... 레이아웃을 위한 flex 등 생략 ... */
background-color: rgb(40 40 190 / 100%);
border: 4px solid rgb(20 20 120);
/* 상태가 변할 때 1초 동안 부드럽게 애니메이션되도록 설정 */
transition:
background-color 1s,
border 1s;
}
자, 이제 대망의 자바스크립트 코드를 살펴볼 시간입니다. 바로 이 코드가 Intersection Observer API를 활용해 마법을 부리는 주인공이죠.
가장 먼저, 우리는 필요한 변수들을 준비하고 observer를 설치할 함수를 호출해야 합니다.
// 0.0부터 1.0 사이를 20단계로 쪼개겠다는 뜻입니다.
const numSteps = 20.0;
const boxElement = document.querySelector("#box");
let prevRatio = 0.0;
let increasingColor = "rgb(40 40 190 / ratio)";
let decreasingColor = "rgb(190 40 40 / ratio)";
createObserver();
여기서 설정한 상수와 변수들은 다음과 같습니다:
numSteps
요소가 화면에 보이는 비율(visibility ratio) 0.0(완전히 가려짐)부터 1.0(완전히 다 보임) 사이에 얼마나 많은 임계값(thresholds) 구간을 만들 것인지를 나타내는 상수입니다. 여기서는 20을 주었으니 5% 단위로 아주 잘게 쪼개어 관찰하겠다는 뜻이 됩니다.
prevRatio
가장 마지막으로 임계값을 넘었을 때의 가시성 비율(visibility ratio)이 얼마였는지를 기억해 두기 위한 변수입니다. 이 값을 기억해 두면, 현재 타겟 요소가 화면에 '더 많이 보이고 있는지' 아니면 '더 가려지고 있는지' 그 방향을 쉽게 유추할 수 있습니다.
increasingColor
가시성 비율이 증가하고 있을 때(즉, 화면에 더 많이 나타나고 있을 때) 타겟 요소에 적용할 색상을 정의한 문자열입니다. 이 문자열 안의 "ratio"라는 단어는 나중에 타겟의 실제 가시성 비율 숫자로 치환될 것입니다. 즉, 요소가 화면에 더 많이 드러날수록 색이 변할 뿐만 아니라 알파값(투명도)도 높아져 점점 더 불투명하고 뚜렷해지게 됩니다.
decreasingColor
마찬가지로, 이 변수는 가시성 비율이 감소하고 있을 때(즉, 화면에서 사라지고 있을 때) 적용할 붉은 계열의 색상 문자열을 정의합니다.
이후 코드는 querySelector()를 사용해 "box"라는 ID를 가진 타겟 요소를 가져오고, 바로 뒤이어 createObserver()라는 함수를 호출합니다. 이 함수 안에서 본격적으로 observer를 조립하고 타겟에 부착하는 작업이 이루어집니다.
createObserver() 함수는 페이지 로드가 완료되면 딱 한 번 호출되며, 새로운 IntersectionObserver를 만들고 타겟 요소를 실제로 관찰(observing)하기 시작하는 역할을 담당합니다.
function createObserver() {
const options = {
root: null, // 브라우저 뷰포트를 기준으로 관찰합니다.
rootMargin: "0px", // 여백 없이 딱 화면 경계에 맞춥니다.
threshold: buildThresholdList(), // 0.0부터 1.0까지 20단계로 쪼갠 배열을 넣습니다.
};
const observer = new IntersectionObserver(handleIntersect, options);
observer.observe(boxElement); // 드디어 box 요소를 감시하기 시작합니다!
}
이 함수는 먼저 observer의 환경 설정을 담은 options 객체를 세팅하는 것으로 시작합니다. 우리는 타겟 요소가 문서의 뷰포트(viewport, 현재 보이는 화면 전체)를 기준으로 얼마나 가려지거나 보이는지를 관찰하고 싶기 때문에, root 값을 null로 지정합니다. 또한 마진 값인 rootMargin은 "0px"로 줍니다. 이렇게 하면 observer는 뷰포트의 경계선에 그 어떤 추가적인 공간(더하거나 빼는 공간) 없이, 딱 맞아떨어지는 뷰포트 경계 그 자체와 타겟 요소의 경계 상자가 교차하는지만을 깔끔하게 관찰하게 됩니다.
threshold(임계값) 속성에 들어가는 가시성 비율 배열은 buildThresholdList()라는 함수가 동적으로 생성해서 반환해 줍니다. 이 예제에서는 임계값의 개수가 꽤 많고(20개) 나중에 숫자를 쉽게 조절할 수 있도록 하려고, 손으로 배열을 다 치는 대신 프로그래밍 방식으로 만들었습니다.
options 객체의 준비가 모두 끝나면, IntersectionObserver() 생성자를 호출하여 새로운 observer를 탄생시킵니다! 이때 첫 번째 인자로는 교차 상태가 임계값을 넘을 때마다 실행될 콜백 함수인 handleIntersect()를 넘겨주고, 두 번째 인자로 방금 만든 options를 넘겨줍니다.
마지막으로, 생성된 observer 객체의 observe() 메서드를 호출하면서 우리가 감시하고 싶은 타겟 요소(boxElement)를 쏙 넣어주면 모든 세팅이 끝납니다.
💡 강사의 팁: 만약 여러분이 화면의 스크롤에 반응해서 나타나는 여러 개의 이미지나 섹션들을 관찰하고 싶다면 어떻게 할까요? 똑같이 observer 객체를 하나만 만들어 두고,
observer.observe(요소1),observer.observe(요소2)처럼 각각의 요소들에 대해observe()메서드만 연달아 호출해주면 됩니다!
임계값 목록을 찍어내는 buildThresholdList() 함수는 이렇게 생겼습니다.
function buildThresholdList() {
const thresholds = [];
const numSteps = 20;
for (let i = 1.0; i <= numSteps; i++) {
const ratio = i / numSteps;
thresholds.push(ratio);
}
thresholds.push(0);
return thresholds;
}
이 함수는 1부터 numSteps까지 정수 i를 증가시키면서 반복문을 돕니다. 매 반복마다 i / numSteps를 계산해 0.0보다 크고 1.0 이하인 비율 값을 만들어 thresholds 배열에 쑥쑥 집어넣습니다(push). 그리고 마지막으로 완벽히 가려졌을 때를 위해 0도 하나 추가해 줍니다.
기본값인 numSteps가 20이라고 했을 때, 이 함수가 최종적으로 뱉어내는 임계값 배열은 이런 모습이 됩니다.
| # | Ratio | # | Ratio | |
|---|---|---|---|---|
| 0 | 0.05 | 11 | 0.6 | |
| 1 | 0.1 | 12 | 0.65 | |
| 2 | 0.15 | 13 | 0.7 | |
| 3 | 0.2 | 14 | 0.75 | |
| 4 | 0.25 | 15 | 0.8 | |
| 5 | 0.3 | 16 | 0.85 | |
| 6 | 0.35 | 17 | 0.9 | |
| 7 | 0.4 | 18 | 0.95 | |
| 8 | 0.45 | 19 | 1 | |
| 9 | 0.5 | 20 | 0 | |
| 10 | 0.55 |
물론 실무에서는 [0, 0.25, 0.5, 0.75, 1] 처럼 배열을 코드에 하드코딩해서 직접 박아 넣는 경우가 훨씬 더 많을 겁니다. 하지만 이렇게 함수로 구현해 두면, 필요할 때 numSteps 변수만 살짝 바꿔서 관찰의 세밀함(granularity)을 자유자재로 조절할 수 있다는 장점이 있습니다.
이제 브라우저가 타겟 요소(우리의 "box")를 열심히 지켜보다가, 이 녀석이 화면에 나타나거나 가려져서 방금 우리가 만든 20개의 임계값 구간 중 하나를 '탁!' 하고 넘어가는 순간을 감지했다고 가정해 봅시다. 그러면 브라우저는 우리가 아까 넘겨주었던 핸들러 함수인 handleIntersect()를 즉시 호출합니다.
function handleIntersect(entries, observer) {
entries.forEach((entry) => {
// 1. 요소가 화면에 나타나는 중인지(가시성이 높아지는지) 확인합니다.
if (entry.intersectionRatio > prevRatio) {
// 나타나는 중이라면 파란색(increasingColor)을 적용하고, 'ratio' 글자를 현재 비율 수치로 교체합니다.
entry.target.style.backgroundColor = increasingColor.replace(
"ratio",
entry.intersectionRatio,
);
} else {
// 2. 요소가 화면 밖으로 사라지는 중이라면 붉은색(decreasingColor)을 적용합니다.
entry.target.style.backgroundColor = decreasingColor.replace(
"ratio",
entry.intersectionRatio,
);
}
// 3. 다음 계산을 위해 현재의 비율을 과거 비율 변수에 업데이트해 둡니다.
prevRatio = entry.intersectionRatio;
});
}
이 함수는 호출될 때 entries라는 배열을 인자로 받습니다. 우리는 forEach를 돌려서 각 IntersectionObserverEntry 객체들을 하나씩 까봅니다.
가장 먼저, 이 entry 객체 안에 있는 intersectionRatio(현재 교차 비율) 값이 우리가 과거에 저장해 두었던 prevRatio보다 더 커졌는지(going up)를 확인합니다.
background-color를 아까 만들어둔 increasingColor 문자열("rgb(40 40 190 / ratio)")로 바꿔줍니다. 이때 문자열 속 "ratio"라는 글자를 현재의 entry.intersectionRatio 숫자 값으로 싹 바꿔치기(replace) 합니다.decreasingColor 문자열을 사용해서 상자를 붉은색 계열로 바꿉니다. 마찬가지로 교차 비율을 알파값에 적용해 주면, 화면 밖으로 사라질수록 상자의 색이 점점 흐려지고 투명해집니다.그리고 이 모든 처리가 끝나면, 다음번에 이 함수가 불렸을 때 비교할 수 있도록 현재의 비율을 prevRatio 변수에 업데이트해 두는 것을 잊지 않습니다!
아래에 완성된 결과물이 있습니다! 스크롤을 위아래로 움직여가며, 상자가 화면에 나타나고 사라질 때마다 색상과 투명도가 어떻게 실시간으로 변하는지 직접 눈으로 확인해 보세요. (파란색으로 선명해졌다가, 붉은색으로 흐려지는 걸 보실 수 있을 거예요!)
[스크롤 시 색상과 투명도가 변하는 상자 데모 화면]
💡 강사의 팁: Intersection Observer는 프론트엔드 개발자에게 빛과 소금 같은 존재입니다! 예전에는 화면 스크롤 이벤트(
window.addEventListener('scroll'))를 달아서 요소의 위치 좌표를 직접 복잡하게 수학으로 계산해야 했는데요, 이게 브라우저 성능을 어마어마하게 깎아먹는 주범이었습니다. 하지만 이 API를 사용하면 브라우저가 보이지 않는 뒷단에서 아주 최적화된 방식으로 가시성 여부를 계산해서 "지금 임계값 넘었어!"라고 우아하게 알려준답니다. 무한 스크롤(Infinite Scroll), 이미지 지연 로딩(Lazy Loading), 스크롤 애니메이션 등은 무조건 이 녀석으로 구현하세요!