프로젝트를 진행하다가 여러가지 이미지 파일로 인해 페이지 로딩이 느려지는 상황을 만났다.
이 문제를 해결하기 위해 lazy loading에 대해 다시 찾아봤고 새로운 점을 알게 되어 글을 작성한다. 이전에 알고 있던 lazy loading은 scroll event를 이용하여 해결한다고 알고 있었는데 이 방법은 몇가지 문제점을 갖고 있었다.
scroll 이벤트는 scroll 이벤트가 발생하면 짧은 시간에 수백번씩 호출되고 된다. 또한 scroll 이벤트뿐만 아니라 다른 이벤트가 존재할 경우 이벤트는 예상보다 더 많이 호출된다. 그리고 element의 위치를 알기 위해 getBoundingClientRect() 함수를 사용하는데 이 함수는 리플로우 현상을 유발하는 문제점을 갖고 있다.
위와 같은 문제점을 보완하기 위해 Intersection Observer가 나왔다는 사실을 알았다.
직역하면 교차 옵저버(!?)라고 할 수 있다. 옵저버의 역할은 관찰, 지켜보는 역할인데 말그대로 지정한 엘리먼트 사이의 교차하는 상황이 발생하면 옵저버가 지켜보고 있다가 알려준다는 것이다. 옵저버를 생성해보자.
const options = {
root: null,
threshold: 1,
};
const observer = new IntersectionObserver(callback, [options]);
옵저버는 모양만 가지고 만들어진다. 옵저버에게 지켜보다가 교차가 발생한 경우 취해야할 행동(callback)과 교차한다는 기준이 무엇인지(options)를 입력해줘야 한다.
callback은 두개의 인자를 받을 수 있는데 그 중 entries는 IntersectionObserverEntry 객체의 리스트 라고 한다. 쉽게 말하면 개발자는 옵저버에게 지켜볼 대상들을 지정할 수 있는데 지정을 하면 entries라는 명단에 관찰 대상으로 배열로 올라간다고 보면 된다. 관찰 대상 중 교차가 발생한 녀석만 잡으면 되니까
options는 옵저버에게 관할 구역을 지정해주는 개념이다. root는 dom요소를 입력할 수 있는데 입력하지 않거나 null을 입력한 경우 브라우저의 viewport가 자동으로 설정된다. threshold는 관할 구역에서 교차 범위가 어느정도 됐을 때 잡느냐를 지정해주는 것이다. [0, 0.5, 1]과 같이 배열로 설정해줘도 되고 숫자만 입력해도 되는데 0.5는 50% 관할 구역에 넘어왔다고 보면 된다.
function callback(entries, testObserver) {
entries.forEach((entry) => {
// 교차된 엘리먼트가 entry로 들어온다. 이를 이용해 취해야할 행동을 입력해주면 된다.
})
}
우리는 위에서 observer를 만들었다.
여기선 react의 ref의 개념을 알아야 하는데 이 개념은 직접 찾아보면서 적용해보면 해결하길 바란다.
useEffect를 이용해 렌더링 이후 img 엘리먼트의 명단을 다 작성한다. 이후 observer에게 img태그들을 보여주면 observe 시킨다.
imgList.forEach((img) => {observer.observe(img)});
useEffect(() => {
const imgList = Array.from(containerRef.current.children);
imgList.forEach((img) => {
observer.observe(img);
});
});
return (
<div className="container" ref={containerRef}>
<img className="img" ref={imgRef} src={logo} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
</div>
);
위에서 말했다시피 옵저버는 이제 지켜볼 명단을 가지고 있다. 그리고 만약 교차되는 엘리먼트를 발견한 경우 if (entry.isIntersecting)
를 통해 경고음이 발생하고 행동을 취할 수 있다.
여기선 사용자 지정 속성(dataset)을 이용하여 사진을 보여주지 않다가 구역에 넘어설 경우 src로 dataset-src값을 할당하여 사진을 보여주고 색깔과 기존에 blur처리도 제거해주는 행동을 취했다.
const observer = new IntersectionObserver((entries, testObserver) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 취할 행동
entry.target.src = entry.target.dataset.src;
entry.target.style.backgroundColor = "blue";
entry.target.style.filter = "blur(0px)";
observer.unobserve(entry.target);
}
});
}, options);
function App() {
const imgRef = useRef(null);
const containerRef = useRef(null);
const options = {
root: null,
threshold: 1,
};
const observer = new IntersectionObserver((entries, testObserver) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
entry.target.style.backgroundColor = "blue";
entry.target.style.filter = "blur(0px)";
observer.unobserve(entry.target);
}
});
}, options);
useEffect(() => {
const imgList = Array.from(containerRef.current.children);
imgList.forEach((img) => {
observer.observe(img);
});
});
return (
<div className="container" ref={containerRef}>
<img className="img" ref={imgRef} src={logo} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
<img className="img" ref={imgRef} data-src={logo} />
</div>
);
}
export default App;