
랜딩 페이지를 이용한 포트폴리오 사이트를 만들려고 하다가 알게된 웹 API이다.
사용법은 간단하지만 재밌는 웹 API인거 같아서 깊게 파보기 위해 정리하려고 한다.
우리가 지정한 타겟요소가 화면에 보일 때 안 보일때를 감지해서 특정 동작을 할 수 있도록 도와주는 웹 API이다.
이를 이용해 무한스크롤에서 특정 위치에 새로운 컨텐츠를 불러올수도 있고 페이지 스크롤 시에 이미지를 lazy loading할수도 있다.
기존에는 addEventListner의 scroll를 이용한 방법을 사용했다.
window.addEventListener('scroll', function(){
console.log( window.scrollY ) //화면상에서 현재 페이지를 위에서 얼마나 스크롤 했는지 알려준다.
});
하지만 해당 방법은 단시간에 이벤트 리스너를 수없이 호출하고 동기적으로 실행된다는 문제가 있다.
또한 감시하려는 요소마다 이벤트 리스너를 통해서 이벤트가 등록되어 있는 경우, 사용자가 해당 요소를 스크롤 할 때마다 이벤트가 끊임없이 호출되기 때문에 성능에서 많은 문제가 발생한다.
혹은 getBoundingClientRect메서드를 사용하는데 해당 메서드는 요소의 크기와 뷰포트에 상대적인 위치정보를 제공해주는 메서드이다.
const el = document.querySelector('#test');
const imgRect = el.getBoundingClientRect();
console(imgRect);
// 출력결과
{
bottom: 200
height: 40
left: 210
right: 1092.5
top: 202
width: 887
x: 216.5
y: 112
}
하지만 해당 메서드는 로컬 좌표계에서의 위치 및 크기를 계산한 후 해당 결과를 기반으로 화면 상에서의 위치를 계산한다. 그리고 계산된 정보를 바탕으로 DOMRect객체를 생성하고 반환한다. 위 과정에서 계산을 할때마다 리플로우(Reflow)현상이 일어난다.
리플로우는 웹 브라우저에서 DOM(Document Object Model)요소의 레이아웃을 다시 계산하는 과정을 말하는데 이 때 화면의 변경에 따라 다시 계산되어야 하는 것이 리플로우 이다.
getBoundingClientRect메서드는 DOM요소의 현재 크기와 위치 정보를 제공해주기 때문에, 이를 호출하면 해당 요소의 레이아웃이 필요한 만큼 다시 계산될 수 있다.
특히 scroll이벤트나 다른 레이아웃에 영향을 주는 이벤트가 발생하면 브라우저는 관련된 요소들의 레이아웃을 다시 계산하게 되고, 이로 인해 리플로우가 발생한다.
리플로우는 성능에 부정적인 영향을 미칠 수 있다. 특히 scroll이벤트와 같이 빈번하게 발생하는 이벤트에서는 성능 문제가 더욱 부각될 수 있다.
또한 여러 요소에 이벤트가 등록되어 있거나 getBoundingClientRect이 자주 호출되면 각 호출 시에 리플로우가 발생하여 성능 문제가 심각해진다.
Intersection Observer는 브라우저의 뷰포트와 특정한 요소가 뷰포트에 보이는지 안 보이는지에 대한 교차지점을 관찰한다.
우리가 관찰하고 싶은 타겟요소가 기기기의 뷰포트나 특정 요소와 교차할 때 실행하고 싶은 로직을 콜백함수로 전달할수도 있다.
Intersection Observer는 비동기적으로 실행되기때문에, 스레드에 영향을 주지 않으면서 요소들의 변경사항들을 관찰할 수 있다.
즉, scroll이벤트에서 동적으로 요소 관찰에서 발생하는 렌더링 성능이나 이벤트의 연속적인 호출문제를 해결해준다.
또한, getBoundingClientRect대신에 IntersectionObserverEntry에 있는 속성들을 활용하면 요소들의 위치를 알 수 있고 여러가지 유용한 옵션도 활용할 수 있다.
물론, 리플로우 현상도 방지할 수 있다.
const options = {
threshold: 0.5
};
const observer = new IntersectionObserver((entries, observer) => {
//감시하는 대상이 뷰포트에 렌더링 될 때 실행시킬 수 있는 콜백함수 로직
}, options)
const $content = document.querySelector('#content3');
observer.observe($content);
IntersectionObserver를 사용하기 위해서는 해당 객체를 생성해 줘야 한다.
객체 생성의 인수에는 콜백함수가 들어가 수 있으며 콜백함수의 인수에는 entries, observer를 넣을 수 있다.
콜백함수는 타겟인 대상이 뷰포트에 렌더링 되어야 동작한다.
observe
unobserve
disconnect
takeRecordsIntersectionObserverEntry객체의 배열을 반환한다.현재 타겟으로 지정한 요소들을 저장하고 있는 배열이다.
각각의 요소에 접근하려면 인덱스를 이용해서 접근해야 한다.
해당 인수의 옵션은 아래와 같다.
boundingClientRect
intersectionRect
intersectionRatio
intersectionRect영역에서 boundingClientRect영역까지 비율isInterSecting
rootBounds
target
time
observer는 사용된 IntersectionObserver객체 자체를 나타낸다.
root
rootMargin
root가 가진 바깥 여백을 이용해 범위를 확장하거나 축소할 수 있다.thresholdIntersection Observer객체의 콜백이 실행된다.MarkUp
<div class="content" id="content1">
<span>Content 1</span>
</div>
<div class="content" id="content2">
<span>Content 2</span>
</div>
<div class="content" id="content3">
<span>Content 3</span>
</div>
<div class="content" id="content4">
<span>Content 4</span>
</div>
<div class="content" id="content5">
<span>Content 5</span>
</div>
Css
* {
margin: 0;
padding: 0;
}
.content {
border: 1px solid black;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
transition: 0.3s;
}
.visible {
background-color: #839fa2;
color: white;
transition: 0.3s;
}
JavaScript
<script>
const options = {
threshold: 0.5,
};
const observer = new IntersectionObserver((entries, observer) => {
if (entries[0].isIntersecting) {
entries[0].target.classList.add("visible");
} else {
entries[0].target.classList.remove("visible");
}
}, options);
const $content = document.querySelector("#content3");
observer.observe($content);
</script>

