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κ°€ λœλ‹€.
  • rootMargin μŒμˆ˜κ°’: νƒ€κ²Ÿμ„ margin μ˜μ—­ 만큼 μΆ•μ†Œν•΄ κ°μ‹œν•œλ‹€. %λŠ” λ¬Όλ‘ , px λ‹¨μœ„λ₯Ό μ‚¬μš©ν•  μˆ˜λ„ 있기 λ•Œλ¬Έμ— entry.intersectionRatio보닀 더 μ„Έλ°€ν•œ 관찰이 κ°€λŠ₯ν•˜λ‹€.
    • viewport 에 λ“€μ–΄μ˜¬ λ•Œ: 쀄어든 margin μ˜μ—­μœΌλ‘œ 인해 μ‹€μ œ viewport 에 쀄어든 margin 보닀 더 많이 보여야 intersection 이 trueκ°€ λœλ‹€.
    • viewport μ—μ„œ λ‚˜κ°ˆ λ•Œ: 쀄어든 margin μ˜μ—­μœΌλ‘œ 인해 μ‹€μ œ viewport 에 아직 보이더라도 쀄어든 margin 보닀 적게 보이면 intersection 이 falseκ°€ λœλ‹€.


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

  1. β€œIntersectionObserver.” MDN Web Docs. Feb. 28, 2023, accessed Mar. 24, 2024, MDN - IntersectionObserver.