라이트하우스
는 구글에서 제공하는 오픈 소스로웹 사이트의 품질을 측정하고 개선 방향을 제시해주는 자동화 도구
이다. 웹 페이지의 성능, 접근성, SEO, Best Practice 항목으로 점수를 매겨 개선사항을 안내해준다. 모바일, 데스크탑 모두 지원한다.
웹 애플리케이션이 커지면서 큰 스크립트와 많은 이벤트 등으로 성능을 측정하는 기준이 모호해지게 되었고 사용자 기준의 측정 방식이 등장하였다.
사용자 기준의 성능 측정
은 사용자에게 의미 있는 콘텐츠가 처음 보이는 시점이 빠를수록 성능이 높다고 판단한다.
분석하고자 하는 사이트를 띄우고 크롬 개발자 도구를 열어준다. Lighthouse 탭을 누르고 측정에 필요한
Categories
를 선택한 뒤Analyze page load
버튼을 눌러 측정을 시작한다.
: 검사는 로컬 PC로 진행되는 것이므로 같은 사이트일지라도 PC 환경에 영향을 받아 점수가 같을 수 없다고 한다. 그래서 검사 결과를 절대적인 지표가 아닌 하나의 가이드로 생각하고 확인하는 것이 좋다!
성능에 관련된 부분은 바로 Performance 부분이다. Performance는
사용자가 얼마나 빠르게 컨텐츠를 인식하는지
평가하는 지표이다.
페이지 로드가 시작된 후 뷰포트내의 의미있는 콘텐츠 일부가 처음 화면에 렌더링 될 때까지의 시간을 측정. (최초의 DOM 콘텐츠를 렌더링하는데 걸리는 시간)
뷰포트의 컨텐츠 중 가장 큰(넓은) 영역을 차지하는 이미지나 텍스트 요소가 처음 로딩되는 시점
. 가장 큰 영역을 차지하는 요소를 페이지의 주요 콘텐츠로 판단하며, 해당 지표를 기준으로 사용자 중심의 페이지 로드 속도를 판단한다.
페이지가 클릭, 키보드 입력 같은 사용자와 상호작용하지 못했던 시간의 총 합을 측정한다. 차단 시간(Blocking Time)은 Long Task로 인해 메인 스레드가 오랫동안 점유되어 사용자와 상호작용하지 못하는 시간을 의미한다
사용자가 로딩 후 폰트 크기 변경, 광고 레이아웃 등 예상하지 못한 레이아웃을 경험하는 빈도를 정량화해서 시각적인 안정성을 판단
뷰포트 내의 콘텐츠가 시각적으로 표시되는 진행 속도를 측정
Lighthouse라는 웹 사이트 성능 측정 도구에 대해 알게되니 약 3개월 전 작업했던 중고 마켓 프로젝트 TRIMM의 라이트하우스 점수가 궁금해졌다.
두둥.. 36점. 충격을 이루 다 말할 수 없다..
Opportunity
는 추천
사항으로 웹 페이지를 빨리 로드하는데 도움이 되는 제안을 나열하는데 Eliminate render-blocking resources
가 눈에 띄게 절감할 수 있다는 추천이 떠 확인해보게 되었다.
웹 페이지의 first paint를 지연시키는 리소스(Render blocking Resources)를 알려준다. 이 리스트를 확인하여 first paint와 관련된 리소스만 inlining하고 중요하지 않은 리소스는 연기하며, 사용되지 않는 리소스를 제거하는 방식으로 성능을 높이는 것을 추천하고 있다.
내용을 살펴보니 카카오맵 API의 appkey에서 문제가 되고 있는 것 같다! 구글링 후 프로젝트 코드를 살펴보니 index.html에 appkey가 노출되어서 그런 것으로 판단되어 코드 수정을 해주었다.
<!-- public/index.html -->
<script
type="text/javascript"
src="//dapi.kakao.com/v2/maps/sdk.js?appkey=노출된appkey&libraries=services,clusterer,drawing"></script>
.env
에 REACT_APP_KAKAO_MAP_API
를 생성해 appkey를 넣어준다. (배포 시 env에 키와 값을 추가한다)%REACT_APP_KAKAO_MAP_API%
로 넣어준다.<!-- public/index.html -->
<script
type="text/javascript"
src="//dapi.kakao.com/v2/maps/sdk.js?appkey=%REACT_APP_KAKAO_MAP_API%&libraries=services,clusterer,drawing"
></script>
프로젝트 배포 당시에도 해당 에러가 발생하여 어쩔 수 없이 appkey를 노출할 수밖에 없었던 기억이 있다. 그래서 map을 사용한 컴포넌트를 가서 확인해보았다.
<S.MapBox id="map" />
const MapBox = styled.div.attrs({
id: "map",
})`
width: 100%;
height: 400px;
`;
<div id="map" style={{ width: "100%", height: "400px" }}></div>
Eliminate render-blocking resources
가 사라진 걸 확인할 수 있다!
png나 jpeg 형식의 이미지를 사용했기 때문에 발생한다고 한다. 물품 판매에서 이미지를 등록할 때 생기는 문제인 것으로 판단되었다.
WebP 혹은 AVIF는 압축률이 훨씬 좋기 때문에 이 포멧을 사용해서 이미지를 인코딩하면 빠른 로딩 & 적은 데이터 가능하다고 한다.
LazyImage
컴포넌트를 만들자!: 이미지 로딩을 지연시키는 기능을 하는 컴포넌트이다. useRef
를 사용하여 imgRef
라는 참조를 생성한다.
: useEffect
훅을 사용하여 컴포넌트가 마운트될 때 IntersectionObserver
를 생성하고, 이미지 요소를 관찰하도록 설정한다. IntersectionObserver
는 웹페이지의 특정 요소가 뷰포트 내에 들어오거나 나갈 때 반응한다.
: 만약 이미지가 뷰포트 안에 들어오면 (if (entry.isIntersecting)
) 이미지 src 속성을 설정하고 이미지를 로드한다. 그리고 이미지가 로딩되면 더 이상 관찰할 필요가 없어 disconnect()
를 호출하여 관찰을 중단시킨다.
: return을 통해 언마운트시 observer를 disconnect(해제)하여 메모리 누수를 방지한다.
: 해당 컴포넌트는 imgRef
참조를 가진 img 태그를 반환한다. 해당 태그는 alt와 className 속성을 가진다.
import { useEffect, useRef } from "react";
const LazyImage = ({ src, alt, className }) => {
const imgRef = useRef();
useEffect(() => {
const img = imgRef.current;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
img.src = src;
observer.disconnect();
}
});
observer.observe(img);
return () => {
observer.disconnect();
};
}, [src]);
return <img ref={imgRef} alt={alt} className={className} />;
};
export default LazyImage;
oneProduct
의 Image 태그를 LazyImage
컴포넌트로 대체해준다.// src/components/ProductList/oneProduct.js
// <S.Image src={ImageURL}></S.Image>
<S.StyledLazyImg src={ImageURL}></S.StyledLazyImg>
const StyledLazyImg = styled(LazyImage)`
width: 100%;
aspect-ratio: 1;
border-radius: 4px;
transition: all 0.2s linear;
:hover {
transform: scale(1.05);
}
`;
초기에 webp 방식은 생각하지 못해 이미지 압축 라이브러리를 통해 서버로 보내는 이미지의 용량을 줄이는 방식으로 택했다.
1) browser-image-compression
라이브러리를 설치한다.
2) 이미지 등록 컴포넌트에 적용해준다.
// src/pages/product-register/components/Images.js
import imageCompression from "browser-image-compression";
// 이미지 리사이징을 위한 옵션
const options = {
maxSizeMB: 1, // 이미지 최대 용량
maxWidthOrHeight: 1920,
useWebWorker: true, // 최대 넓이(혹은 높이)
};
for (let i = 0; i < files.length; i++) {
const file = files[i];
updatedImages.push(URL.createObjectURL(file)); // 미리보기
updatedDBImages.push(file); // DB용
// updatedImages.push(URL.createObjectURL(file)); // 미리보기
// updatedDBImages.push(file); // DB용
// console.log("이미지 압축 저장 전", file);
try {
const compressedFile = await imageCompression(file, options);
await updatedImages.push(URL.createObjectURL(compressedFile)); // 미리보기
await updatedDBImages.push(compressedFile); // DB용
console.log("이미지 압축 저장 후", compressedFile);
} catch (error) {
console.log(error);
}
}
➡️ 이미지 압축 후 size가 159204
-> 53752
로 약 3배 가량 줄어들었다.
File
형식이Blob
형식으로 나오게 되었다.Blob(Binary Large Object)
은 JavaScript에서 이미지, 사운드, 비디오와 같은 멀티미디어 데이터를 다룰 때 사용한다고 한다.
사용하지 않는 javascript를 로드하면 대역폭이 불필요하게 증가하고 페이지의 첫 번째 페인트(FCP)가 지연되어 전체 페이지 성능이 느려진다.
import { createBrowserRouter } from "react-router-dom";
import Main from "pages/main";
import MakeScrollToTop from "components/MakeScrollToTop";
// import 생략...
const router = createBrowserRouter([
{
element: (
<>
<PrivateRouter>
<MakeScrollToTop />
</PrivateRouter>
</>
),
children: [
{
path: "/",
element: <Main />,
},
// 생략
React.lazy()
와 Suspense
적용import { createBrowserRouter } from "react-router-dom";
import React, { lazy, Suspense } from "react";
const Main = lazy(() => import("pages/main"));
const MakeScrollToTop = lazy(() => import("components/MakeScrollToTop"));
// import 생략...(모든 컴포넌트 lazy() 적용)
const router = createBrowserRouter([
{
element: (
<Suspense fallback={<div>Loading...</div>}>
<>
<PrivateRouter>
<MakeScrollToTop />
</PrivateRouter>
</>
</Suspense>
),
children: [
{
path: "/",
element: <Main />,
},
// 생략
프로젝트 폴더 내부에서 사용하는 배너이미지가 jpeg 형태이기도 하고 크기 자체도 커 해당 문구들이 나온다.
jpg
를 webp
로 변환하여 사용 파일을 수정하자.reference비록 아직 57점으로 성능 점수가 크게 좋아지진 않았지만 나름 36점에서 57점으로 20점 가량 상승했다는 점과 Lighthouse의 성능 개선 부분을 하나하나 파악하고 리팩토링해보면서 실시간으로 점수가 상승하는 것을 눈으로 확인하니 신기하기도 했다. 아직은 많이 부족한 최적화였지만 성능 최적화에 대해서도 고민할 수 있는 시간이었고 추후 개발을 할 때 성능 최적화를 생각하면서 작업할 수 있을 거 같아 큰 도움이 된 시간이었다.