Intersection Observer
Deep dive into intersection observer
1. Intersection Observer π©βπ»
μμ£Ό μ¬μ©νμ§λ§ entry.intersectionRatio
μ μ¬μ©ν μ§, threshold
λ₯Ό μ¬μ©ν μ§ νμ κ³ λ―Όμ΄ λ§μ΄ λλ κ² κ°μ μ 리ν΄λ³΄κ³ μ νλ€.
μ°μ Intersection Observer λ μΉ λΈλΌμ°μ κ° μ 곡νλ Intersection Observer API
μ
μν΄ μΉ μμμ μλνλ API λ‘, DOM μ΄ λΈλΌμ°μ μ Viewport μ 보μ΄λμ§ κ°μμ±μ κ΄μ°°ν΄, μ¬μ©μκ° μ μν ν¨μλ₯Ό μ€νμμΌμ€λ€.
API μ¬μ© λ°©μμ JavaScript Classes κΈ°λ°μΌλ‘, λ€μκ³Ό κ°μ΄ μΈμ€ν΄μ€λ₯Ό μμ±νκ³ ,
new IntersectionObserver(callback)
new IntersectionObserver(callback, options)
μΈμ€ν΄μ€ λ©μλλ₯Ό μ¬μ©ν΄ κ΄μ°°ν DOM νκ²μ arguments λ‘ λ겨 Observer Events λ₯Ό λ±λ‘μν¨λ€.
const io = new IntersectionObserver(callback, options)
const yellowBoxEl = document.querySelector(".box--yellow")
io.observe(yellowBoxEl)
μ λ¬ λ°λ callback
parameters λ Observer Patterns μ λ±λ‘ν ν¨μλ‘, νλμ μ΅μ λ² μΈμ€ν΄μ€κ° μ¬λ¬ λμμ
Observing ν μ μκΈ° λλ¬Έμ ν΄λΉ μΈμ€ν΄μ€κ° observe
λ©μλλ₯Ό μ¬μ©ν΄ λ±λ‘ν λͺ¨λ DOM νκ²μ Array Parameters λ‘
λ°λ ν¨μλ₯Ό μ¬μ©ν΄μΌνλ€. ν¨μμ μλ₯Ό λ€λ©΄ λ€μκ³Ό κ°λ€.
const callbackFn = (entries) => {
entries.forEach((entry) => {
entry.intersectionRatio > 0
? entry.target.classList.add("show")
: entry.target.classList.remove("show");
});
}
λ¬Όλ‘ , λλΆλΆμ μμ λ μΈμ€ν΄μ€ μμ±κ³Ό λ©μλ attachment λ§ κ΅¬λΆν΄ μ€λͺ νκ³ μμ΄ λ€μκ³Ό κ°μ ννκ° μ΅μν κ²μ΄λ€.
const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
entry.intersectionRatio > 0
? entry.target.classList.add("show")
: entry.target.classList.remove("show");
});
})
const boxEls = document.querySelectorAll(".box")
boxEls.forEach((el) => io.observe(el))
2. Instance Properties and Methods π©βπ»
1. Instance Properties
IntersectionObserver μ μΈμ€ν΄μ€ properties λ root
, rootMargin
, thresholds
3κ°κ° μλ€.
1 ) root
root
λ μ μ¬μ©λμ§ μμΌλ 무μνμ.
2 ) rootMargin
CSS μ margin
μ κ°λ
κ³Ό λμΌνλ€. CSS μμ content-box μ ν¬κΈ°λ padding
μ μμ‘΄νκ³ , margin
μ
λ€λ₯Έ μ리먼νΈμμ 거리λ₯Ό μ‘°μ νκΈ° μν΄ μ¬μ©νμ§λ§, Intersection Observer λ
rootMargin μ κ΄μ λμμ μμμΌλ‘ μ¬μ©νλ€.
μμμ μ¬μ©ν entry.intersectionRatio > 0
λ κ΄μ°°μκ° λ³΄μ΄κΈ° μμνλ μ¦μ true
κ° λλ€. λ§μ½ 20% μ΄μ λ³΄μΌ λ
true
κ° λκ² νλ €λ©΄ entry.intersectionRatio >= 20
μ μ€ μ μλ€.
μ λ°©λ² λμ rootMargin
μ μ¬μ©νλ©΄ μ’ λ μ§κ΄μ μΌλ‘ κ΄μ°° μμμ νλνκ±°λ μΆμν μ μλ€.
- rootMargin κΈ°λ³Έκ°:
0px 0px 0px 0px
λ₯Ό κΈ°λ³Έκ°μΌλ‘ κ°λλ€. - rootMargin μμκ°: νκ²μ margin μμ λ§νΌ νμ₯ν΄ κ°μνλ€.
- viewport μ λ€μ΄μ¬ λ: λμ΄λ margin μμμΌλ‘ μΈν΄ μ€μ viewport μ 보μ΄κΈ° μ intersection μ΄
true
κ° λλ€. - viewport μμ λκ° λ: λμ΄λ margin μμμΌλ‘ μΈν΄ μ€μ viewport μμ μ보μ΄κ³ λμλ μμμ μμ ν λ²μ΄λμΌ
intersection μ΄
false
κ° λλ€.
- viewport μ λ€μ΄μ¬ λ: λμ΄λ margin μμμΌλ‘ μΈν΄ μ€μ viewport μ 보μ΄κΈ° μ intersection μ΄
- rootMargin μμκ°: νκ²μ margin μμ λ§νΌ μΆμν΄ κ°μνλ€.
%
λ λ¬Όλ‘ ,px
λ¨μλ₯Ό μ¬μ©ν μλ μκΈ° λλ¬Έμentry.intersectionRatio
λ³΄λ€ λ μΈλ°ν κ΄μ°°μ΄ κ°λ₯νλ€.- viewport μ λ€μ΄μ¬ λ: μ€μ΄λ margin μμμΌλ‘ μΈν΄ μ€μ viewport μ μ€μ΄λ margin λ³΄λ€ λ λ§μ΄ 보μ¬μΌ
intersection μ΄
true
κ° λλ€. - viewport μμ λκ° λ: μ€μ΄λ margin μμμΌλ‘ μΈν΄ μ€μ viewport μ μμ§ λ³΄μ΄λλΌλ μ€μ΄λ margin λ³΄λ€ μ κ² λ³΄μ΄λ©΄
intersection μ΄
false
κ° λλ€.
- viewport μ λ€μ΄μ¬ λ: μ€μ΄λ margin μμμΌλ‘ μΈν΄ μ€μ viewport μ μ€μ΄λ margin λ³΄λ€ λ λ§μ΄ 보μ¬μΌ
intersection μ΄
3 ) thresholds
μ rootMargin
μ²λΌ κ΄μ°° μμμ νμ₯νλ κ²μ ν μ μλ€. λ€λ§, entry.intersectionRatio > 0
,
entry.intersectionRatio > 20
, entry.intersectionRatio > 40
μ΄λ°μμΌλ‘ μ¬μ©νλ λμ
threshold
λ₯Ό μ¬μ©νλ©΄ callback ν¨μλ₯Ό entry.isIntersecting
λ§μΌλ‘ μμ±ν μ μμ΄ μ¬μ¬μ©μ±μ λμ¬μ€λ€.
const callbackFn = (entries) => {
entries.forEach((entry) => {
entry.isIntersecting
? entry.target.classList.add("show")
: entry.target.classList.remove("show");
});
}
new IntersectionObserver(callback) // 0
new IntersectionObserver(callback, { threshold: 0.2 }) // 20%
new IntersectionObserver(callback, { threshold: 0.4 }) // 40%
λ°λΌμ κ΄μ°° μμμ νμ₯νλ κ²μ
rootMargin
λ₯Ό μ¬μ©νκ³ , κ΄μ°° μμμ΄ μΌμ λΉμ¨ μ΄μ λ³΄μΌ λλthreshold
λ₯Ό μ¬μ©νλ κ²μ΄ μ’λ€.μ°Έκ³ λ‘ μΌμ λΉμ¨μ΄ κ΄μ°°λ λ μλνλ νΈλ¦¬κ±°λ λ€μ μΈ κ°μ§ λ°©λ² μ€ μ΄λ€ κ²μ μ¬μ©ν΄λ λμΌνλ€.
entry.intersectionRatio > 20
entry.isIntersecting
&&rootMargin: "20%"
entry.isIntersecting
&&{ threshold: 0.2 }
2. Instance Methods
observe(target:)
: ν΄λΉ μΈμ€ν΄μ€μ νλμ κ°μν λμ λ°°μ΄μ μΆκ°νλ€.unobserve(target:)
: ν΄λΉ μΈμ€ν΄μ€κ° κ°μμ€μΈ λμ μ€ νλλ₯Ό λ°°μ΄μμ μ κ±°νλ€.disconnect()
: ν΄λΉ μΈμ€ν΄μ€κ° κ°μμ€μΈ λͺ¨λ κ΄μ°° λμμ μ κ±°νλ€.
3. Observing Directions π©βπ»
1. Formula
Infinite Scroll
μ²λΌ κ΄μ°°μ ν λ²λ§ ν΄λ λλ κ²½μ°λ
const callbackFn = (entries) => {
entries.forEach((entry) => {
if(entry.isIntersecting) {
// fetch in here
io.unobserve(entry)
}
});
}
μ κ°μ΄ κ΄μ°°μ μ±κ³΅νλ©΄ ν¨μλ₯Ό μ€νμν€λ©° ν΄λΉ κ΄μ°° λμμ μ κ±°ν΄μ£Όλ©΄ λλ€. κΈ°λ₯κ³Ό μ±λ₯ λ©΄μμ μ κ±°ν΄μ£Όλ κ²μ΄ κ°μ₯ μ’λ€.
νμ§λ§ μ λλ©μ΄μ
μ€νμΌ μ μ©μ μν΄ μ¬μ©ν κ²½μ°, μλ°©ν₯μ΄ μλ μμμ μλ, λλ μλμ μλ‘ κ°λ λ°©ν₯μμ κ΄μ°°λ λλ§
μλνλλ‘ ν΄μΌ νλ κ²½μ°κ° μλ€. νμ§λ§ entry.intersectionRatio
, rootMargin
, threshold
λ λ°©ν₯μ κ΄κ³ μμ΄
κ΄μ°°νκΈ° λλ¬Έμ μ¬μ©ν μκ° μλ€.
μλλ‘ λ΄λ €κ°λ λ°©ν₯μμλ§ μλνλλ‘ ν΄μΌνλ Observer κ° μλ€κ³ ν΄λ³΄μ. rootMargin
μ μ¬μ©ν΄ 0px 0px 9999px 0
κ³Ό κ°μ΄
μ£Όλ©΄ μνλλλ‘ μλνκΈ΄ ν κ²μ΄λ€. νμ§λ§ μ΄κ³ ν΄μλ λͺ¨λν°λ λ§μμ‘κ³ , λͺ¨λν°λ₯Ό μΈλ‘λ‘ λκ³ λ³΄κ±°λ νλ/μΆμλ₯Ό νκΈ°λ νλλ° μ΄λ°
λͺ¨λ μν©μ κ³ λ €νλ©΄ μλ²½ν λ°©λ²μ΄λΌ ν μλ μλ€.
νμ§λ§ entry.boundingClientRect.top
λ₯Ό κ΄μ°°νλλ‘ νλ©΄, λ¨μ§ 보μ΄κΈ° μμνκ±°λ μ¬λΌμ§ λκ° μλ, μλ¨μ΄ 보μ΄κΈ° μμνκ±°λ
μ¬λΌμ§ λ, νλ¨μ΄ 보μ΄κΈ° μμνκ±°λ μ¬λΌμ§ λλ₯Ό ꡬλΆν μ μκ² λλ€.
Β | entry.boundingClientRect.top |
entry.isIntersecting |
---|---|---|
λ΄λ €κ°λ©° 보μ΄κΈ° μμν λ | μμ | true |
λ΄λΌκ°λ©° μ¬λΌμ§κΈ° μμν λ | μμ | false |
μ¬λΌκ°λ©° 보μ΄κΈ° μμν λ | μμ | true |
μ¬λΌκ°λ©° μ¬λΌμ§κΈ° μμν λ | μμ | false |
μ°Έκ³ λ‘ μ 곡μμ
rootMargin
κ°μ μ£ΌλλΌλ λ³νμ§ μλλ€. λ°λΌμ μλ°©ν₯, λ΄λ €κ° λ, μ¬λΌκ° λ λͺ¨λμ λν μ΄λ²€νΈ 쑰건μΌλ‘ μ¬μ©ν μ μλ€.
μ 곡μμ μ¬μ©ν΄ μλλ‘ λ΄λ €κ° λλ§ show
λ₯Ό μΆκ°νκ³ , λ΄λ €κ°λ©° μ¬λΌμ§ λλ κ·Έλλ‘ μ μ§, μλ‘ μ¬λΌκ°λ©° μ¬λΌμ§ λ
show
λ₯Ό μ κ±°νλ μ΅μ λ² λ λ€μκ³Ό κ°μ΄ μ μν μ μλ€.
const observerDownward = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const topIsIntersecting = entry.boundingClientRect.top >= 0;
if (topIsIntersecting) {
entry.isIntersecting
? entry.target.classList.add("show")
: entry.target.classList.remove("show");
}
});
},
{
threshold: 0.2,
},
);
observerDownward
λ 3κ°μ§ 쑰건 β μλ¨μ΄ 보μ΄κ³ , β‘ νκ²μ΄ κ΄μ°°λλλ° β’ 20% μ΄μμΌ λλ₯Ό λͺ¨λ λ§μ‘±ν λ show
λ₯Ό μΆκ°νλ€.
λ°λΌμ λ΄λ €κ°λ©° 20% μ΄μ 보μ΄κΈ° μμν λλ show
λ₯Ό μΆκ°νμ§λ§, λ΄λ €κ°λ©° μ¬λΌμ§ λλ
β μλ¨μ΄ 보μ΄κ³ κ° false
μ΄κΈ° λλ¬Έμ μ무 κ²λ νμ§ μλλ€.
κ·Έλ¦¬κ³ μλ‘ μ¬λΌκ°λ©° μμ ν μ¬λΌμ§ λλ show
λ₯Ό μ κ±°ν΄μΌνλλ°, μ¬λΌκ°λ©° μ¬λΌμ§ λ 20% μ΄νλ‘ λ³΄μ΄κ² λλ©΄,
β μλ¨μ΄ 보μ΄κ³ λ true
μΈλ°, β‘ νκ²μ΄ κ΄μ°°λλλ° β’ 20% μ΄μμΌ λλ₯Όκ°
false
κ° λκΈ° λλ¬Έμ show
λ₯Ό μ κ±°νλ€.
2. Examples
μμμ μ€λͺ ν κ°λ μ VanillaJS λ‘ μ νΈλ‘ λ§λ€λ©΄ λ€μκ³Ό κ°λ€.
- performance.js
// @ts-check
/**
* Apply throttling to a function.
* @param fn {Function} - A function to apply throttling.
* @param delay {number} - milliseconds (default 500)
* @returns {Function} - A function is applied throttling.
*/
export const throttle = (fn, delay = 500) => {
let available = true;
return (...args) => {
if (available) {
available = false;
fn(...args);
setTimeout(() => {
available = true;
}, delay);
}
};
};
/**
* Apply debouncing to a function.
* @param fn {Function} - A function to apply debouncing.
* @param delay {number} - milliseconds (default 500)
* @returns {Function} - A function is applied debouncing.
*/
export const debounce = (fn, delay = 500) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
};
- observer.js
// @ts-check
/* Intersection Observer */
import { debounce } from "./performance";
const createIObserver = (callback, options = { threshold: 0.2 }) =>
new IntersectionObserver(callback, options);
const twoWayCallback = (entries) =>
entries.forEach((entry) =>
entry.isIntersecting
? entry.target.classList.add("show")
: entry.target.classList.remove("show"),
);
const downwardCallback = (entries) =>
entries.forEach((entry) => {
const topIsIntersecting = entry.boundingClientRect.top >= 0;
if (topIsIntersecting) {
entry.isIntersecting
? entry.target.classList.add("show")
: entry.target.classList.remove("show");
}
});
const upwardCallback = (entries) =>
entries.forEach((entry) => {
const topIsHiding = entry.boundingClientRect.top < 0;
if (topIsHiding) {
entry.isIntersecting
? entry.target.classList.add("show")
: entry.target.classList.remove("show");
}
});
const observer = createIObserver(twoWayCallback);
const observerDownward = createIObserver(downwardCallback);
const observerUpward = createIObserver(upwardCallback);
/* Mutation Observer */
const createMObserver = (callback) => new MutationObserver(callback);
const observerMutations = (callback) => {
const debouncedCallback = debounce(callback);
return createMObserver((mutations) => {
mutations.forEach((mutation) => {
debouncedCallback(mutation);
});
});
};
const mutationConfig = {
attributes: false,
childList: true,
subtree: true,
};
export {
observer,
observerDownward,
observerUpward,
observerMutations,
mutationConfig,
};
μ performance.js
μ observer.js
μΈμλ eventBinding.js
, fp.js
, render.js
,
styleHelper.js
μ κ°μ μ’ λ λ§μ μ νΈμ μ΄κ³³μμ νμΈν μ μλ€.
Reference
- βIntersectionObserver.β MDN Web Docs. Feb. 28, 2023, accessed Mar. 24, 2024, MDN - IntersectionObserver.