๐Ÿ‘€ 30. Intersection Observer API โ€” ์Šคํฌ๋กค ๊ฐ์ง€์˜ ํšจ์œจ์ ์ธ ๋Œ€์•ˆ

JM_Devยท2025๋…„ 5์›” 7์ผ
0
post-thumbnail

ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•  ๋•Œ๊ฐ€ ๋งŽ๋‹ค.

  • ์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ์„น์…˜์— ๋„๋‹ฌํ–ˆ์„ ๋•Œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹คํ–‰
  • ๋ฌดํ•œ ์Šคํฌ๋กค์—์„œ ๋‹ค์Œ ์ฝ˜ํ…์ธ  ์ž๋™ ๋กœ๋”ฉ
  • lazy loading ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ
  • scroll spy ๊ธฐ๋Šฅ ๊ตฌํ˜„

์˜ˆ์ „์—” scroll ์ด๋ฒคํŠธ๋ฅผ ์ง์ ‘ ๊ฐ์ง€ํ•ด์„œ ์ฒ˜๋ฆฌํ–ˆ์ง€๋งŒ,
์ง€๊ธˆ์€ ๋” ์„ฑ๋Šฅ ์ข‹๊ณ , ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•œ API๊ฐ€ ์žˆ๋‹ค.
๋ฐ”๋กœ Intersection Observer API๋‹ค.


โœ… Intersection Observer๋ž€?

์š”์†Œ๊ฐ€ ๋ทฐํฌํŠธ(๋˜๋Š” ํŠน์ • ๋ถ€๋ชจ ์š”์†Œ)์™€ ๊ต์ฐจ๋˜๋Š” ์ง€์ ์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ฐ์ง€ํ•˜๋Š” ๋ธŒ๋ผ์šฐ์ € API

์ฆ‰, ํ™”๋ฉด์— ๋ณด์ด๊ธฐ ์‹œ์ž‘ํ–ˆ๋Š”์ง€ ๊ฐ์ง€ํ•˜๋Š” ๋ฐ ์ตœ์ ํ™”๋œ API๋‹ค.
์Šคํฌ๋กค ์ด๋ฒคํŠธ์™€ ๋‹ฌ๋ฆฌ ์—ฐ์†์ ์ธ ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ ์—†์ด ํšจ์œจ์ ์œผ๋กœ ๊ฐ์ง€ ๊ฐ€๋Šฅํ•˜๋‹ค.


๐Ÿ”ง ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log('์š”์†Œ๊ฐ€ ํ™”๋ฉด์— ๋ณด์ž„');
    }
  });
});

observer.observe(document.querySelector('#target'));

์ฃผ์š” ๊ฐœ๋…

  • entries: ๊ด€์ฐฐ ๋Œ€์ƒ ์š”์†Œ๋“ค์˜ ๊ต์ฐจ ์ƒํƒœ ๋ชฉ๋ก
  • entry.isIntersecting: ์š”์†Œ๊ฐ€ ํ™”๋ฉด ์•ˆ์— ์žˆ๋Š”์ง€ ์—ฌ๋ถ€
  • entry.intersectionRatio: ๋ณด์ด๋Š” ๋น„์œจ (0 ~ 1)

โš™๏ธ ์˜ต์…˜ ์„ค์ •

new IntersectionObserver(callback, {
  root: null, // ๊ด€์ฐฐ ๊ธฐ์ค€ (null์ด๋ฉด viewport)
  rootMargin: '0px 0px -100px 0px', // ์—ฌ๋ฐฑ ์ง€์ •
  threshold: 0.5 // 50% ์ด์ƒ ๋ณด์ผ ๋•Œ ํŠธ๋ฆฌ๊ฑฐ
});

๐Ÿ’ก ์‹ค์ „ ์˜ˆ์ œ: ๋ฌดํ•œ ์Šคํฌ๋กค

const observerRef = useRef<IntersectionObserver | null>(null);
const targetRef = useRef(null);

useEffect(() => {
  observerRef.current = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting) {
      loadMore();
    }
  });

  if (targetRef.current) {
    observerRef.current.observe(targetRef.current);
  }

  return () => {
    observerRef.current?.disconnect();
  };
}, []);

