최근 성능 최적화에 관심이 많아졌습니다. 운영중인 사이드 프로젝트의 운영이 어느정도 안정화 되면서 프론트엔드 성능 최적화에 집중해보기로 했습니다.

프론트엔드 성능 최적화에는 Code-splitting, Lazy loading, Tree shaking, Full Route Cache 등 여러 방법이 존재합니다. 이러한 기법들은 모두 웹 애플리케이션의 성능을 향상시키는 데 도움을 줍니다. 최근에는 Next.js와 같은 프레임워크들이 이러한 최적화 과정을 자동화하여 더 쉽게 성능을 개선할 수 있도록 지원하고 있습니다. Webpack을 잘 알고 있으면 최적화 과정 이해에 도움이 되겠죠?
Webpack의 등장 배경
우리가 완성된 웹 사이트를 로딩해보면 많은 파일들이 다운로드 되는 것을 볼 수 있습니다. 당연하게도 이 파일들이 많아지면 페이지는 느리게 로딩됩니다. 이런 문제를 해결하기 위해 과거에 Js에서는 script 태그를 분리하여 모듈처럼 사용했다고 합니다. 이는 전역을 공유하는 방식으로 같은 변수 이름에 대해 문제가 발생하는 경우가 있었습니다. IIFE(즉시 실행함수), AMD, CommonJs등의 다양한 모듈 스펙을 활용했지만 구현 난이도가 어려운점, 크로스 브라우징 이슈가 있었습니다. 이외에 브라우저별 HTTP 요청 숫자의 제약, 사용하지 않는 코드의 관리, Dynamic Loading & Lazy Loading 미지원 이슈를 해결하기 위해 Bundler가 등장하게 되었습니다.
Webpack이 해결하려는 문제

