자바스크립트 DOM 심화 [2] – DOM 트래버싱, 탭 컴포넌트, Intersection Observer API, Lazy Loading

DOM Traversing

const h1 = document.querySelector('h1');

// Going downwards: child
console.log(h1.querySelectorAll('.highlight')); // h1 내부의 .highlight 클래스
console.log(h1.childNodes);
console.log(h1.children); // only for direct children
h1.firstElementChild.style.color = 'white';
h1.lastElementChild.style.color = 'orangered';

// Going upwards: parents
console.log(h1.parentNode);
console.log(h1.parentElement);
h1.closest('.header').style.background = 'var(--gradient-secondary)';
h1.closest('h1').style.background = 'var(--gradient-primary)'; // h1 itself

// Going sideways: siblings
console.log(h1.previousElementSibling);
console.log(h1.nextElementSibling);
console.log(h1.previousSibling);
console.log(h1.nextSibling);

console.log(h1.parentElement.children);
  • 요소를 선택한 후 인접한 요소를 탐색하고, 선택하는 등의 작업을 DOM 트래버싱이라고 한다.

Tabbed Component

  • 각자의 콘텐츠를 가진, ‘탭’을 가진 요소를 탭 컴포넌트라고 부른다.
// Tabbed Component
const tabs = document.querySelectorAll('.operations__tab');
const tabsContainer = document.querySelector('.operations__tab-container');
const tabsContent = document.querySelectorAll('.operations__content');

// tabs.forEach(t => t.addEventListener('click', () => console.log('tab')));
// use event delegation
tabsContainer.addEventListener('click', function (e) {
  const clicked = e.target.closest('.operations__tab'); // 버튼 내부의 텍스트 요소 등을 선택했을 때도 버튼으로 선택하기 위함

  // Guard Clause
  if (!clicked) return; // 잘못 클릭했을 때 함수 종료

  // Remove active classes
  tabs.forEach(t => t.classList.remove('operations__tab--active')); // clearing
  tabsContent.forEach(c => c.classList.remove('operations__content--active')); // clearing

  // Active
  clicked.classList.add('operations__tab--active');

  // Activate content area
  document.querySelector(`.operations__content--${clicked.dataset.tab}`)
    .classList.add('operations__content--active');
});

이벤트 핸들러에 인자 전달하기

// Menu fade animation
const handleHover = function (e, opacity) {
  if (e.target.classList.contains('nav__link')) {
    const link = e.target;
    const siblings = link.closest('.nav').querySelectorAll('.nav__link');
    const logo = link.closest('.nav').querySelector('img');
    siblings.forEach(el => {
      if (el !== link) el.style.opacity = opacity;
    });
    logo.style.opacity = opacity;
  }
}

const nav = document.querySelector('.nav');
nav.addEventListener('mouseover', function (e) {
  handleHover(e, 0.5);
});

nav.addEventListener('mouseout', function (e) {
  handleHover(e, 1);
});
  • bind 메소드를 사용해서 코드를 더 간결하게 만들 수 있다.
const handleHover = function (e) {
  if (e.target.classList.contains('nav__link')) {
    const link = e.target;
    const siblings = link.closest('.nav').querySelectorAll('.nav__link');
    const logo = link.closest('.nav').querySelector('img');
    siblings.forEach(el => {
      if (el !== link) el.style.opacity = this;
    });
    logo.style.opacity = this;
  }
}

nav.addEventListener('mouseover', handleHover.bind(0.5));
nav.addEventListener('mouseout', handleHover.bind(1));

Intersection Observer API

const obsCallback = function (entries, observer) {
  entries.forEach(entry => {
    console.log(entry);
  })
};

const obsOptions = {
  root: null,
  threshold: [0, 0.2],
}

const observer = new IntersectionObserver(obsCallback, obsOptions);
observer.observe(section1);
  • 스크롤 이벤트에 맞춰 코드를 실행하고 싶을 때 사용할 수 있다.
  • threshold로 설정한 뷰 포트에 도달할 때마다 콜백 함수를 호출한다.
