2019-03-26 16:03 μž‘μ„±λ¨
πŸ’…πŸ» A banner created from here

Contents


  1. πŸ€” IntersectionObserver API λž€?
  2. πŸ˜‡ λΈŒλΌμš°μ € 지원 ν˜„ν™©
  3. πŸ“ƒ μš©μ–΄ μ„€λͺ…
  4. 🎈 μ‚¬μš© μ˜ˆμ‹œ
  5. πŸ‘ 마치며

πŸ€” IntersectionObserver API λž€?


MDN λ¬Έμ„œμ—μ„œλŠ” μ΄λ ‡κ²Œ μ„€λͺ…ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.

(μ˜μ—­)
κ΄€μ°°ν•˜κ³ μž ν•˜λŠ” μ—˜λ¦¬λ¨ΌνŠΈμ™€ 쑰상 μ—˜λ¦¬λ¨ΌνŠΈ λ˜λŠ” viewport κ°„μ˜
ꡐ차(Intersection)의 λ³€ν™”λ₯Ό λΉ„λ™κΈ°μ μœΌλ‘œ κ°μ§€ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€

음... 무슨 μ–˜κΈ°μΈμ§€ λͺ¨λ₯΄κ² λ‹€κ΅¬μš”? μ €λ‘μš”

ꡐ차의 λ³€ν™”λ₯Ό κ°μ§€ν•œλ‹€λŠ” 것은 λ‹¨νŽΈμ μœΌλ‘œ μ„€λͺ…ν•˜μžλ©΄
μ—˜λ¦¬λ¨ΌνŠΈμ˜ visibility (κ°€μ‹œμ„±) 에 κ΄€ν•œ λ³€ν™”λ₯Ό κ°μ§€ν•œλ‹€λŠ” 의미 μž…λ‹ˆλ‹€

Intersection Observer APIλŠ” μ—˜λ¦¬λ¨ΌνŠΈκ°€ viewport μ—μ„œ
λ˜λŠ” λ‹€λ₯Έ μ—˜λ¦¬λ¨ΌνŠΈμ™€μ˜ κ΄€κ³„μ—μ„œ "λ³΄μ΄λŠ”μ§€ μ•ˆλ³΄μ΄λŠ”μ§€"
λ₯Ό μ‰½κ²Œ μ•Œ 수 μžˆλ„λ‘ κΈ°λŠ₯을 μ œκ³΅ν•˜λŠ” 역할을 ν•©λ‹ˆλ‹€

APIλŠ” μ•„λž˜μ™€ 같은 보편적인 κ²½μš°μ— μ‚¬μš©λ©λ‹ˆλ‹€

  1. μ΄λ―Έμ§€μ˜ Lazy Loading
  2. "Infinite Scrolling" 이라고 ν•˜λŠ” 비동기 λ¬΄ν•œ 슀크둀 UI
  3. κ΄‘κ³  맀좜 츑정을 μœ„ν•΄ λ°°λ„ˆ λ˜λŠ” κ΄‘κ³ κ°€ λ…ΈμΆœ λ˜μ—ˆλŠ”μ§€ 확인
  4. μ• λ‹ˆλ©”μ΄μ…˜μ„ μž‘μ—…μ„ μˆ˜ν–‰ν•  지λ₯Ό λ…ΈμΆœ μ—¬λΆ€λ₯Ό μ‚¬μš©ν•˜μ—¬ κ²°μ •

이전에, .getBoundingClientRect() 와 window.innerHeight λ₯Ό μ‚¬μš©ν•΄
어렡사리 κ°€μ‹œμ„±μ— λŒ€ν•œ 정보λ₯Ό μ–»μ—ˆλ‹€λ©΄
Intersection Observer API λ₯Ό μ‚¬μš©ν•΄ 쑰금 더 νŽΈν•˜κ²Œ μž‘μ—…ν•  수 μžˆμŠ΅λ‹ˆλ‹€

πŸ˜‡ λΈŒλΌμš°μ € 지원 ν˜„ν™©