Webpack은 모듈 번들러입니다. 모듈 번들러는 사진처럼 웹 어플리케이션을 구성하는 자원(HTML, CSS, JS, Image 등)을 모두 각각의 모듈로 보고 이를 조합해서 하나의 결과물을 만드는 도구를 의미합니다. webpack을 이용하면 하나의 JS 파일 뿐만 아니라 Image 모듈들을 모듈화해서 최적화 할 수도 있고 필요하다면 다시 분리할 수도 있습니다. 다양한 방식으로 작업할 수 있는 만큼 많은 확장 기능이 존재합니다.
Core Concept
1. entry
dependency graph를 만들기 위해 탐색을 시작하는 지점입니다.
entry: {
bundle: path.resolve(__dirname, "src/index.js"),
},
2. output
번들을 배포할 절대 경로를 정의합니다.
const path = require("path");
module.exports = {
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name][contenthash].js",
},
}
path나 filename과 같은 옵션으로 위치 및 파일명을 지정할 수 있습니다.
이 부분에서 publicPath로 Image와 같은 외부 리소스를 로드할 때 또 다른 path 설정해줄 수 있습니다. 이는 Prefix 개념으로 사용자가 webpack.config.js에 설정한 주소가 실제 배포되는 주소 앞에 설정됩니다.
// publicPath: /assets/
src="Image.jpg" -> /assets/picture.jpg
3. Loader
Webpack은 JS와 JSON 파일만 인식할 수 있습니다.
그래서 Image와 같은 type의 파일을 처리하기 위해 그에 맞는 Loader가 필요합니다.
rules: [
{
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
Loader는 test와 use 두 옵션을 포함한 rules 객체로 표현합니다. test는 어떤 파일을 처리할건지, use는 어떤 Loader를 사용하여 처리할 것인지 알려줍니다. 특이사항으로 Loader는 코드를 아래부터 읽기 때문에 오른쪽 -> 왼쪽 순으로 작성해줘야합니다.
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: "asset/resource",
},
Webpack5에서 asset/module이 추가되었다. asset/module로 처리하지 못하는 파일을 loader로 처리합니다. resource 파일을 가져와서 수정된 상태를 return 합니다.
4. Plugin
Loader가 주로 Image, CSS, File 등 Chunk 단위로 처리한다면 플러그인은 주로 번들된 결과물을 처리합니다.
plugins: [
new HTMLWebpackPlugin({
title: "Webpack App",
filename: "index.html",
template: "src/template.html",
}),
],
대표적으로 html-webpack-plugin 가 있습니다. 이 plugin은 번들이 완료된 결과물을 HTML script tag에 지정해주는 역할을 합니다.
번들 사이즈 줄이기
번들 사이즈를 줄이면 네트워크 비용, 파싱 및 컴파일 비용, 메모리 비용을 아낄 수 있습니다. 4가지 방법으로 번들 사이즈를 줄일 수 있습니다.
1. Webpack mode
1-1) none: 기본적으로 아무런 최적화나 디폴트 세팅이 없습니다. 주로 개발자가 원하는대로 커스터마이징할 때 이용합니다.
1-2) Development: 디버깅에 도움을 주는 강력한 소스 매핑, localhost서버에서는 라이브 리로딩이나 개발중 새로고침을 하지 않아도 업데이트 할 수 있는 HMR 기능을 목적으로 이용합니다.
1-3) Production: 로드 시간을 줄이기 위해 번들 최소화, 가벼운 소스맵 및 에셋 최적화에 목적을 둡니다. 기본적으로 tersurPlugin을 이용하여 코드를 압축, 난독화 합니다.
2. source map
소스맵이란 배포용으로 빌드한 파일과 원본 파일을 서로 연결시켜주는 기능입니다. tersurPlugin과 같은 방법으로 압축, 난독화된 코드를 디버깅하기 쉽게 이어줍니다.
대부분의 source map은 {option}-source-map 형식입니다. eval, inline, hidden, nosources, cheap과 같은 속성이 있습니다. 속성 별로 번들 사이즈, 빌드 속도, 리빌드 속도의 차이가 있습니다.
성능이 좋은 eval-source-map을 Development에서, 코드를 보여주지 않아도 되는 Production 모드에서는 none, hidden을 사용할 것 같습니다. 특히, eval-, inline- 같은 source map은 인라인으로 생성되기 때문에 번들 사이즈가 커져 Production 빌드에 적합하지 않습니다.
3. code splitting
지금 당장 화면에 보여지는 코드가 아니라면 따로 분리시켜서 필요할 때 불러올 수 있는 기능입니다. JS 파일 용량이 커졌을 때, 페이지 로딩 속도가 저하되는 것을 방지할 수 있는 기능입니다.
code splitting은 MPA와 SPA에서 구현방법이 달라집니다. MPA에서는 entry-point와 dynamic import 를 통해 나눌 수 있습니다. 후자의 경우 크로스 브라우징 이슈가 있어 babel의 플러그인을 사용합니다. preload와 prefetch 기능도 있는데요, 다른 방식과 같이 먼저 불러와야하는 코드와 나중에 불러와야하는 코드를 분리시켜 동작하는 동일한 방식입니다. SPA에서는 React의 React.lazy를 사용할 수 있습니다. Suspense 키워드화 함께 활용하며 이는 컴포넌트가 로딩될 때 UI를 표시해주는 기능을 담당합니다.
optimization.splitChunks라는 기능은 option을 세분화해서 code-splitting을 구현할 수 있도록 도와줍니다.
4. tree shaking
사용하지 않는 코드를 번들링 결과에 포함하지 않는 것을 의미합니다. 코드 단위가 아닌 파일 단위로 동작한다는 것이 중요합니다. 이 기능은Production 모드에서 잘 동작하지만, 라이브러리를 직접 만드는 경우나, commonJs 기반의 라이브러리를 사용할 때 유의해서 사용해야 빌드 속도를 줄일 수 있습니다. 특히 전자의 경우 side effect 개념을 잘 알고 있어야합니다. Side effect 의 예시인 전역 변수입니다.
// example.js
window.someGlobalVariable = 'This is a side effect';
이 코드는 export 를 하지 않고 어떤 파일도 import하고 있지 않지만 전역에 영향을 줄 수 있는 코드이기 때문에 webpack이 인지하지 못합니다. (tree shaking 대상이 아닙니다) 이럴 때 파일경로를 명시해서 직접 설정할 수 있습니다.
"sideEffects": [
"./src/example.js",
]
빌드 속도 줄이기
Webpack5에서 새롭게 추가된 Persistent cache를 통해 빌드 속도를 줄일 수 있습니다. 빌드된 데이터를 하드디스크에 저장해서 re-build시 변화를 감지하는 방식입니다. 첫번째 빌드 시 0.817 secs가 걸리던 빌드 속도가 0.644 secs로 줄어든 것을 볼 수 있습니다.


cache 무효화 정책을 위해 cache options, snapshot과 같은 option이 존재합니다. snapshot에는 대표적으로 timestamp, content hash가 있는데요, 각각 파일 변경의 시간과 파일 내용 전체를 비교하는 방식입니다. 속도와 안정성의 trade-off가 있는 방식입니다.
// development
buildDependencies: { timeStamp: true },
module: { timeStamp: true },
resolve: { timeStamp: true },
// CI Build
buildDependencies: { hash: true },
module: { hash: true },
resolve: { hash: true },
source-map처럼 webpack에서 추천하는 snapshot option이 있습니다. 개발모드에서는 build가 자주 일어나기 때문에 hash를 사용하게 되면 파일 전체를 hasing하니 속도가 늦어집니다. 따라서 timestamp만 사용해서 캐싱 작업을 효율화합니다. CI Build의 경우 주로 git에서 clone을 해오기 때문에 hash를 통해 파일의 내용 전체를 확인 후 캐시의 신뢰성을 높힙니다.
https://www.youtube.com/watch?v=IZGNcSuwBZs
'Tech > Moqly (모클리)' 카테고리의 다른 글
| React lazy, Suspense 동작 방식 알아보기 (1) | 2024.11.12 |
|---|---|
| tanstack-query 소스 뜯어보기 (4) | 2024.11.06 |
| Next13 - Amplify 배포 에러 디버깅 (0) | 2024.03.06 |
| [모클리] Next13 을 선택한 이유 (0) | 2024.02.06 |
| [모클리] Polling으로 채팅 구현하기 (1) | 2024.02.05 |