"Content 3"영역의 y축 기준 절반 부분에 왔을 때 isIntersecting플래그에 의해서 "visible"클래스가 타겟요소의 클래스에 추가되어지고 타겟요소가 뷰포트에서 y축 기준 절반이상 벗어난다면 "visible"클래스가 제거되는걸 확인할 수 있다.
타겟요소가 관찰교차시점에 들어온 경우 isIntersecting값은 true가 된다.
타겟요소가 관찰교차시점에 들어오지 않은 경우 isIntersecting값은 false가 된다.
threshold : 0.5는 타겟의 가시성을 0.5로 지정하여서 타겟요소의 y축 기준으로 0.5정도가 관찰교차시점으로 정의한 것이다.
<script>
const options = {
threshold: [0.8],
};
const observer = new IntersectionObserver((entries, observer) => {
if (entries[0].isIntersecting) {
const pElement = document.createElement("p");
pElement.textContent = "hello";
entries[0].target.appendChild(pElement);
entries[0].target.classList.add("visible");
} else {
entries[0].target.classList.remove("visible");
const pElement = entries[0].target.querySelector("p");
if (pElement) {
entries[0].target.removeChild(pElement);
}
}
}, options);
const $content = document.querySelector("#content3");
observer.observe($content);
</script>

위와같이 콜백함수에서 타겟요소에다가 appendChild를 통해서 새로운 요소를 만들어서 넣을수도 있다.
기존의 scroll이벤트를 통한 랜딩 페이지 구현 혹은 getBoundingClientRect를 통해서 랜딩 페이지를 구현한 예시를 많이 봤는데 Intersection Observer은 앞에 둘의 단점을 보완한 웹 API라고 생각한다. 물론 해당 API를 깊게 뜯어보던가 랜딩페이지에서 구현로직 퀄리티가 점점 높아지면서 그만큼 Intersection Observer에 대한 한계점을 느낀건 아니지만 이번 포트폴리오 프로젝트에는 그만큼의 기대성능을 요구하지 않을거라는 생각이들어서 해당 웹 API를 통해서 포트폴리오 프로젝트를 도전하려고 한다.