μ£Όμš” λΈŒλΌμš°μ €μ˜ 지원 ν˜„ν™©μž…λ‹ˆλ‹€
Google Chrome: 51^
Microsoft Edge: 15^
Firefox: 55^
IE: No Support (뭘 κΈ°λŒ€ν•˜μ‹ κ±°μ£ ?)
Opera: No Support
Safari: No Support

Chrome κ³Ό Safari λͺ¨λ‘ Webkit 기반 λΈŒλΌμš°μ €μΈλ° Chrome 만 μ§€μ›ν•˜λŠ”κ²Œ μ’€ ν₯λ―Έλ‘œμ› μŠ΅λ‹ˆλ‹€
λ―Έ 지원 λΈŒλΌμš°μ €λ₯Ό μœ„ν•œ polyfill 이 μžˆμ§€λ§Œ, μ™„λ²½ν•˜μ§€λŠ” μ•ŠμŠ΅λ‹ˆλ‹€
(w3c Github 에 폴리필이 μžˆμŠ΅λ‹ˆλ‹€. 링크)

그리고 λ‹Ήμ—°ν•˜μ§€λ§Œ, IE μ—μ„œλŠ” 지원 λ˜μ§€ μ•Šκ³ , 지원 κ³„νšλ„ μ—†μŠ΅λ‹ˆλ‹€
(죽이고 싢은 IE μœ μ €μ™€μ˜ 10μ„ )

polyfill 을 μ‚¬μš©ν•˜λ©΄ λ˜λŠ”κ²Œ μ•„λ‹ˆλƒκ΅¬μš”?
λΈŒλΌμš°μ €λ§ˆλ‹€ λ™μž‘μ΄ λ‹€λ¦…λ‹ˆλ‹€... (상상도 λͺ»ν•œ λ™μž‘ γ„΄(Β°0Β°)γ„±)

이 νŽ˜μ΄μ§€λ₯Ό μ˜€νŽ˜λΌλ‚˜ μ‚¬νŒŒλ¦¬, 파폭으둜
ν™•μΈν•΄λ³΄μ‹œλ©΄ 폴리필을 적용 ν–ˆμŒμ—λ„ λΆˆκ΅¬ν•˜κ³  λ―Έλ¬˜ν•˜κ²Œ λ™μž‘μ΄ λ‹€λ¦…λ‹ˆλ‹€
(이미지가 μ•ˆ 보이면 μ›λž˜ 사라져야 ν•˜λŠ”λ° μ•ˆ μ‚¬λΌμ§„λ‹€κ±°λ‚˜...)

IntersectionObserver API 의 개발 진행 상황은 이 κ³³ μ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€
빨리 μ—¬λŸ¬ λΈŒλΌμš°μ €μ—μ„œ μ‚¬μš© κ°€λŠ₯ν–ˆμœΌλ©΄ μ’‹κ² λ„€μš” (IE λΉΌκ³  πŸ€ͺ)

πŸ“ƒ μš©μ–΄ μ„€λͺ…


Intersection Observer API λ₯Ό μ΄ν•΄ν•˜κΈ° μœ„ν•΄ λͺ‡ 가지 κ΄€λ ¨ μš©μ–΄λ₯Ό μ •λ¦¬ν•©λ‹ˆλ‹€

객체 생성과 callback, root, rootMargin, threshold


Observer 객체의 생성은 λ‹€μŒκ³Ό 같이 ν•©λ‹ˆλ‹€

const io = new IntersectionObserver(callback, {
  root: null, // λ˜λŠ” scrollable ν•œ element
  rootMargin: '0px', // μ§€μ •ν•˜μ§€ μ•ŠμœΌλ©΄ 기본값은 0px μž…λ‹ˆλ‹€,
  threshold: 0.5 // 0.0 λΆ€ν„° 1.0 μ‚¬μ΄μ˜ 숫자λ₯Ό 지정할 수 μžˆμŠ΅λ‹ˆλ‹€. λ°°μ—΄ 값도 κ°€λŠ₯ν•©λ‹ˆλ‹€
})

1. root


