이슈
COEAT 이라는 서비스를 개발하고 있었다.
약 50개의 이미지를 한 번에 불러오는 페이지가 있는데 이 부분에서 문제가 발생했다.
이미지의 크기가 너무 크다 보니까 50개의 이미지를 한 번에 요청하니 페이지가 멈춰버리는 것이었다.
이를 해결하기위해 서버 측에서 Image Resizing을 진행하기로 했고 클라 측에서는 Lazy loading을 추가하기로 했다.
사실 이미지의 리사이징만 완료되어도 성능이 대폭 개선될 것이다.
하지만 추후에 불러와야할 이미지들의 개수가 늘어가게 되었을 때에는 리사이징만으로는 한계가 있을 것이라고 판단했다.
이에 이전에 한 번 살펴보았던 Intersection Observer API를 사용해서 유저가 특정 위치에 도달했을 때 이미지를 로딩할 수 있도록 구현해보았다.
Intersection Observer API
Intersection Observer API 는 web API 중 하나로 대부분의 브라우저에서 지원하고 있다.
하지만 IE는 지원하지 않는다.
이전에는 getBoundingClientRect() 메서드를 사용해서 일일이 요소가 접촉했는지를 따져보았어야 했는데
이 친구가 등장함으로 굉장히 편리해졌다.
나의 계획
<img data-lazySrc={realImgSrc} src={fakeSrc} />
- 초기에는 대체 이미지를 보여준다.
- lazyLoading을 적용하고 싶은 이미지들의 dataset lazySrc에 실제 이미지 src를 보관한다.
- 이미지가 intersecting 되었을 때 src에 lazySrc 값을 넣어준다.
시작하기
가장 먼저 IntersectionObserver 인스턴스를 생성해야 한다.
const observerRef = useRef(null);
useEffect(() => {
observerRef.current = new IntersectionObserver(callback, options);
return () => {
if (observerRef.current) observerRef.current.disconnect();
}
}, []);
생성자는 두 가지 인자를 요구하는데
1. observe 하고 있는 요소가 root의 임계값을 지났을 때 실행되는 callback 함수
2. 설정 속성이 담긴 객체
Options
options에서 중요한 두가지 속성을 알아보자.
root
observe 하고 있는 요소의 가시성을 확인하기 위함. 기본값은 뷰포트이다.
즉, root를 명시하지 않으면 뷰포트 내에 observe 하고 있는 요소가 보이는가? 에 따라 callback이 실행될 것이다.
threshold
위에서 언급했던 임계값에 해당하는 속성이다.
0~1 사이의 값을 가질 수 있다.
threshold: 0일 경우,
observe 하고 있는 요소가 root에 단 1px이라도 보일 경우 callback을 실행함.
threshold: 1일 경우,
observe하고 있는 요소가 root에 전부 보일 때 callback을 실행함.
0.5를 제공할 경우 요소가 50% 보일 때 callback이 실행될 것이다.
기본값은 0이다.
요소 관찰하기
observerRef.current.observe(element);
위와 같이 우리가 관찰하고자 하는(= root의 임계값과 접촉하는지 관찰하고자 하는) 요소를 observe 해야 한다.
여기서 고민이 생겼다.
내가 observe 하고자 하는 img element들을 어떻게 가져올까...
가장 쉬운 방법은 querySelectorAll을 사용해서 모든 img element 리스트를 가져와 forEach로 각 img element에 대하여 observe 해주는 방법이다.
하지만 이 방법은 img에 클래스 네임을 부여해야 하고 돔 라이프사이클과는 조금 다르게 동작할 것 같아 지양하고 싶었다.
그래서 조금 더 React 스러운 방법으로 ref를 사용하고자 했다.
다만 문제는 50여 개의 img element에 어떻게 ref를 모두 부착하냐는 것이다.
ref를 배열로 관리해야 하나...
컨테이너에 ref를 달고 children으로 접근해야 하나...
등 여러 방법을 고려해보았으나 모두 지나치게 비효율적이라고 생각했다.
최종적으로 결정한 방법은 콜백 ref 를 사용하는 것이었다.
react element의 ref props를 줄 때 반드시 useRef 훅을 통해 생성된 ref만 전달할 수 있는 것은 아니다.
const imgRef = useRef(null);
useEffect(() => {
if (imgRef.current) console.log(imgRef.current);
} , []);
return (
<img ref={imgRef} src={} ... />
);
일반적으로 우리는 위와 같은 방식으로 useRef가 반환하는 ref를 element에 부착하여 해당 element에 접근한다.
콜백 ref
콜백 ref는 함수를 ref가 아닌 함수를 전달하는 방식으로 사용한다.
const observeImg = (refElement) => {
if (refElement && observerRef.current) {
observerRef.current.observe(refElement);
}
};
return (
<img ref={observeImg} data-lazySrc={realSrc} src={fakeSrc} ... />
);
콜백 ref는 첫 번째 매개변수로 콜백 ref가 부착된 element를 받는다.
즉, 각 img를 observe 하는 함수를 콜백 ref로 전달하여 해결할 수 있었다.
handleIntersection 함수 작성하기
이제 내가 관찰하고자 하는 요소들을 모두 observe 중이다.
그럼 위에서 IntersectionObserver을 생성할 때 제공해야 하는 callback을 작성해보자.
이 함수에서 해야 할 일들은 다음과 같다.
- 현재 intersecting 상태인지 판단.
- intersecting이라면, 해당 요소의 src 값에 lazySrc를 할당한다.
- 이미지가 로드 완료되었다는 상태를 추가한다.
const handleIntersection = (entries) => {
const LOADED = 'loaded';
entries.forEach((entry) => {
if (entry.isIntersecting && !entry.target.classList.contains(LOADED)) {
entry.target.src = entry.target.dataset.lazysrc;
entry.target.addEventListener('load', function detectLoad(e) {
e.currentTarget.classList.add(LOADED);
e.currentTarget.removeEventListener('load', detectLoad);
});
}
});
};
IntersectionObserver의 callback함수의 첫 번째 매개변수로는 intersecting 상태가 변경된 요소들의 리스트가 제공된다.
이를 entries라고 하는데 entries의 각 요소는 isIntersecting이라는 Boolean 상태를 갖고 있다.
이 상태를 통해 어떤 요소가 우리가 의도한 위치에 들어왔는지를 판단할 수 있다.
또한 element의 이벤트 중에는 load라는 이벤트가 있는데 어떤 리소스가 불러와졌을 때 발생하는 이벤트이다.
이를 통해 이미지가의 로드가 온전하게 완료되었을 때 load라는 클래스를 추가함으로 로딩되었는가의 상태를 부여할 수 있었다.
구현된 모습
영상이 보이지 않는다면 Adblock을 해제해주세요 !
(중요한 영상은 아닙니다만 . . . )
'유연해지기 > React.js' 카테고리의 다른 글
간단하게 React 리렌더링 최적화하기 (3) | 2022.05.29 |
---|---|
React 18 의 새 기능 자동 배칭(Automatic Batching)은 무엇일까? (7) | 2022.04.10 |
리액트에서 이미지 미리보기 만들어보기 (React Image Preview) (1) | 2021.11.14 |
간단한 리액트 커스텀 훅 만들어보기. (React custom hook) (0) | 2021.11.04 |
리액트 절대경로 설정 및 모듈/경로 별명 짓기 with CRA (absolute path, path alias) (0) | 2021.10.21 |