Intersection Observer로 동적 헤더 만들기
게시 됨: 2022-03-10Intersection Observer API는 요소를 관찰하고 스크롤 컨테이너의 지정된 지점을 통과할 때(항상 그런 것은 아니지만) 콜백 함수를 트리거하는 요소를 감지할 수 있게 해주는 JavaScript API입니다.
Intersection Observer는 비동기식이므로 기본 스레드에서 스크롤 이벤트를 수신 대기하는 것보다 성능이 더 좋은 것으로 간주될 수 있으며 콜백은 우리가 관찰하는 요소가 지정된 임계값을 충족할 때만 발생합니다. 대신 스크롤 위치가 업데이트될 때마다 발생합니다. 이 기사에서는 Intersection Observer를 사용하여 웹 페이지의 다른 섹션과 교차할 때 변경되는 고정 헤더 구성 요소를 구축하는 방법의 예를 살펴보겠습니다.
기본 사용법
Intersection Observer를 사용하려면 먼저 두 개의 매개변수를 사용하는 새 관찰자를 만들어야 합니다. 관찰자의 옵션이 있는 객체와 관찰하는 요소(관찰자 대상이라고 함)가 교차할 때마다 실행하려는 콜백 함수입니다. 루트(대상 요소의 조상이어야 하는 스크롤 컨테이너)와 함께.
const options = { root: document.querySelector('[data-scroll-root]'), rootMargin: '0px', threshold: 1.0 } const callback = (entries, observer) => { entries.forEach((entry) => console.log(entry)) } const observer = new IntersectionObserver(callback, options)
관찰자를 만들면 대상 요소를 관찰하도록 지시해야 합니다.
const targetEl = document.querySelector('[data-target]') observer.observe(targetEl)
옵션 값은 기본값으로 돌아가므로 생략할 수 있습니다.
const options = { rootMargin: '0px', threshold: 1.0 }
루트를 지정하지 않으면 브라우저 뷰포트로 분류됩니다. 위의 코드 예제는 rootMargin
및 threshold
모두에 대한 기본값을 보여줍니다. 이것들은 시각화하기 어려울 수 있으므로 설명할 가치가 있습니다.
rootMargin
rootMargin
값은 CSS 여백을 루트 요소에 추가하는 것과 약간 비슷하며 여백과 마찬가지로 음수 값을 포함하여 여러 값을 사용할 수 있습니다. 대상 요소는 여백을 기준으로 교차하는 것으로 간주됩니다.
이는 요소가 기술적으로 보이지 않는 경우에도 "교차"로 분류될 수 있음을 의미합니다(스크롤 루트가 뷰포트인 경우).
rootMargin
기본값은 0px
이지만 CSS의 margin
속성을 사용하는 것처럼 여러 값으로 구성된 문자열을 사용할 수 있습니다.
threshold
threshold
은 단일 값 또는 0과 1 사이의 값 배열로 구성될 수 있습니다. 이는 교차로 간주되기 위해 루트 경계 내에 있어야 하는 요소의 비율을 나타냅니다. 기본값 1을 사용하면 대상 요소의 100%가 루트 내에서 표시될 때 콜백이 실행됩니다.
이러한 옵션을 사용하여 요소가 표시되는 것으로 분류되는 시점을 시각화하는 것이 항상 쉬운 것은 아닙니다. Intersection Observer를 이해하는 데 도움이 되는 작은 도구를 만들었습니다.
헤더 생성
이제 기본 원칙을 파악했으므로 동적 헤더를 작성해 보겠습니다. 섹션으로 나누어진 웹페이지부터 시작하겠습니다. 이 이미지는 우리가 만들 페이지의 전체 레이아웃을 보여줍니다.
이 기사의 끝에 데모를 포함했으므로 코드를 선택 해제하려는 경우 곧바로 데모로 이동하십시오. (Github 저장소도 있습니다.)
각 섹션의 최소 높이는 100vh
입니다(내용에 따라 더 길 수도 있음). 헤더는 페이지 상단에 고정되어 있으며 사용자가 스크롤해도 제자리에 유지됩니다( position: fixed
사용). 섹션의 배경색은 서로 다르며, 헤더와 만나면 헤더의 색상이 섹션의 색상을 보완하도록 변경됩니다. 사용자가 있는 현재 섹션을 표시하는 마커도 있으며, 다음 섹션이 도착하면 따라 움직입니다. 관련 코드로 바로 이동할 수 있도록 하기 위해 시작점으로 최소한의 데모를 설정했습니다(Intersection Observer API 사용을 시작하기 전에).
마크업
헤더용 HTML부터 시작하겠습니다. 이것은 홈 링크와 탐색이 있는 상당히 단순한 헤더가 될 것이며 특별히 화려하지는 않지만 몇 가지 data-header
속성을 사용할 것입니다. , 및 속성 data-link
가 있는 세 개의 앵커 링크를 클릭하면 사용자가 관련 섹션으로 스크롤됩니다.
<header data-header> <nav class="header__nav"> <div class="header__left-content"> <a href="#0">Home</a> </div> <ul class="header__list"> <li> <a href="#about-us" data-link>About us</a> </li> <li> <a href="#flavours" data-link>The flavours</a> </li> <li> <a href="#get-in-touch" data-link>Get in touch</a> </li> </ul> </nav> </header>
다음으로 섹션으로 나누어진 나머지 페이지의 HTML입니다. 간결함을 위해 기사와 관련된 부분만 포함했지만 전체 마크업은 데모에 포함되어 있습니다. 각 섹션에는 배경색의 이름을 지정하는 데이터 속성과 헤더의 앵커 링크 중 하나에 해당하는 id
가 포함됩니다.
<main> <section data-section="raspberry"> <!--Section content--> </section> <section data-section="mint"> <!--Section content--> </section> <section data-section="vanilla"> <!--Section content--> </section> <section data-section="chocolate"> <!--Section content--> </section> </main>
사용자가 스크롤할 때 페이지 상단에 고정된 상태로 유지되도록 헤더를 CSS로 배치합니다.
header { position: fixed; width: 100%; }
또한 섹션에 최소 높이를 지정하고 콘텐츠 중앙에 배치합니다. (이 코드는 Intersection Observer가 작동하는 데 필요하지 않으며 단지 디자인을 위한 것입니다.)
section { padding: 5rem 0; min-height: 100vh; display: flex; justify-content: center; align-items: center; }
iframe 경고
이 Codepen 데모를 빌드하는 동안 완벽하게 작동 했어야 하는 Intersection Observer 코드가 올바른 교차 지점에서 콜백을 실행하는 데 실패했지만 대신 대상 요소가 뷰포트 가장자리와 교차할 때 실행되는 복잡한 문제에 부딪쳤습니다. 약간의 머리를 긁적 인 후에 나는 이것이 Codepen에서 컨텐츠가 다르게 취급되는 iframe 내에로드되기 때문이라는 것을 깨달았습니다. (자세한 내용은 클리핑 및 교차 사각형에 대한 MDN 문서 섹션을 참조하세요.)
이 문제를 해결하기 위해 데모에서 마크업을 다른 요소로 래핑할 수 있습니다. 이 요소는 예상대로 브라우저 뷰포트가 아닌 스크롤 컨테이너(IO 옵션의 루트) 역할을 합니다.
<div class="scroller" data-scroller> <header data-header> <!--Header content--> </header> <main> <!--Sections--> </main> </div>
동일한 데모 대신 루트로 뷰포트를 사용하는 방법을 보고 싶다면 Github 저장소에 포함되어 있습니다.
CSS
CSS에서 우리는 우리가 사용하는 색상에 대한 몇 가지 사용자 정의 속성을 정의할 것입니다. 또한 헤더 텍스트 및 배경 색상에 대한 두 개의 추가 사용자 정의 속성을 정의하고 일부 초기 값을 설정합니다. (나중에 다른 섹션에 대해 이 두 사용자 정의 속성을 업데이트할 예정입니다.)
:root { --mint: #5ae8d5; --chocolate: #573e31; --raspberry: #f2308e; --vanilla: #faf2c8; --headerText: var(--vanilla); --headerBg: var(--raspberry); }
헤더에서 다음 사용자 정의 속성을 사용합니다.
header { background-color: var(--headerBg); color: var(--headerText); }
다른 섹션의 색상도 설정합니다. 데이터 속성을 선택기로 사용하고 있지만 원하는 경우 클래스를 쉽게 사용할 수 있습니다.
[data-section="raspberry"] { background-color: var(--raspberry); color: var(--vanilla); } [data-section="mint"] { background-color: var(--mint); color: var(--chocolate); } [data-section="vanilla"] { background-color: var(--vanilla); color: var(--chocolate); } [data-section="chocolate"] { background-color: var(--chocolate); color: var(--vanilla); }
각 섹션이 표시될 때 헤더에 대한 몇 가지 스타일을 설정할 수도 있습니다.
/* Header */ [data-theme="raspberry"] { --headerText: var(--raspberry); --headerBg: var(--vanilla); } [data-theme="mint"] { --headerText: var(--mint); --headerBg: var(--chocolate); } [data-theme="chocolate"] { --headerText: var(--chocolate); --headerBg: var(--vanilla); }
각 교차점에서 헤더의 data-theme
속성을 토글할 것이기 때문에 여기에서 데이터 속성을 사용하는 더 강력한 경우가 있습니다.
관찰자 생성
이제 페이지에 대한 기본 HTML 및 CSS가 설정되었으므로 각 섹션이 표시되는 것을 감시하는 관찰자를 만들 수 있습니다. 페이지를 아래로 스크롤할 때 섹션이 헤더의 맨 아래에 닿을 때마다 콜백을 실행하려고 합니다. 이것은 헤더의 높이에 해당하는 음수 루트 여백을 설정해야 함을 의미합니다.
const header = document.querySelector('[data-header]') const sections = [...document.querySelectorAll('[data-section]')] const scrollRoot = document.querySelector('[data-scroller]') const options = { root: scrollRoot, rootMargin: `${header.offsetHeight * -1}px`, threshold: 0 }
섹션의 일부가 루트 여백과 교차 하는 경우 실행되기를 원하므로 임계값을 0 으로 설정합니다.
먼저 헤더의 data-theme
값을 변경하는 콜백을 생성합니다. (특히 헤더 요소에 다른 클래스가 적용될 수 있는 경우 이는 클래스를 추가 및 제거하는 것보다 더 간단합니다.)
/* The callback that will fire on intersection */ const onIntersect = (entries) => { entries.forEach((entry) => { const theme = entry.target.dataset.section header.setAttribute('data-theme', theme) }) }
그런 다음 교차하는 섹션을 감시하는 관찰자를 만듭니다.
/* Create the observer */ const observer = new IntersectionObserver(onIntersect, options) /* Set our observer to observe each section */ sections.forEach((section) => { observer.observe(section) })
이제 각 섹션이 헤더를 만날 때 헤더 색상이 업데이트되는 것을 볼 수 있습니다.
그러나 아래로 스크롤할 때 색상이 올바르게 업데이트되지 않는 것을 알 수 있습니다. 실제로 헤더는 매번 이전 섹션의 색상으로 업데이트됩니다! 반면 위로 스크롤하면 완벽하게 작동합니다. 스크롤 방향을 결정하고 그에 따라 동작을 변경해야 합니다.
스크롤 방향 찾기
스크롤 방향에 대한 변수를 JS에 설정합니다. 초기 값은 'up'
이고 다른 하나는 마지막으로 알려진 스크롤 위치( prevYPosition
)입니다. 그런 다음 콜백 내에서 스크롤 위치가 이전 값보다 크면 direction
값을 'down'
으로 설정하거나 그 반대의 경우 'up'
으로 설정할 수 있습니다.
let direction = 'up' let prevYPosition = 0 const setScrollDirection = () => { if (scrollRoot.scrollTop > prevYPosition) { direction = 'down' } else { direction = 'up' } prevYPosition = scrollRoot.scrollTop } const onIntersect = (entries, observer) => { entries.forEach((entry) => { setScrollDirection() /* ... */ }) }
또한 대상 섹션을 인수로 전달하여 헤더 색상을 업데이트하는 새 함수를 생성합니다.
const updateColors = (target) => { const theme = target.dataset.section header.setAttribute('data-theme', theme) } const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() updateColors(entry.target) }) }
지금까지는 헤더의 동작에 변화가 없어야 합니다. 그러나 이제 스크롤 방향을 알았 updateColors()
함수에 대해 다른 대상을 전달할 수 있습니다. 스크롤 방향이 위쪽이면 진입 대상을 사용합니다. 다운된 경우 다음 섹션을 사용합니다(있는 경우).
const getTargetSection = (target) => { if (direction === 'up') return target if (target.nextElementSibling) { return target.nextElementSibling } else { return target } } const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() const target = getTargetSection(entry.target) updateColors(target) }) }
그러나 한 가지 더 문제가 있습니다. 섹션이 헤더에 도달할 때뿐 아니라 다음 요소가 뷰포트 하단에 표시될 때 헤더가 업데이트됩니다. 이것은 관찰자가 콜백을 두 번 실행하기 때문입니다. 한 번은 요소가 들어갈 때, 또 한 번은 나갈 때입니다.
헤더가 업데이트되어야 하는지 여부를 결정하기 위해 entry
객체에서 isIntersecting
키를 사용할 수 있습니다. 헤더 색상이 업데이트되어야 하는지 여부에 대한 부울 값을 반환하는 또 다른 함수를 만들어 보겠습니다.
const shouldUpdate = (entry) => { if (direction === 'down' && !entry.isIntersecting) { return true } if (direction === 'up' && entry.isIntersecting) { return true } return false }
이에 따라 onIntersect()
함수를 업데이트합니다.
const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() /* Do nothing if no need to update */ if (!shouldUpdate(entry)) return const target = getTargetSection(entry.target) updateColors(target) }) }
이제 색상이 올바르게 업데이트되어야 합니다. CSS 전환을 설정할 수 있으므로 효과가 조금 더 좋습니다.
header { transition: background-color 200ms, color 200ms; }
동적 마커 추가
다음으로 다른 섹션으로 스크롤할 때 위치를 업데이트하는 마커를 헤더에 추가합니다. 이를 위해 의사 요소를 사용할 수 있으므로 HTML에 아무것도 추가할 필요가 없습니다. 헤더의 왼쪽 상단에 배치하기 위해 간단한 CSS 스타일을 지정하고 배경색을 지정합니다. 헤더 텍스트 색상 값을 취하므로 이를 위해 currentColor
를 사용합니다.
header::after { content: ''; position: absolute; top: 0; left: 0; height: 0.4rem; background-color: currentColor; }
너비에 대해 사용자 정의 속성을 사용할 수 있으며 기본값은 0입니다. 또한 번역 x 값에도 사용자 정의 속성을 사용할 것입니다. 사용자가 스크롤할 때 콜백 함수에서 이러한 값을 설정할 것입니다.
header::after { content: ''; position: absolute; top: 0; left: 0; height: 0.4rem; width: var(--markerWidth, 0); background-color: currentColor; transform: translate3d(var(--markerLeft, 0), 0, 0); }
이제 교차점에서 마커의 너비와 위치를 업데이트하는 함수를 작성할 수 있습니다.
const updateMarker = (target) => { const id = target.id /* Do nothing if no target ID */ if (!id) return /* Find the corresponding nav link, or use the first one */ let link = headerLinks.find((el) => { return el.getAttribute('href') === `#${id}` }) link = link || headerLinks[0] /* Get the values and set the custom properties */ const distanceFromLeft = link.getBoundingClientRect().left header.style.setProperty('--markerWidth', `${link.clientWidth}px`) header.style.setProperty('--markerLeft', `${distanceFromLeft}px`) }
색상을 업데이트하는 동시에 함수를 호출할 수 있습니다.
const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() if (!shouldUpdate(entry)) return const target = getTargetSection(entry.target) updateColors(target) updateMarker(target) }) }
마커의 초기 위치도 설정해야 하므로 아무데도 표시되지 않습니다. 문서가 로드되면 첫 번째 섹션을 대상으로 사용하여 updateMarker()
함수를 호출합니다.
document.addEventListener('readystatechange', e => { if (e.target.readyState === 'complete') { updateMarker(sections[0]) } })
마지막으로, 마커가 한 링크에서 다음 링크로 헤더를 가로질러 미끄러지도록 CSS 전환을 추가해 보겠습니다. width
속성을 전환할 때 will-change
를 사용하여 브라우저에서 최적화를 수행할 수 있습니다.
header::after { transition: transform 250ms, width 200ms, background-color 200ms; will-change: width; }
부드러운 스크롤
마지막 터치를 위해 사용자가 링크를 클릭할 때 섹션으로 이동하는 대신 페이지 아래로 부드럽게 스크롤되면 좋을 것입니다. 요즘에는 JS가 필요 없이 CSS에서 바로 할 수 있습니다! 보다 접근 가능한 환경을 위해 사용자가 시스템 설정에서 모션 감소에 대한 기본 설정을 지정하지 않은 경우에만 부드러운 스크롤을 구현하여 사용자의 모션 기본 설정을 존중하는 것이 좋습니다.
@media (prefers-reduced-motion: no-preference) { .scroller { scroll-behavior: smooth; } }
최종 데모
위의 모든 단계를 통합하면 완전한 데모가 됩니다.
브라우저 지원
Intersection Observer는 최신 브라우저에서 널리 지원됩니다. 필요한 경우 구형 브라우저에 대해 폴리필을 사용할 수 있지만 가능한 경우 점진적인 향상 접근 방식을 선호합니다. 헤더의 경우 지원하지 않는 브라우저에 간단하고 변경되지 않는 버전을 제공하는 것은 사용자 경험에 크게 해롭지 않을 것입니다.
Intersection Observer가 지원되는지 감지하기 위해 다음을 사용할 수 있습니다.
if ('IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype) { /* Code to execute if IO is supported */ } else { /* Code to execute if not supported */ }
자원
교차로 관찰자에 대해 자세히 알아보기:
- MDN의 몇 가지 실용적인 예가 포함된 광범위한 문서
- 교차로 관찰자 시각화 도구
- Intersection Observer API를 사용한 타이밍 요소 가시성 – 광고 가시성을 추적하기 위해 IO를 사용할 수 있는 방법을 살펴보는 MDN의 또 다른 튜토리얼
- Denys Mishunov의 이 기사에서는 자산을 지연 로드하는 것을 포함하여 IO의 다른 용도를 다룹니다. 지금은 덜 필요하지만(
loading
속성 덕분에) 여기에서 여전히 배울 것이 많습니다.