root 의 값은 Element λ˜λŠ” null μž…λ‹ˆλ‹€
target (κ΄€μΈ‘ λŒ€μƒ) 을 κ°μ‹ΈλŠ” element λ₯Ό μ§€μ •ν•œλ‹€κ³  ν•  수 μžˆμŠ΅λ‹ˆλ‹€
λ§Œμ•½ null 둜 μ§€μ •ν•œλ‹€λ©΄ viewport κ°€ λ©λ‹ˆλ‹€

λ¬Έμ„œ λ‚΄μ˜ scrollable ν•œ μš”μ†Œκ°€ 있고 <div class="scroll"></div>
κ΄€μΈ‘ ν•˜κ³ μž ν•˜λŠ” element κ°€ κ·Έ μ•ˆμ— μžˆλ‹€κ³  κ°€μ •ν•˜λ©΄ <img class="lazy-loaded-image">
root 의 값은 document.querySelector('.scroll') 이 λ©λ‹ˆλ‹€

2. rootMargin


root μš”μ†Œλ₯Ό κ°μ‹ΈλŠ” margin 값을 μ§€μ •ν•©λ‹ˆλ‹€
λ¬Έμžμ—΄λ‘œ μž‘μ„±ν•΄μ•Ό ν•˜λ©°, css의 margin 처럼 px λ˜λŠ” % λ‹¨μœ„λ‘œ μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€
threshold λ‚˜ ꡐ차 μƒνƒœλ₯Ό 점검할 λ•Œ margin 값이 지정 λ˜μ–΄ μžˆλ‹€λ©΄, 이λ₯Ό ν™œμš©ν•˜μ—¬ κ³„μ‚°ν•©λ‹ˆλ‹€

3. threshold


target element κ°€ root 와 λͺ‡ % κ΅μ°¨ν–ˆμ„ λ•Œ, callback 을 싀행할지 κ²°μ •ν•˜λŠ” κ°’ μž…λ‹ˆλ‹€
값은 float κ°’μœΌλ‘œ 된 단일값 λ˜λŠ” 배열값을 μž…λ ₯ν•  수 있고
0.0 λΆ€ν„° 1.0 κΉŒμ§€μ˜ 뢀동 μ†Œμˆ˜μ  값을 μž…λ ₯ν•©λ‹ˆλ‹€

예λ₯Ό λ“€μ–΄, 슀크둀 ν•˜λŠ” 쀑에 element κ°€ 50% λ³΄μ˜€μ„ λ•Œ μ‹€ν–‰ν•  callback 이 μžˆλ‹€κ³  ν•˜λ©΄
threshold 값은 0.5 κ°€ λ©λ‹ˆλ‹€

λ˜λŠ”, element κ°€ λ³΄μ΄λŠ” 것을 μ—¬λŸ¬ κ΅¬κ°„μ—μ„œ callback 을 μ‹€ν–‰ν•˜κ³  μ‹Άλ‹€λ©΄
λ°°μ—΄λ‘œ μž‘μ„±ν•˜λ©΄ λ©λ‹ˆλ‹€

element 의 λ…ΈμΆœλ„ 10% λ‹Ή λ¬΄μ–Έκ°€μ˜ λ™μž‘μ„ ν•˜κ³  μ‹Άλ‹€λ©΄, threshold λŠ”
[0.0, 0.1, 0.2, 0.3, ..., 0.9, 1.0] 이 되겠죠!

4. callback


callback 은 target element 의 visibility κ°€ threshold 만큼 λ„λ‹¬ν–ˆμ„ λ•Œ 호좜될 ν•¨μˆ˜ μž…λ‹ˆλ‹€
Element 배열인 entries와 자기 μžμ‹ μΈ observer κ°€ 맀개 λ³€μˆ˜λ‘œ 전달 λ©λ‹ˆλ‹€

이λ₯Όν…Œλ©΄ μ΄λ ‡μŠ΅λ‹ˆλ‹€

new IntersectionObserver((entries: Element[], observer: IntersectionObserver) => { /* ... */ }, {})

