kokoball의 devlog
article thumbnail
728x90

 

(이 글은 다크모드에 최적화되어 있지 않습니다 ㅠ, 라이트 모드로 봐주시면 감사하겠습니다)

 

반성하고 있습니다. 🥲🥲🥲

신입 FE 개발자로 취업 후 야근 + 눈물과 함께 만든 서비스의 성능이 이렇게 안 좋을지 몰랐습니다...

 

이번글은 정말 아무것도 몰랐던 신입 개발자가 Lighthouse 성능 점수를 16점 (25%) 올렸던 경험담을 소개하려 합니다.

참고로 Lighthouse 점수는 '성능', '접근성', '권장사항', '검색엔진 최적화' 이렇게 4가지 기준으로 산정됩니다.

 

(이 글은 '성능' 개선 과정에 대해서 중점적으로 다루도록 하겠습니다.)

 

Lighthouse 도입 이유

작년 6월에 회사에 입사하여 기존에 있던 5년 된 Polymer 기반 레거시 프로젝트를 React로 마이그레이션 하면서 숨 가쁘게 2개월을 보냈습니다.

 

사수 없이 혼자서 프런트엔드를 도맡아 작성한 이 코드가 '올바른 것인지' 잘못되었다면 어떤 부분일지 조언을 얻을 곳도 마땅치 않아서 프로젝트를 마친 후에도 끝냈다는 생각에 마냥 행복하기보다는 그냥 '동작'하는 코드를 작성했을 뿐이라는 불안감이 가득했습니다.

 

때문에 일정이 빈 기간에 해야 할 일을 생각하던 중 성능 개선이 가장 먼저 떠올랐으며, 사용자에게 '더 빠르고 성능이 좋은' 서비스를 제공하겠다는 욕심에 '객관적인 수치화된 목표'가 필요했고 Lighthouse를 도입하게 되었습니다.

주요 과정

첫 테스트 당시 점수

 

Lighthouse를 이용하여 첫 테스트를 돌려봤을 때 가장 심각해 보이는 것은 LCP(Largest Contentful Paint) 부분이었습니다.

 

LCPCore Web Vitals의 지표이며 뷰포트에서 가장 큰 콘텐츠 엘리먼트가 나타날 때 측정하며,

페이지의 주요 내용이 화면에 렌더링이 완료되는 시기를 결정하는 데 사용합니다.

 

LCP를 최적화하기 위해서는 서버를 최적화하는 방법도 있지만 이 글에서는 클라이언트 집중해서 알아보겠습니다.

 

서드파티 자원을 일찍 연결한다.

서드파티의 origin에 대한 서버 요청은 특히 페이지에 중요한 콘텐츠를 표시하는 데 사용되는 경우, LCP에 영향을 줄 수 있습니다.

 

이때  rel="preconnect"를 사용하여 페이지가 가능한 한 빨리 연결을 설정할 것임을 브라우저에게 알려줄 수 있습니다.

브라우저는 필요한 소켓을 미리 설정할 있기 때문에 DNS, TCP, TLS 왕복에 필요한 시간을 절약할 있게 됩니다.

<link rel="preconnect" href="https://example.com" />

 

빠른 DNS Lookup 위해 dns-prefetch 사용할 수도 있으며

<link rel="dns-prefetch" href="https://example.com" />

 

preconnect 지원하지 않는 브라우저의 폴백으로 dns-prefetch 설정할 있습니다.

<head>
  …
  <link rel="preconnect" href="https://example.com" />
  <link rel="dns-prefetch" href="https://example.com" />
</head>

 

Preload를 사용하면 현재 페이지에서 사용될 것이 확실한 리소스들을 빠르게 가져올 수 있습니다.

<link rel="preload" as="script" href="super-important.js">
<link rel="preload" as="style" href="critical.css">

 

중요하지 않은 CSS를 비동기 방식으로 로드하기

최적화를 위해서 사용하지 않는 CSS를 전부 제거해야 하며, 

