Lighthouse를 통해 실제 배포한 웹 사이트에 성능 최적화를 해보자!

kim yeseul·2023년 11월 20일
2
post-thumbnail

🌟 먼저 Lighthouse란 무엇일까?

라이트하우스는 구글에서 제공하는 오픈 소스로 웹 사이트의 품질을 측정하고 개선 방향을 제시해주는 자동화 도구이다. 웹 페이지의 성능, 접근성, SEO, Best Practice 항목으로 점수를 매겨 개선사항을 안내해준다. 모바일, 데스크탑 모두 지원한다.

🧐 왜 사용할까?

웹 애플리케이션이 커지면서 큰 스크립트와 많은 이벤트 등으로 성능을 측정하는 기준이 모호해지게 되었고 사용자 기준의 측정 방식이 등장하였다. 사용자 기준의 성능 측정은 사용자에게 의미 있는 콘텐츠가 처음 보이는 시점이 빠를수록 성능이 높다고 판단한다.

🙆‍♀️어떻게 측정할까?

분석하고자 하는 사이트를 띄우고 크롬 개발자 도구를 열어준다. Lighthouse 탭을 누르고 측정에 필요한 Categories를 선택한 뒤 Analyze page load 버튼을 눌러 측정을 시작한다.

🧨 측정결과에 대한 참고사항

: 검사는 로컬 PC로 진행되는 것이므로 같은 사이트일지라도 PC 환경에 영향을 받아 점수가 같을 수 없다고 한다. 그래서 검사 결과를 절대적인 지표가 아닌 하나의 가이드로 생각하고 확인하는 것이 좋다!

📑 카테고리

  • 성능(Performance) : 웹 페이지의 로딩 과정에서 발생하는 성능 문제를 분석
  • 접근성(Accessibility) : 서비스의 사용자 접근성 문제를 분석
  • 권장 사항(Best practices) : 웹사이트의 보안 측면과 웹 개발의 최신 표준에 중점을 두고 분석
  • 검색엔진 최적화(SEO) : 검색엔진에서 얼마나 잘 크롤링되고 검색 결과에 표시되는지 분석
  • 프로그레시브 웹 앱(Progressive Web App) : 서비스 워커와 오프라인 동작 등, PWA와 관련된 문제를 분석

💯 성능(Performance) 지표에 대하여...

성능에 관련된 부분은 바로 Performance 부분이다. Performance는 사용자가 얼마나 빠르게 컨텐츠를 인식하는지 평가하는 지표이다.

📊 FCP(First Contentful Paint)

페이지 로드가 시작된 후 뷰포트내의 의미있는 콘텐츠 일부가 처음 화면에 렌더링 될 때까지의 시간을 측정. (최초의 DOM 콘텐츠를 렌더링하는데 걸리는 시간)

📊 LCP(Largest Contentful Paint)

뷰포트의 컨텐츠 중 가장 큰(넓은) 영역을 차지하는 이미지나 텍스트 요소가 처음 로딩되는 시점. 가장 큰 영역을 차지하는 요소를 페이지의 주요 콘텐츠로 판단하며, 해당 지표를 기준으로 사용자 중심의 페이지 로드 속도를 판단한다.

📊 TBT(Total Blocking Time)

페이지가 클릭, 키보드 입력 같은 사용자와 상호작용하지 못했던 시간의 총 합을 측정한다. 차단 시간(Blocking Time)은 Long Task로 인해 메인 스레드가 오랫동안 점유되어 사용자와 상호작용하지 못하는 시간을 의미한다

📊 CLS(Cumulative Layout Shift)

사용자가 로딩 후 폰트 크기 변경, 광고 레이아웃 등 예상하지 못한 레이아웃을 경험하는 빈도를 정량화해서 시각적인 안정성을 판단

📊 Speed Index

뷰포트 내의 콘텐츠가 시각적으로 표시되는 진행 속도를 측정

실제 배포한 웹 사이트 Lighthouse 검사 후 성능 최적화해보기

Lighthouse라는 웹 사이트 성능 측정 도구에 대해 알게되니 약 3개월 전 작업했던 중고 마켓 프로젝트 TRIMM의 라이트하우스 점수가 궁금해졌다.

검사 결과는...?

두둥.. 36점. 충격을 이루 다 말할 수 없다..

🧎 먼저 하나씩 분석하여 성능을 최적화해보기로 하자.


Opportunity추천 사항으로 웹 페이지를 빨리 로드하는데 도움이 되는 제안을 나열하는데 Eliminate render-blocking resources가 눈에 띄게 절감할 수 있다는 추천이 떠 확인해보게 되었다.