entries λŠ” target 으둜 μ§€μ •ν•œ DOM element κ°μ²΄μž…λ‹ˆλ‹€.
보톡은 callback ν•¨μˆ˜λŠ” μ‹€μ œ μ—­μΉ˜ (threshold) 에 도달 ν–ˆμ„ λ•Œ μ‹€ν–‰ν•  λ™μž‘μ„ μž‘μ„±ν•©λ‹ˆλ‹€!

🎈 μ‚¬μš© μ˜ˆμ‹œ


μ‚¬μš© μ˜ˆμ‹œλŠ” μœ„μ— MDN μ—μ„œ μ œμ‹œν•œ 것 처럼
Lazy loading, Infinite Scrolling 등에 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€

λ‘˜ λ‹€ ν•  수 μžˆλ‹€κ³  ν•˜λ‹ˆκΉŒ λ‘˜ λ‹€ λ§Œλ“€μ–΄ λ³ΌκΉŒμš”?

Lazy Loading


λ¨Όμ € κΈ€κ³Ό 이미지가 있고, 슀크둀이 ν•„μš”ν•œ HTML λ¬Έμ„œκ°€ μžˆλ‹€κ³  κ°€μ •ν•˜κ² μŠ΅λ‹ˆλ‹€

<head>
<style>
  div.image-box {
    display: block;
    margin: 20px auto 0;
    width: 200px;
    height: 200px;
    background-color: #ACACAC;
    color: #FFFFFF;
    text-align: center;
    line-height: 200px;
    font-size: 24px;
  }
</style>
</head>
<body>
  <div class="image-box" data-src="https://source.unsplash.com/random/200x200">No Image</div>
</body>

μœ„ μ΄λ―Έμ§€μ˜ κ°€μ‹œμ„± (Visibility) κ°€ 80% 에 λ„λ‹¬ν–ˆμ„ λ•Œ, 이미지λ₯Ό μ‹€μ œλ‘œ λΆˆλŸ¬μ™€ 보도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€
이미지가 λ‘œλ”©λ˜μ§€ μ•Šμ€ λ°•μŠ€λŠ”, νšŒμƒ‰ 배경에 No Image 둜 좜λ ₯λ©λ‹ˆλ‹€

μ•„λž˜μ™€ 같이 IntersectionObserver λ₯Ό λ§Œλ“­λ‹ˆλ‹€

const images = document.querySelectorAll('.image-box');
const threshold = 0.8;
const lazyLoadOption = { root: null, threshold };
const lazyLoadImage = (entries, observer) => {
  entries.forEach(entry => {
    const { target } = entry;
    if (entry.isIntersecting) {
      target.style.backgroundImage = `url("${target.dataset.src}")`;
      target.style.backgroundSize = `cover`;
      target.style.backgroundColor = `transparent`;
      target.textContent = '';
    }
  })
};
const lazyLoadingIO = new IntersectionObserver(lazyLoadImage, lazyLoadOption);
images.forEach(image => lazyLoadingIO.observe(image));

κ°€μ‹œμ„± 80% 일 λ•Œ, 이미지λ₯Ό λΆˆλŸ¬μ™€μ•Ό ν•˜λ―€λ‘œ 2번째 라인 처럼 threshold = 0.8 μž…λ‹ˆλ‹€

entry.isIntersecting ν”„λ‘œνΌν‹°λŠ” κ΄€μ°°ν•˜κ³ μž ν•˜λŠ” DOM 객체가
μ„€μ •ν•œ root 와 threshold 만큼 κ΅μ°¨ν–ˆλŠ”μ§€μ— λŒ€ν•œ true/false κ°’ μž…λ‹ˆλ‹€
(κ΄€μ°°ν•˜λ €λŠ” DOM 의 κ°€μ‹œμ„±μ΄ threshold λ₯Ό μ΄ˆκ³Όν•˜λ©΄ true, μ•„λ‹ˆλ©΄ false μž…λ‹ˆλ‹€)