사이트에서 별도 페이지로 사용되는 경우 다른 스타일 시트로 이동하도록 유도합니다.

 

초기 렌더링에 필요하지 않은 CSS 경우에는 rel="preload" onload 등을 이용하여 비동기 방식으로 로드하도록 합니다.

<link rel="preload" href="stylesheet.css" as="style" onload="this.rel='stylesheet'" />

 

또한, Font 등을 불러올 때 사용되는 @import 문의 사용을 자제하는 것도 도움이 될 수 있습니다.

 

@import 문을 사용하여 외부 스타일 시트를 가져올 때, 브라우저에서 스타일을 병렬로 다운하지 않고,

순차적으로 다운로드하기 때문에 페이지 로딩 속도가 느려질 수 있기 때문입니다.

 

아래 예시처럼 @import 대신에 Link 태그들을 사용하면 병렬 다운로드를 통해 페이지 로딩 성능을 향상할  있습니다.

// @NOTE: 이 코드를

@import url('~~');
@import url('~~');
@import url('~~');


// @NOTE: 이렇게
<link rel="stylesheet" href="~~">
<link rel="stylesheet" href="~~">
<link rel="stylesheet" href="~~">

 

코드 분할로 JS 페이로드 줄이기

기존에는 유저가 보는 첫 화면에서 당장 필요한 부분과 필요하지 않은 부분을 구분하지 않았습니다.

이를 React.lazy 및 Suspense를 사용하여 쉽게 코드를 분할하였고 필요할 때에만 컴포넌트를 로드하도록 변경하였습니다.

 

예를 들어 아래 첫 번째 사진처럼 특정 조건에서만 보이는 컴포넌트들이 많았는데 (ex. 모달 등)

 번째 사진처럼 Hooks 분리하여 지저분하고 길어진 코드들을 따로 관리하여 lazy 로딩을 적용하였습니다.

 

이후 다시 분석 테스트를 했을  성능 점수는 소폭 상승했으며, LCP 부분은 감소한 것을 확인할  있었습니다.

 

LCP 최적화 후 테스트 결과

 

Webpack splitChunks 설정으로 modules 모듈 크기 줄이기

다만, 성능 부분의 추천 탭을 살펴보면 JS 코드 크기에 대한 내용이 많은 것을 확인할 있었는데

아직 큰 크기를 차지하고 있는 JS

 

자세한 설명을 확인해 보면 node modules 같이 webpack 이용해 만든 번들 파일인 vendor.js, outlook.js, polyfill.js 임을 확인할 있었습니다.

JS 중복 모듈 및 크기

 

실제로 npx webpack 통해 파일들을 확인했을  entry points 사용된 부분들의 용량이 크다는 경고를 확인할 있었습니다.

 

webpack 검사 결과

 

  entry points 들은 webpack config 파일에서 확인 가능하며, 이를 제거했을 점수가 높게 나오는 것을 확인할 있었습니다.

 

webpack config의 entry 설정과 제거 했을 때 결과

 

그럼 이 설정을 제거해도 되는 걸까요?

대답은 '아니요'입니다.

 

Polyfill은 브라우저 간 호환성을 향상시키기는데 사용되며 오래된 브라우저에서 최신 JavaScript 기능을 지원하는데 도움을 줍니다. 'core-js/stable'은 Core.js 라이브러리의 stable script를 가져오고, 'regenerator-runtime/runtime'는 asyns/await를 지원하기 위한 Regenerator 런타임을 가져옵니다.

 

Vendor은 외부 라이브러리 및 프레임워크를 번들에 추가하기 위한 것입니다. 일반적으로 웹 애플리케이션에서 자주 사용되는 라이브러리 및 프레임 워크를 'vendor' entry points로 따로 분리하여 캐싱 및 빌드 성능을 최적화하는 데 사용됩니다. 

 

이러한 entry points 설정을 통해 웹팩은 각각의 번들을 생성하고 최종적으로 웹 애플리케이션을 빌드하게 되는데 이는 애플리케이션의 성능을 최적화하고 브라우저 캐싱을 향상하는데 도움이 됩니다.

 

