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);
});
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로 설정한 뷰 포트에 도달할 때마다 콜백 함수를 호출한다.
sticky navigation 구현
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);
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 마지막에 써주면 된다.
Related