1) 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>
  • 수정 후
    먼저, .envREACT_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>

그런데 자꾸만 map() is not defined 에러가 발생했다.

프로젝트 배포 당시에도 해당 에러가 발생하여 어쩔 수 없이 appkey를 노출할 수밖에 없었던 기억이 있다. 그래서 map을 사용한 컴포넌트를 가서 확인해보았다.

  • 문제가 된 코드
    : 카카오 맵을 사용할 때 id로 map을 전달해야하는데 아래와 같이 스타일드 컴포넌트를 사용해서 문제가 생겼다.
<S.MapBox id="map" />
  
const MapBox = styled.div.attrs({
	id: "map",
})`
	width: 100%;
	height: 400px;
`;
  • 해결한 코드
    : 스타일 컴포넌트를 제거하고 div 태그에 style 속성을 넣어 해당 에러를 해결했다.
<div id="map" style={{ width: "100%", height: "400px" }}></div>

⭕ 수정 이후 Lighthouse

➡️ Eliminate render-blocking resources가 사라진 걸 확인할 수 있다!

2) Serve Images in next-gen formats


png나 jpeg 형식의 이미지를 사용했기 때문에 발생한다고 한다. 물품 판매에서 이미지를 등록할 때 생기는 문제인 것으로 판단되었다.
WebP 혹은 AVIF는 압축률이 훨씬 좋기 때문에 이 포멧을 사용해서 이미지를 인코딩하면 빠른 로딩 & 적은 데이터 가능하다고 한다.

내가 해결한 방식

1-1) 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;

1-2) 이미지를 뿌려주는 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);
	}
`;

2. 이미지 압축을 시도하자.

초기에 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에서 이미지, 사운드, 비디오와 같은 멀티미디어 데이터를 다룰 때 사용한다고 한다.

⭕ 수정 이후 Lighthouse

➡️ Serve Images in next-gen formats 비율이 감소한 것을 확인할 수 있다.

3) Reduce unused Javascript

사용하지 않는 javascript의 문제점

사용하지 않는 javascript를 로드하면 대역폭이 불필요하게 증가하고 페이지의 첫 번째 페인트(FCP)가 지연되어 전체 페이지 성능이 느려진다.

Code Splitting으로 번들 크기를 줄여보자!

  • 기존 라우팅 코드
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 />,
      },
      // 생략

⭕ 수정 이후 Lighthouse

➡️ code splitting 후 Reduce unused JavaScript 문구가 사라진 것을 볼 수 있다.

➡️ code splitting을 통해 불필요한 JavaScript 로드, 실행 코드 제거함으로써 TBT 점수 또한 420ms에서 20ms로 줄어든 것을 확인할 수 있다.

4) Serve Images in next-gen formats, Efficiently encode Images

프로젝트 폴더 내부에서 사용하는 배너이미지가 jpeg 형태이기도 하고 크기 자체도 커 해당 문구들이 나온다.

jpgwebp로 변환하여 사용 파일을 수정하자.

➡️ 해당 안내 문구가 사라졌다..! (근데 점수는 딱히 크게 오르지 않았다)


📈 최종 Performance 점수 (36 ➡️ 57)

회고

비록 아직 57점으로 성능 점수가 크게 좋아지진 않았지만 나름 36점에서 57점으로 20점 가량 상승했다는 점과 Lighthouse의 성능 개선 부분을 하나하나 파악하고 리팩토링해보면서 실시간으로 점수가 상승하는 것을 눈으로 확인하니 신기하기도 했다. 아직은 많이 부족한 최적화였지만 성능 최적화에 대해서도 고민할 수 있는 시간이었고 추후 개발을 할 때 성능 최적화를 생각하면서 작업할 수 있을 거 같아 큰 도움이 된 시간이었다.

reference
  • [React] 성능최적화 1편 - Lighthouse란?
  • 웹 페이지 성능 개선에 필요한 Lighthouse 지표 알아보기
  • 라이트하우스 성능 지표 살펴보기
  • lighthouse를 이용해 성능 최적화 하기 (intro)
  • 성능 개선 #4. LightHouse 진단해서 나온 문제점 개선하기
  • 웹 성능 개선 (1) - Eliminate Render Blocking Resources
  • Lighthouse를 이용한 성능 최적화하기(Performance)
  • profile
    출발선 앞의 준비된 마음가짐, 떨림, 설렘을 가진 주니어 개발자

    0개의 댓글