만약 이러한 번들(entry points)을 나누지 않거나 필요한 번들 분리를 수행하지 않는다면 webpack은 모든 모듈을 하나의 큰 번들로 묶어서 번들링 합니다.

 

이 경우 번들 크기가 증가하여 사용자가 애플리케이션 로드 시 초기 로딩 시간이 오히려 증가할 수 있으며, 큰 번들을 다운로드하고 파싱 하는 데 시간이 더 오래 걸릴 수 있으므로 페이지 렌더링이 지연될 수 있습니다.

 

따라서 entry points를 분리하고 번들을 세분화함으로써 초기 로딩 시간을 단축하고 캐싱을 개선하는 게 코드를 더 효과적으로 관리하는 방법입니다. 특히 외부 라이브러리와 프레임 워크를 분리하여 캐싱을 향상하는 것은 일반적인 최적화 방법 중 하나입니다.

 

 

그럼 entry points를 유지하고 중복된 모듈을 제거하여 용량을 줄여야 하는데요.

그 정답은 webpack 설정에서 찾을 수 있었습니다.

 

 

webpack 버전 4가 되면서 파일 청크 플러그인으로 splitChunks를 사용하게끔 변경되었습니다. 

이 플러그인은 initial, async, all 3가지 옵션 중 하나를 필수로 지정되어야 합니다.

 

각 옵션에 대해 간단히 설명하자면

 

initial 옵션은 정적 모듈에 대해 최적화하며, async 옵션은 비동기 모듈에 대한 최적화를 진행합니다.

all 옵션의 경우 비동기로 impory 하든 정적으로 import 하든 상관하지 않고 하나의 청크 파일로 만듭니다.

 

저는 이 프로젝트에 아래 사진과 같이 initial 옵션을 사용했습니다.

splitChunks 옵션 추가

 

 

그 이유는 정적으로 구분된 모듈들에서 중복된 부분을 공통으로 사용하는 청크 파일로 만들어 관리하기 때문입니다

예를 들어 a와 b라는 모듈이 있을 경우 node_vendors~a~b 형식의 번들을 만들어 효과적으로 관리할 수 있습니다.

 

그 이후 npx webpack를 통해 번들 크기를 확인해 봤을 때, 기존에 큰 크기를 차지하고 있던 outlook.js, polyfill.js 등등 파일들의 크기가 크게 줄어든 것을 확인할 수 있었습니다.

splitChunks 설정 후 webpack 상태

 

 

TerserPlugin 플러그인을 통한 텍스트 압축

splitChunks 설정 이후 다시 한번 lighthouse 검사를 했을 땐 점수가 오른 것을 볼 수 있었지만, 

outlook/691.js, outlook/454.js 같은 파일들이 많아지다 보니 아래 사진처럼 텍스트 압축 사용에 대한 추천을 받았습니다.

 

 

텍스트 압축 사용 추천

 

 

이 경우 TerserPlugin 플러그인을 설치하여 JS 파일을 압축하였고 크기를 최적화할 수 있었습니다.

npm install terser-webpack-plugin --save-dev
optimization: {
      minimize: true,
      minimizer: [new TerserPlugin({ extractComments: true })],

 

추가적으로 extractComments 설정을 통해 주석을 추출하거나 제거할 수 있습니다.

 

최종적으로 검사해 보니 성능 점수 85점으로 기존의 69점에서 16 오른 것을 있었습니다. 😀

 

진짜_최종_진짜_진짜_최종

 

 

 

https://ui.toast.com/posts/ko_202012101720

https://web.dev/articles/code-splitting-suspense? utm_source=lighthouse&utm_medium=devtools

https://simsimjae.medium.com/webpack4-splitchunksplugin-%EC%98%B5%EC%85%98-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-19f5de32425a

https://webpack.js.org/configuration/mode/

https://negabaro.github.io/archive/webpack-splitChunks

728x90
profile

kokoball의 devlog

@kokoball-dev

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!