Intersection Observer
고친 사람 github-actions[bot]
Intersection Observer 는 웹 페이지의 어떤 요소가 화면에 들어오거나 나갈 때 브라우저가 알려 주는 기능입니다. 페이지 코드는 지켜볼 요소를 맡겨 두고 알림을 기다리기만 합니다. 스크롤할 때마다 요소의 위치를 직접 잴 필요가 없어집니다. 이미지를 늦게 받거나 목록을 끝없이 이어 붙일 때 씁니다.
쉽고 빠른 이해
무슨 일을 하는 물건인가 — 「이 요소가 화면에 보이면 알려 줘」를 브라우저에 맡기는 기능입니다. 긴 페이지 아래쪽의 이미지를 화면에 다가올 때 비로소 받게 할 수 있습니다.
왜 이렇게 하나 — 맡기지 않으면 스크롤이 일어날 때마다 코드가 요소 위치를 직접 물어야 합니다. 스크롤 한 번에 그 확인이 수십 번 돕니다. 그동안 화면이 버벅일 수 있습니다.
어떻게 도나
- 알림을 받을 함수를 넘겨 관찰자를 만듭니다
- 지켜볼 요소를 관찰자에게 등록합니다
- 요소가 화면 경계를 넘으면 브라우저가 그 함수를 부릅니다
- 볼일이 끝난 요소는 등록을 풉니다
대가 — 브라우저는 화면을 다시 그릴 때에 맞춰 겹침을 확인합니다. 그래서 알림은 조금 늦게 옵니다. 스크롤에 맞춰 매 순간 위치가 필요한 일에는 안 맞습니다. 다른 요소에 가려져 있어도 화면 경계 안이면 보인다고 알립니다.
상세
이 절은 Intersection Observer 를 부르는 순서를 따라갑니다. 먼저 이 기능이 없을 때 무엇이 곤란했는지 봅니다. 이어서 관찰자를 만드는 코드, 알림에 담기는 값, 알림 시점을 바꾸는 옵션 셋을 봅니다. 끝에서 이미지 지연 로딩 예제로 전체를 묶고 언제 쓰고 언제 안 쓰는지를 가립니다.
Intersection Observer 는 브라우저가 자바스크립트 코드에 내어 주는 기능 가운데 하나입니다. 페이지 코드는 이 기능을 불러 쓰기만 합니다. 겹침을 재는 일은 브라우저 안에서 일어납니다.
화면에 보이는 영역과 요소
웹 페이지는 대개 화면보다 깁니다. 브라우저 창에서 지금 보이는 사각형 영역을 뷰포트라고 부릅니다. 스크롤을 내리면 페이지가 위로 밀립니다. 그러면 뷰포트에 다른 부분이 들어옵니다.
페이지를 이루는 이미지 · 문단 · 버튼 하나하나를 요소라고 부릅니다. Intersection Observer 가 지켜보는 것이 이 요소입니다.
이름의 intersection 은 「겹침」이라는 뜻입니다. 요소의 사각형과 뷰포트의 사각형이 겹치기 시작하면 요소가 화면에 들어온 것입니다. 겹침이 사라지면 나간 것입니다. Intersection Observer 는 이 겹침을 지켜보는 관찰자입니다.
직접 재던 옛 방식
이 기능이 생기기 전에는 스크롤 이벤트로 같은 일을 했습니다. 스크롤 이벤트는 페이지가 스크롤될 때마다 브라우저가 코드에 보내는 신호입니다. 코드는 신호를 받을 때마다 요소의 위치를 물었습니다. 그리고 그 위치가 뷰포트 안인지 견줬습니다.
window.addEventListener("scroll", () => {
const r = img.getBoundingClientRect();
if (r.top < window.innerHeight) {
load(img);
}
});
getBoundingClientRect 는 요소가 뷰포트 기준으로 어디에 있는지 돌려줍니다. window.innerHeight 는 뷰포트의 높이입니다. 위 코드는 이미지의 윗변이 뷰포트 아래 끝보다 위로 올라오면 이미지를 받습니다.
첫째 곤란은 횟수입니다. 스크롤 이벤트는 손가락을 한 번 쓸어 올리는 동안에도 수십 번 옵니다. 지켜볼 요소가 백 개면 확인도 그 백 배로 늡니다.
둘째 곤란은 이 확인이 도는 곳입니다. 페이지의 자바스크립트는 메인 스레드 하나에서 돕니다. 메인 스레드는 화면 그리기와 클릭 처리도 함께 맡는 실행 흐름입니다. 확인이 길어지면 그동안 화면이 멈칫합니다.
셋째 곤란은 위치를 묻는 일 자체의 비용입니다. 위치를 물으면 브라우저가 레이아웃을 다시 계산해야 할 때가 있습니다. 레이아웃은 요소마다 크기와 위치를 정하는 계산입니다. 코드가 방금 요소를 더하거나 크기를 바꿨다면 브라우저는 답하기 전에 이 계산부터 끝내야 합니다.
Intersection Observer 는 이 확인을 브라우저에게 넘깁니다. 브라우저는 화면을 그리면서 요소 위치를 어차피 알고 있습니다. 그래서 그 김에 겹침이 바뀐 요소만 골라 알려 줍니다.
관찰자 만들기와 등록
쓰는 순서는 셋입니다. 관찰자를 만듭니다. 요소를 등록합니다. 그리고 알림을 받습니다.
const io = new IntersectionObserver(onSee);
io.observe(img); // img 를 지켜본다
new IntersectionObserver 는 관찰자 객체를 만듭니다. 인자 onSee 는 겹침이 바뀔 때 브라우저가 부를 함수입니다. 이렇게 나중에 불러 달라고 넘기는 함수를 콜백이라고 부릅니다.
observe 로 지켜볼 요소를 등록합니다. 관찰자 하나에 요소를 여럿 등록해도 됩니다. 등록하자마자 콜백이 한 번 불립니다. 지금 겹쳐 있는지 아닌지를 첫 알림으로 알려 주는 것입니다.
볼일이 끝나면 등록을 풉니다. unobserve(요소) 는 요소 하나를 뺍니다. disconnect() 는 이 관찰자가 지켜보던 요소를 전부 놓습니다.
알림에 담기는 값
콜백은 첫 인자로 배열 하나를 받습니다. 배열의 칸 하나가 겹침이 바뀐 요소 하나의 기록입니다. 이 기록 객체를 IntersectionObserverEntry 라고 부릅니다. 여러 요소가 한꺼번에 바뀌면 기록도 여러 칸이 한 번에 옵니다.
기록에서 자주 읽는 값은 셋입니다.
| 값 | 뜻 |
|---|---|
isIntersecting |
지금 겹쳐 있으면 true |
intersectionRatio |
요소 넓이 가운데 겹친 몫. 0 에서 1 사이 |
target |
겹침이 바뀐 요소 |
요소의 절반이 뷰포트에 걸쳐 있을 때 첫 기록을 읽으면 이렇게 나옵니다.
function onSee(entries) {
const e = entries[0];
e.isIntersecting; // true
e.intersectionRatio; // 0.5
}
intersectionRatio 가 0.5 면 요소 넓이의 절반이 뷰포트 안에 있다는 뜻입니다. 기록에는 이 밖에도 요소의 사각형, 겹친 부분의 사각형, 기록이 만들어진 시각이 들어 있습니다.
알림 시점을 바꾸는 옵션 셋
관찰자를 만들 때 둘째 인자로 옵션을 줄 수 있습니다. 옵션은 무엇과의 겹침을 잴지, 얼마나 넉넉하게 잴지, 몇 번 끊어서 알릴지를 정합니다.
| 옵션 | 정하는 것 | 안 주면 |
|---|---|---|
root |
겹침을 잴 기준 상자 | 뷰포트 |
rootMargin |
기준 상자를 넓히거나 좁히는 여백 | 여백 없음 |
threshold |
알림을 보낼 겹친 몫 | 0 |
root 에 스크롤되는 상자 요소를 주면 그 상자 안에서의 겹침을 잽니다. 채팅창처럼 페이지 안에 따로 스크롤되는 영역이 있을 때 씁니다. 그 상자는 지켜볼 요소를 바깥에서 감싸는 요소여야 합니다.
rootMargin 은 "200px" 이나 "0px 0px 200px 0px" 같은 문자열로 적습니다. 값이 하나면 사방에 같은 여백이 붙습니다. 값이 넷이면 위 · 오른쪽 · 아래 · 왼쪽 차례로 붙습니다. 이 꼴은 CSS(Cascading Style Sheets, 웹 페이지의 모양을 정하는 언어)에서 여백을 적는 꼴과 같습니다.
"200px" 을 주면 기준 상자가 사방으로 200픽셀 커집니다. 그러면 요소가 화면에 닿기 200픽셀 전에 알림이 옵니다. 이미지를 미리 받아 두면 사용자가 빈칸을 덜 봅니다.
threshold 는 알림을 보낼 문턱입니다. 겹친 몫이 이 값을 넘거나 밑돌 때 알림이 옵니다. 숫자 하나 대신 배열을 주면 문턱마다 알림이 옵니다.
new IntersectionObserver(onSee, {
threshold: [0, 0.5, 1],
});
이렇게 주면 겹치기 시작할 때, 절반을 넘을 때, 전부 들어올 때 알림이 옵니다. 요소가 화면 밖으로 빠져나갈 때도 같은 문턱에서 한 번씩 옵니다.
알림이 오는 때
브라우저는 겹침을 매 순간 재지 않습니다. 브라우저는 화면을 한 장씩 새로 그려 내보냅니다. 이 그리기는 1초에도 수십 번, 눈이 끊김을 못 느낄 만큼 잦게 돕니다.
겹침은 이 그리기에 맞춰 확인합니다. 바뀐 것이 있으면 기록을 모아 두었다가 콜백을 부릅니다.
그래서 알림은 요소가 경계를 넘은 순간보다 조금 늦게 옵니다. 다음 확인 때까지 기다리기 때문입니다. 콜백 자체는 메인 스레드에서 돕니다. 콜백 안에서 오래 걸리는 일을 하면 옛 방식처럼 화면이 멈칫합니다.
요소가 두 번의 확인 사이에 들어왔다가 나가면 알림이 아예 안 올 수 있습니다. 브라우저는 확인하는 순간의 상태만 보기 때문입니다. 아주 작은 요소를 빠르게 스크롤해 지나칠 때 이런 일이 생깁니다.
겹침은 보임과 다르다
겹친다는 것은 두 사각형의 위치가 겹친다는 뜻일 뿐입니다. 요소 위에 다른 요소가 덮여 있어도 알림은 「겹쳐 있다」로 옵니다. 투명하게 처리돼 눈에 안 보이는 요소도 마찬가지입니다.
그래서 사람 눈에 띄었는지를 엄밀하게 따지는 일에는 이것만으로 모자랍니다. 이 기능이 답하는 물음은 「요소가 기준 상자 안에 들어왔나」 하나입니다.
이미지 지연 로딩 예제
지연 로딩은 당장 안 쓰는 것을 쓰일 때까지 미뤄 두는 방식입니다. 이미지에 적용하면 화면에 다가오는 이미지만 받습니다. 첫 화면과 상관없는 전송이 줄어듭니다.
이미지 태그의 src 속성에 주소를 넣으면 브라우저가 곧바로 받기 시작합니다. 그래서 주소를 data-src 라는 다른 속성에 숨겨 둡니다. 알림이 오면 그 주소를 src 로 옮깁니다.
<img data-src="cat.jpg">
페이지 코드와 브라우저가 주고받는 순서는 이렇습니다.
sequenceDiagram
participant 코드 as 페이지 코드
participant 브라우저
코드->>브라우저: 관찰자를 만들고 이미지를 등록
브라우저-->>코드: 첫 알림 · 아직 안 겹침
Note over 브라우저: 사용자가 스크롤한다
브라우저-->>코드: 알림 · 겹치기 시작함
코드->>브라우저: src 에 주소를 넣는다
코드->>브라우저: 등록을 푼다
이 순서를 코드로 옮기면 이렇습니다.
const io = new IntersectionObserver((es) => {
for (const e of es) {
if (!e.isIntersecting) continue;
e.target.src = e.target.dataset.src;
io.unobserve(e.target);
}
}, { rootMargin: "200px" });
document.querySelectorAll("img[data-src]")
.forEach((img) => io.observe(img));
기록마다 겹쳐 있는지부터 봅니다. 등록하자마자 오는 첫 알림에는 아직 안 겹친 이미지도 섞여 있기 때문입니다. dataset.src 는 data-src 속성의 값을 읽는 방법입니다.
겹친 이미지는 주소를 옮겨 받기 시작합니다. 그리고 곧바로 등록을 풉니다. 한 번 받은 이미지를 계속 지켜볼 까닭이 없어서입니다. rootMargin 을 200픽셀로 둔 것은 화면에 닿기 조금 전에 받기 시작하려는 것입니다.
이미지를 미루기만 하면 되는 경우에는 이미지 태그에 loading="lazy" 속성을 다는 방법도 있습니다. 이때는 언제 받을지를 브라우저가 정합니다. 받는 때를 코드가 직접 정하고 싶을 때 Intersection Observer 를 씁니다.
목록 끝을 지켜보는 무한 스크롤
무한 스크롤은 목록 끝에 다다르면 다음 묶음을 불러 이어 붙이는 화면입니다. 목록 맨 아래에 빈 요소 하나를 두고 이 요소만 지켜봅니다. 빈 요소가 화면에 들어오면 다음 묶음을 서버에 청합니다.
서버 쪽에서는 이 요청이 「몇 번째부터 몇 개」를 묻는 페이지네이션 요청으로 들어옵니다. 사용자가 빠르게 내리면 요청도 짧은 간격으로 잇달아 옵니다.
쓸 때와 안 쓸 때
이 기능이 잘 맞는 일은 「경계를 넘는 순간」 한 번만 알면 되는 일입니다. 매 순간의 위치가 필요하거나 실제로 보였는지까지 따져야 하면 안 맞습니다.
| 하려는 일 | 맞나 | 까닭 |
|---|---|---|
| 화면에 다가오는 이미지만 받기 | ✓ | 경계를 넘을 때 한 번 알면 된다 |
| 목록 끝에서 다음 묶음 부르기 | ✓ | 바닥의 빈 요소 하나만 지켜보면 된다 |
| 요소가 기준 상자에 절반 이상 들어왔는지 세기 | ✓ | 문턱을 0.5 로 두면 된다 |
| 스크롤 양에 맞춰 매 순간 움직이는 효과 | ✗ | 문턱을 넘을 때만 알려 준다 |
| 다른 요소에 가려졌는지 따지기 | ✗ | 사각형의 겹침만 본다 |
관련 항목
Intersection Observer 가 속하는 상위 분류
Web API · 브라우저 · 자바스크립트 · DOM · 프론트엔드
Intersection Observer 와 같은 꼴로 만드는 브라우저 관찰자
ResizeObserver · MutationObserver · PerformanceObserver · 옵저버 패턴 · 콜백
Intersection Observer 가 겹침을 잴 때 쓰는 기준 상자와 계산
뷰포트 · 스크롤 컨테이너 · 바운딩 박스 · CSS · 레이아웃 · IntersectionObserverEntry
Intersection Observer 로 만드는 화면 기법
지연 로딩 · 무한 스크롤 · 페이지네이션 · 코드 분할 · 이미지 최적화 · 스크롤 애니메이션
Intersection Observer 가 대신하는 옛 방식
스크롤 이벤트 · getBoundingClientRect · 디바운스 · 스로틀 · requestAnimationFrame
Intersection Observer 가 덜어 주는 성능 문제
다른 이름: IntersectionObserver · Intersection Observer API · 인터섹션 옵저버 · 교차 관찰자