κΈ°μ‘΄ div.box-image μ—˜λ¦¬λ¨ΌνŠΈμ˜ λ°°κ²½κ³Ό κΈ€μžλ₯Ό μ§€μš°κ³  background-image λ₯Ό μΆ”κ°€ν•˜λ©΄
μ•„λž˜μ™€ 같이 잘 λ™μž‘ ν•©λ‹ˆλ‹€!

μ½”λ“œλŠ” Gist μ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€!

ezgif.com-optimize.gif

Infinite Scrolling

(Native Infinite Scrolling with the IntersectionObserver API λ₯Ό 일뢀 μ°Έκ³ ν–ˆμŠ΅λ‹ˆλ‹€)

Infinite Scrolling 을 κ΅¬ν˜„ν•˜λ €λ©΄ DOM μš”μ†Œμ˜ scrollTop, scrollHeight λ“±μ˜ κ°’κ³Ό
'scroll' μ΄λ²€νŠΈλ„ λ“±λ‘ν•˜κ³ , 슀크둀이 λκΉŒμ§€ λ‚΄λ €κ°”λ‚˜ μ²΄ν¬ν•˜κ³ ... λ“± ν•  일이 λ§ŽμŠ΅λ‹ˆλ‹€ πŸ™

Intersection Observer λ₯Ό μ“°λ©΄ κ°„κ²°ν•˜κ²Œ...? μ•„λ¬΄νŠΌ 큰 어렀움 없이 κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€
(λ¬Όλ‘  직접 바닐라 μžλ°”μŠ€ν¬λ¦½νŠΈλ‘œ κ΅¬ν˜„ν• μΌλ„ μ—†μœΌμ‹œκ² μ£  πŸ˜… μ—¬λŸ¬ ν›Œλ₯­ν•œ λΌμ΄λΈŒλŸ¬λ¦¬λ“€μ΄ μžˆμ–΄μš”)

그럼 κ°„λ‹¨ν•˜κ²Œ κ΅¬ν˜„ ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€

μ•„λž˜μ™€ 같은 Layout 이 μžˆμŠ΅λ‹ˆλ‹€

<body>
    <h1 class="title">Obey the cats 🐈</h1>
    <ul class="infinite-container"></ul>
</body>

ul.infinite-container 에 li λ₯Ό κ³„μ†ν•΄μ„œ λ§Œλ“€μ–΄ λ‚΄λŠ” ν˜•νƒœλ₯Ό κ΅¬ν˜„ν•©λ‹ˆλ‹€

const ioOptions = {
  root: null,
  threshold: 1,
};
const io = new IntersectionObserver(handleInfiniteScrolling, ioOptions);

λ¨Όμ € IntersectionObserver 객체λ₯Ό λ§Œλ“€μ–΄ μ€λ‹ˆλ‹€.

슀크둀이 μ™„μ „νžˆ 끝났을 λ•Œ, μƒˆλ‘œμš΄ μ•„μ΄ν…œμ„ λ‘œλ”©ν•΄μ•Ό ν•˜λ―€λ‘œ threshold = 1 μž…λ‹ˆλ‹€.
root λŠ” μš°μ„ μ€ viewport κΈ°μ€€μ΄λ―€λ‘œ null κ°’ μž…λ‹ˆλ‹€
(λ§Œμ•½, λ³„λ„μ˜ μž‘μ€ 슀크둀 μ˜μ—­μ— μ μš©ν•˜μ‹ λ‹€λ©΄ root λ₯Ό 지정해 μ£Όμ‹œλ©΄ λ©λ‹ˆλ‹€!)

그럼 handleInfiniteScrolling ν•¨μˆ˜λ₯Ό κ΅¬ν˜„ν•΄ λ³Όκ²Œμš”

const loadingNewCats = (newCats) => {
  const loadingTemplate = `<li><span>Loading New Cats...</span></li>`;
  return new Promise((resolve, reject) => {
    container.insertAdjacentHTML('beforeend', loadingTemplate);
    setTimeout(() => {
      resolve(newCats);
      container.removeChild(container.lastChild);
    }, 1000)
  })
};