โœ… targetRef์— ๊ฑธ๋ฆฐ ์š”์†Œ๊ฐ€ ํ™”๋ฉด์— ๋ณด์ด๋ฉด loadMore() ํ˜ธ์ถœ๋จ


๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ Lazy Loading ์˜ˆ์‹œ

<img data-src="real-image.jpg" class="lazy" />
const images = document.querySelectorAll('.lazy');
const io = new IntersectionObserver((entries, obs) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.src = entry.target.dataset.src;
      obs.unobserve(entry.target);
    }
  });
});

images.forEach(img => io.observe(img));

โœ… ๋ทฐํฌํŠธ์— ๋“ค์–ด์˜ค๋ฉด ์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์‹œ์ž‘, ์„ฑ๋Šฅ ์ตœ์ ํ™”์— ํƒ์›”


๐Ÿง  ์™œ Intersection Observer๊ฐ€ ์ข‹์€๊ฐ€?

ํ•ญ๋ชฉscroll ์ด๋ฒคํŠธIntersection Observer
CPU ๋ถ€ํ•˜๋†’์Œ (๊ณ„์† ํŠธ๋ฆฌ๊ฑฐ๋จ)๋‚ฎ์Œ (๋น„๋™๊ธฐ ๊ฐ์ง€)
์„ฑ๋Šฅ์Šค๋กœํ‹€๋ง ํ•„์š”๊ธฐ๋ณธ์ ์œผ๋กœ ํšจ์œจ์ 
๊ตฌํ˜„ ๋‚œ์ด๋„๋น„๊ต์  ๋ณต์žกํ•จ๋งค์šฐ ๊ฐ„๋‹จ
์‚ฌ์šฉ์„ฑ๋ชจ๋“  ์š”์†Œ ์ง์ ‘ ์ถ”์  ํ•„์š”Observer 1๊ฐœ๋กœ ์—ฌ๋Ÿฌ ์š”์†Œ ์ถ”์  ๊ฐ€๋Šฅ

๐Ÿ“ ๋‚ด๊ฐ€ ๋А๋‚€ ์ 

์˜ˆ์ „์—” ๋ฌดํ•œ ์Šคํฌ๋กค ๋งŒ๋“ค ๋•Œ scrollTop, offsetHeight ๊ฐ™์€ ๊ณ„์‚ฐ์„
๋งค๋ฒˆ ํ•˜๋ฉด์„œ ์Šค๋กœํ‹€๋ง๋„ ๊ฑธ๊ณ  ๋ณต์žกํ•˜๊ฒŒ ์งฐ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ Intersection Observer๋ฅผ ์•Œ๊ฒŒ ๋œ ํ›„์—๋Š”
์ฝ”๋“œ๋„ ๊ฐ„๋‹จํ•˜๊ณ , ์„ฑ๋Šฅ๋„ ์ข‹๊ณ , ์œ ์ง€๋ณด์ˆ˜๋„ ์‰ฌ์›Œ์กŒ๋‹ค.

์‹ค์ œ๋กœ Next.js์—์„œ ์ด๋ฏธ์ง€ lazy loading ๊ธฐ๋ณธ ์ฒ˜๋ฆฌ๋„ ์ด ๋ฐฉ์‹์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค๋Š” ๊ฑธ ์•Œ๊ณ 
๋”๋”์šฑ ์‹ ๋ขฐ๊ฐ€ ์ƒ๊ฒผ๋‹ค.


๐Ÿ‘€ "์Šคํฌ๋กค ์ถ”์ , ๋” ์ด์ƒ ์ง์ ‘ ํ•˜์ง€ ๋ง์ž. Intersection Observer๊ฐ€ ๋‹ค ํ•ด์ค€๋‹ค."


profile
๊ฐœ๋ฐœ์ž๋กœ ์ทจ์—…์„ ์ค€๋น„ ์ค‘ ์ด๋ฉฐ, ์—ด์‹ฌํžˆ ๊ณต๋ถ€ ์ค‘ ์ž…๋‹ˆ๋‹ค!

0๊ฐœ์˜ ๋Œ“๊ธ€