const header = document.querySelector('.header');
const navHeight = nav.getBoundingClientRect().height;

const stickyNav = function (entries) {
  const [entry] = entries;
  // console.log(entry);
  if (!entry.isIntersecting) nav.classList.add('sticky');
  else nav.classList.remove('sticky');
}
const headerObserver = new IntersectionObserver
  (stickyNav, {
    root: null,
    threshold: 0,
    rootMargin: `-${navHeight}px`,
  });
headerObserver.observe(header);

Scroll – reveal

const allSections = document.querySelectorAll('.section');

const revealSection = function (entries, observer) {
  const [entry] = entries;

  if (!entry.isIntersecting) return;

  entry.target.classList.remove('section--hidden');
  observer.unobserve(entry.target);
};

const sectionObserver = new IntersectionObserver(revealSection, {
  root: null,
  threshold: 0.15,
});

allSections.forEach(function (section) {
  sectionObserver.observe(section);
  section.classList.add('section--hidden');
});
  • observer.unobserve(entry.target);로 한 번 reveal 된 요소는 다시 함수를 동작시키지 않도록 할 수 있다.

Lazy Loading

...
        <img
          src="img/card-lazy.jpg"
          data-src="img/card.jpg"
          alt="Credit card"
          class="features__img lazy-img"
        />
...
const imgTargets = document.querySelectorAll('img[data-src]');

const loadImg = function (entries, observer) {
  const [entry] = entries;

  if (!entry.isIntersecting) return;

  // Replace src with data-src
  entry.target.src = entry.target.dataset.src;

  entry.target.addEventListener('load', function () {
    entry.target.classList.remove('lazy-img');
  });

  observer.unobserve(entry.target);
};

const imgObserver = new IntersectionObserver(loadImg, {
  root: null,
  threshold: 0,
  rootMargin: '200px',
});

imgTargets.forEach(img => imgObserver.observe(img));
  • 저사양의 이미지를 로드해놓고 해당 이미지에 도달하면 좋은 사양의 이미지로 갈아끼우는 방식이다.

DOM 이벤트의 라이프사이클

  • 1) DOMContentLoad
    • DOM 트리 구성이 완료되었을 때(HTML이 파싱되고 defer 스크립트가 다운로드, 실행) 발생한다.
  • 2) load
    • 이미지, css 등의 모든 리소스가 다운로드되면 발생한다.
  • 3) beforeunload
    • 페이지를 떠나기 직전에 사용할 수 있는 이벤트

Script Loading: defer & async

  • regular 스크립트 태그를 HTML 문서 맨 위에 넣을 경우 HTML 파싱이 늦어지므로 Body 마지막에 스크립트 태그를 사용한다. 하지만 이또한 비효율적이기 때문에 다른 방식을 사용하기도 한다.
  • async 스크립트 태그는 HTML파싱과 동시에 비동기로 스크립트 가져온다. 하지만 실행 자체는 동기로 진행되므로 다운로드가 완료되는 즉시 실행이 되면서 HTML 파싱이 또 다시 밀리게 된다.
  • defer 스크립트는 비동기로 스크립트를 가져오나, 파싱이 모두 끝난 후에 실행이 되도록 한다.
  • body에 스크립트 태그를 넣는 경우 async, defer여도 파싱이 완료된 후에 스크립트를 가져오므로 해당 방식으로 사용할 필요가 없다.
  • DomContentLoaded 이벤트는 async 스크립트를 기다리지 않는다.
  • async는 실행 순서를 보장하지 않지만 defer는 실행 순서를 보장한다.
  • 보통은 defer를 쓰길 권장, async의 경우 GA와 같이 실행 순서를 보장받지 않아도 되는 스크립트에 사용하면 좋다.
  • 모던 브라우저만 async, defer를 지원하므로, regular 스크립트 태그를 사용해야할 경우 body 마지막에 써주면 된다.

Leave a Reply

Your email address will not be published. Required fields are marked *