const handleInfiniteScrolling = (entries, observer) => {
  const $last = [...entries].pop();
  if ($last.isIntersecting) {
    loadingNewCats(createNewCats()).then(newCats => {
      container.append(...newCats);
      currentLast = lastChild();
      observer.unobserve($last.target);
      observer.observe(currentLast);
    });
  }
};

(loadingNewCats λŠ” κ·Έλƒ₯ μƒˆλ‘œμš΄ 고양이 사진 10개λ₯Ό λΆˆλŸ¬μ˜€λŠ” ν•¨μˆ˜ μž…λ‹ˆλ‹€)

슀크둀이 λ§ˆμ§€λ§‰μ— 도달 ν–ˆμ„ λ•Œ 호좜될 ν•¨μˆ˜μΈ handleInfiniteScrolling λŠ”

  1. ul μ»¨ν…Œμ΄λ„ˆμ˜ λ§ˆμ§€λ§‰ li 의 visibility κ°€ 1이 λ˜μ—ˆμ„ λ•Œ μƒˆ 고양이 사진듀을 λ‘œλ”©ν•©λ‹ˆλ‹€
  2. ν˜„μž¬ observe li μ—˜λ¦¬λ¨ΌνŠΈλŠ” unobserve ν•˜κ³  μƒˆ li μ—˜λ¦¬λ¨ΌνŠΈλ₯Ό observe ν•΄μ£Όλ©΄ λ©λ‹ˆλ‹€

μ½”λ“œλŠ” Gist λ˜λŠ” Codepen μ—μ„œ 확인 κ°€λŠ₯ν•©λ‹ˆλ‹€

ezgif.com-crop.gif

πŸ‘ 마치며


JavaScript30 의 13일차 Slide in on Scroll λ₯Ό μ‹€μŠ΅ν•˜λ˜ 쀑
이미지가 일정 μŠ€ν¬λ‘€μ— λ„λ‹¬ν–ˆμ„ λ•Œ, transition μ• λ‹ˆλ©”μ΄μ…˜μœΌλ‘œ slide-in 처리λ₯Ό μœ„ν•΄
μ–΄λ–€ 방법이 μžˆμ„κΉŒ ν•˜λ‹€κ°€ 과거에 λ ˆμ§„ 기술 λΈ”λ‘œκ·Έ μ—μ„œ λ³Έ 글을 κΈ°μ–΅ν•΄
μ‹€μ œλ‘œ μ μš©ν•΄ λ³΄μ•˜μŠ΅λ‹ˆλ‹€

이미지 lazy-loading μ΄λ‚˜ λ¬΄ν•œ 슀크둀 등을 직접 κ΅¬ν˜„ν•  수 μžˆλŠ” λŠ₯λ ₯이 μ—†λ‹€κ³  μƒκ°ν–ˆλŠ”λ°
Intersection Observer API λ₯Ό μ΄μš©ν•˜λ‹ˆ μ‰½κ²Œ κ΅¬ν˜„ ν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€

λΉ λ₯Έ μ‹œμΌλ‚΄μ— μ—¬λŸ¬ λΈŒλΌμš°μ €μ—μ„œ μ‚¬μš©ν•  수 있으면 μ’‹κ² λ„€μš”!

잘λͺ»λœ λΆ€λΆ„μ΄λ‚˜ 지적은 μ–Έμ œλ‚˜ ν™˜μ˜ν•©λ‹ˆλ‹€
κΈ΄ κΈ€ μ½μ–΄μ£Όμ…”μ„œ κ°μ‚¬ν•©λ‹ˆλ‹€

πŸ“š References


WebKit Bugzilla
W3 Working Draft
DWB
λ ˆμ§„ 기술 λΈ”λ‘œκ·Έ
codepink.github.io
Unsplash Source ← λ‹€μ±„λ‘œμš΄ 이미지λ₯Ό 뢈러올 λ•Œ ꡉμž₯히 νŽΈλ¦¬ν•©λ‹ˆλ‹€ μΆ”μ²œ γ…‡γ……γ…‡
Using the Intersection Observer API to Trigger Animations and Transitions