23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
안녕하세요, OpenLab 2기 웹 프론트엔드 성능 최적화 스터디에 참여중인 장세영입니다 😊
웹 프론트엔드 성능 최적화 스터디에서는 [프론트엔드 성능 최적화 가이드] 도서를 기반으로 챕터 별 발표 형식으로 스터디를 진행하고 있습니다.
저는 네번째 스터디에서 [3장 홈페이지 최적화]의 이미지 지연 로딩 파트를 담당하여 발표를 진행했습니다.
발표한 내용을 바탕으로 글을 정리해보고자 합니다.
다음은 목차입니다.
Network Custom throttling
최적화할 페이지 구조 및 분석
이미지 지연 로딩 원리
Intersection Observer API
Intersection Observer로 이미지 지연 로딩 구현하기
최적화 전/후 성능 및 지표 비교하기
느낀점
서비스를 분석하고 최적화 해야할 때에는 어떤 작업을 먼저 해야 한다는 규칙은 없습니다.
상황에 따라 판단하여 원하는 분석을 진행하면 되는데, 여기서는 네트워크를 먼저 살펴보겠습니다.
브라우저 개발자 도구의 네트워크 탭에서는 네트워크에 throttling을 적용하여 네트워크의 명확한 흐름을 파악할 수 있으며 원하는 속도로 커스텀 설정이 가능합니다.
저는 3G 설정보다는 빠른 6000kb/s로 Custom throttling 옵션을 추가하여 분석을 진행했습니다.
저희가 최적화할 페이지는 첫 페이지 접속 시 보이는 동영상 배너와 스크롤을 내릴 경우 보이는 이미지들로 구성되어 있습니다.
Network throttling 설정 후 페이지를 로드해보면, 사용자에게 가장 먼저 보여지는 동영상의 로드가 늦어져서 매끄럽지 못한게 보입니다.
네트워크 탭에서 컨텐츠의 다운로드 순서를 확인해보니, 하단에 표시되는 이미지들이 먼저 다운로드되고 동영상은 팬딩 상태(흰색 바)였다가 다운로드가 늦게 진행되는걸 확인할 수 있습니다.
현재 비디오는 가장 처음으로 사용자에게 보여지는 콘텐츠인데, 이미지/폰트가 로드된 후에 로드가 되어 사용자 경험에 좋지 못한 상황입니다.
위의 문제를 해결하기 위해서 당장 사용되지 않는 이미지들을 나중에 다운로드하는 이미지 지연 로딩을 적용하여 동영상이 먼저 다운로드될 수 있도록 개선해보겠습니다.
이미지 지연 로딩은 아직 스크롤이 이미지 위치까지 도달하지 않았을 때는 이미지를 로드하지 않다가, 이미지 위치에 도달하면 그때 이미지를 로드하는 방식으로 적용할 수 있습니다.
여기서 문제점은 스크롤 이동 시 해당 뷰포트에 이미지를 표시할지 판단하는 로직이 필요한데, 이를 스크롤 이벤트에 직접 연결하면 스크롤할 때마다 이벤트가 발생하여 브라우저의 메인 스레드에 부담을 주게 됩니다.
위의 간단한 예시를 통해서도 잠깐의 스크롤로 수십번의 이벤트가 발생하는 것을 확인할 수 있습니다.
(물론 lodash의 throttle 같은 방식으로 이벤트 발생 빈도를 줄일 수는 있지만, 이는 근본적인 해결책이 될 순 없습니다.)
다행히 이런 스크롤 문제를 Intersection Observer API를 통해 쉽게 처리할 수 있습니다!
(단, Intersection Observer는 IE에서 지원되지 않으므로 IE 지원이 필요한 경우 polyfill 라이브러리를 설치해야 합니다.)
Intersection Observer는 웹 페이지에서 특정 요소가 뷰포트나 지정한 영역과 교차하는 것을 감지하는 API입니다.
예를 들어 스크롤할 때 어떤 요소가 화면에 얼마나 보이는지를 알려줍니다.
scroll 이벤트는 스크롤할 때마다 계속 실행되어 성능이 저하되지만, Intersection Observer는 교차가 발생할 때만 동작하므로 훨씬 효율적입니다.
Intersection Observer를 활용하기 위해 options, callback, observer 객체에 대해 알아보겠습니다.
const options = {
root: null,
rootMargin: "0px",
thresold: 1.0,
}root: 관찰할 영역을 지정하는 요소입니다. 따로 지정하지 않으면 브라우저의 뷰포트가 기본값이 됩니다. (default: 브라우저의 뷰포트)
rootMargin: root 요소의 바깥 여백을 설정합니다. CSS 마진처럼 '10px 20px 30px 40px' 형태로 위/오른쪽/아래/왼쪽 값을 지정할 수 있습니다. 이 여백으로 교차 영역을 더 크게 혹은 작게 만들 수 있습니다. (default: 0)
threshold: 콜백 함수를 실행할 시점을 설정합니다. 예를 들어 0.5로 설정하면 관찰 대상이 50% 보일 때 콜백이 실행됩니다. 0부터 1까지의 숫자나 숫자 배열로 지정할 수 있습니다. (default: 0)
예를 들어 위 이미지처럼 threshold: [0.5]로 설정하면, 관찰 대상이 뷰포트에 50% 정도 들어왔을 때 콜백 함수가 실행됩니다. 이를 통해 요소가 화면에 얼마나 보일 때 동작할지 세밀하게 제어할 수 있습니다.
callback 함수는 페이지 로드 시점에 최초로 실행되며, 이후 관찰 대상이 지정한 임계값(threshold)을 만족할 때마다 실행됩니다.
const callback = (entries, observer) => {
entries.forEach((entry) => {
// 각 엔트리는 관찰된 하나의 교차 변화를 표시
// 대상 요소의 경계 사각형 정보:
// entry.boundingClientRect
// entry.intersectionRatio // 교차 비율
// entry.intersectionRect // 교차 영역 정보
// entry.isIntersecting // 교차 여부
// entry.rootBounds // root 요소의 경계 정보
// entry.target // 관찰 대상 요소
// entry.time // 교차 변경이 발생한 시간
});
};콜백함수 매개변수
entries: 교차 정보를 담은 IntersectionObserverEntry 객체들, isIntersecting 값을 통해 타겟 요소가 뷰포트에 들어왔는지에 대한 여부 판단할 수 있습니다.
observer: 현재 관찰자 인스턴스
주의점
callback은 메인 스레드에서 실행되기 때문에, 가능한 빠르게 처리되어야 합니다.
시간이 많이 필요한 작업은 requestIdleCallback*을 사용하여 브라우저 유휴 시간에 처리되도록 해야합니다.
*브라우저의 유휴 시간을 활용하여 우선순위가 낮은 작업을 비동기적으로 실행하는 웹 API
const observer = new IntersectionObserver(callback, options);IntersectionObserver를 통해 생성한 observer 객체에서는 다음과 같은 메소드를 사용할 수 있습니다.
observe(target) : 지정된 대상 요소에 대한 관찰을 시작합니다.
unobserve(target) : 지정한 대상 요소에 대한 관찰을 중지합니다.
disconnect() : 관찰자를 중지하고 모든 대상에 대한 관찰을 종료합니다.
takeRecords() : 아직 처리되지 않은 모든 Intersection 변경 기록을 반환하고 큐를 비웁니다.
이제 Intersection Observer를 활용하여 이미지가 화면에 보이는 순간에 로드되는 지연 로딩을 구현해보겠습니다.
이미지 지연 로딩이 필요했던 Card 컴포넌트 입니다.
Card 컴포넌트의 img 태그를 관찰 대상으로 지정하고, 이미지가 화면에 나타나는 순간에 실제 이미지를 로드하도록 만들어보겠습니다.
import React, { useEffect, useRef } from "react";
function Card(props) {
const imgRef = useRef();
useEffect(() => {
const options = {};
const callback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log("is intersecting", entry.target.dataset.src);
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
};
const observer = new IntersectionObserver(callback, options);
observer.observe(imgRef.current);
return () => observer.disconnect();
}, []);
return (
<div className="Card text-center">
<img ref={imgRef} data-src={props.image} alt="메인이미지" />
<div className="p-5 font-semibold text-gray-700 text-xl md:text-lg lg:text-xl keep-all">
{props.children}
</div>
</div>
);
}
export default Card;수정된 코드에서는 이미지가 화면에 보이면(isIntersecting이 true) data-src의 값을 src로 옮겨서 이미지를 로드합니다.
img 태그는 src 속성에 값이 있을 때만 실제로 이미지를 로드하기 때문입니다.
또 중요한 점으로는, Intersection Observer 인스턴스는 useEffect 내부에서 생성해야 합니다. 그 이유는 다음과 같습니다.
useEffect를 사용하지 않으면 컴포넌트가 렌더링될 때마다 인스턴스가 새로 생성되어 같은 요소에 여러 콜백이 중복 실행될 수 있습니다.
리소스 관리를 위해 useEffect의 정리(clean-up) 함수에서 observer.disconnect를 호출해야 합니다.
네트워크 탭을 통해 확인해보면, 스크롤하여 이미지가 화면에 보이는 순간에 이미지 로드가 시작되는 것을 확인할 수 있습니다.
Card 컴포넌트에서 사용한 이미지 지연 로딩을 공통 컴포넌트로 만들어 다른 이미지들에도 적용해보았습니다.
그 결과, network throttling을 설정했음에도 이전과 달리 비디오가 빠르게 로드되는 것을 확인할 수 있습니다.
네트워크 탭을 보면 비디오가 pending 상태(흰색 바) 없이 바로 다운로드되며 이미지보다 먼저 다운로드가 된 것을 볼 수 있습니다.
lighthouse 분석 지표를 보면 이미지 지연 로딩 적용으로 메인 페이지의 중요 콘텐츠인 비디오가 더 빨리 로드되어 성능(Performance)이 개선되었습니다.
그리고 페이지의 최대 콘텐츠풀 페인트(LCP) 요소인 비디오의 로딩이 빨라져 측정 시간도 감소된 것을 확인할 수 있습니다.
프론트엔드 성능 최적화 스터디를 통해 다양한 최적화 기법들을 배우면서, 실습과 발표를 통해 기존에 알고 있던 내용들을 더욱 깊이 있게 이해하게 되었습니다.
예를 들어 평소 사용하던 Next.js의 경우, 단순히 '이미지나 폰트를 최적화해준다'는 것을 넘어서서 구체적인 최적화 메커니즘과 그 필요성까지 파악할 수 있었습니다.
프레임워크가 제공하는 최적화 기능을 단순히 '아, 이런 게 있구나' 수준으로 아는 것과, 그것이 왜 필요하고 어떤 문제를 해결하는지 이해하는 것은 분명한 차이가 있다고 생각합니다.
스터디를 하며 성능 최적화에 대한 다양한 관점과 접근을 통해, '아는 만큼 보인다'는 진리를 다시 한번 체감할 수 있었던 값진 시간이었습니다.
앞으로 남은 스터디 기간 동안 더 다양한 최적화 기법들을 배우고 적용해볼 수 있다는 점이 기대됩니다.
감사합니다 😀
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.