Caching
우리는 배포 가능한 /dist디렉터리를 생성하는 모듈형 애플리케이션을 번들링하기 위해 webpack을 사용하고 있습니다. 일단 서버에 /dist의 콘텐츠가 배포되면 클라이언트(일반적으로 브라우저)가 해당 서버에 접근하여 사이트와 애셋을 가져옵니다. 마지막 단계는 시간이 많이 걸릴 수 있기 때문에 브라우저는 캐싱이라는 기술을 사용합니다. 이렇게 하면 불필요한 네트워크 트래픽을 줄이면서 사이트를 더 빨리 로드할 수 있습니다. 그러나 새 코드를 불러올 경우에는 어려움을 느낄 수 있습니다.
이 가이드는 webpack 컴파일로 생성 된 파일의 내용이 변경되지 않는 한 캐시된 상태로 유지되도록 하는 데 필요한 설정에 초점을 맞춥니다.
Output Filenames
output.filename substitutions 설정을 사용하여 출력 파일의 이름을 정의할 수 있습니다. Webpack은 substitutions 이라고 하는 대괄호 문자열을 사용하여 파일 이름을 템플릿화하는 방법을 제공합니다. [contenthash] substitution은 애셋의 콘텐츠에 따라 고유한 해시를 추가합니다. 애셋의 콘텐츠가 변경되면 [contenthash]도 변경됩니다.
index.html 파일을 수동으로 관리할 필요가 없도록 출력 관리의 플러그인과 시작하기의 예제를 사용하여 프로젝트를 설정해 보겠습니다.
project
webpack-demo
├── package.json
├── package-lock.json
├── webpack.config.js
├── /dist
├── /src
│ └── index.js
└── /node_moduleswebpack.config.js
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import HtmlWebpackPlugin from 'html-webpack-plugin';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
- title: 'Output Management',
+ title: 'Caching',
}),
],
output: {
- filename: 'bundle.js',
+ filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};이 설정으로 빌드 스크립트 npm run build를 실행하면, 다음과 같은 출력이 생성됩니다.
...
Asset Size Chunks Chunk Names
main.7e2c49a622975ebd9b7e.js 544 kB 0 [emitted] [big] main
index.html 197 bytes [emitted]
...보다시피, 번들의 이름은 해시를 통해 콘텐츠를 반영합니다. 변경하지 않고 다른 빌드를 실행하면 해당 파일 이름이 동일하게 유지될 거라고 생각합니다. 그러나 다시 실행하면 이 경우에는 그렇지 않을 수 있다는 것을 알 수 있습니다.
...
Asset Size Chunks Chunk Names
main.205199ab45963f6a62ec.js 544 kB 0 [emitted] [big] main
index.html 197 bytes [emitted]
...이것은 webpack이 특정 보일러플레이트, 특히 런타임과 매니페스트를 엔트리 청크에 포함하기 때문입니다.
Extracting Boilerplate
코드 스플릿팅에서 배운 것처럼 SplitChunksPlugin을 사용하여 모듈을 별도의 번들로 분할 할 수 있습니다. Webpack은 optimization.runtimeChunk 옵션을 사용하여 런타임 코드를 별도의 청크로 분할하는 최적화 기능을 제공합니다. 모든 청크에 대해 단일 런타임 번들을 생성하려면 single로 설정합니다.
webpack.config.js
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import HtmlWebpackPlugin from 'html-webpack-plugin';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
+ optimization: {
+ runtimeChunk: 'single',
+ },
};추출한 런타임 번들을 보기위해 다른 빌드를 실행해 보겠습니다.
Hash: 82c9c385607b2150fab2
Version: webpack 4.12.0
Time: 3027ms
Asset Size Chunks Chunk Names
runtime.cc17ae2a94ec771e9221.js 1.42 KiB 0 [emitted] runtime
main.e81de2cf758ada72f306.js 69.5 KiB 1 [emitted] main
index.html 275 bytes [emitted]
[1] (webpack)/buildin/module.js 497 bytes {1} [built]
[2] (webpack)/buildin/global.js 489 bytes {1} [built]
[3] ./src/index.js 309 bytes {1} [built]
+ 1 hidden modulelodash 또는 react와 같은 타사 라이브러리는 로컬 소스 코드보다 변경 될 가능성이 적기 때문에 별도의 vendor 청크로 추출하는 것도 좋은 방법입니다. 이 단계를 통해 클라이언트는 최신 상태를 유지하기 위해 서버에 더 적은 요청을 할 수 있습니다.
이는 Example 2 of SplitChunksPlugin에 표시된 SplitChunksPlugin의 cacheGroups 옵션을 사용하여 수행할 수 있습니다. cacheGroups과 함께 optimization.splitChunks를 추가하고 다음 파라미터를 사용하여 빌드합니다.
webpack.config.js
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import HtmlWebpackPlugin from 'html-webpack-plugin';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
runtimeChunk: 'single',
+ splitChunks: {
+ cacheGroups: {
+ vendor: {
+ test: /[\\/]node_modules[\\/]/,
+ name: 'vendors',
+ chunks: 'all',
+ },
+ },
+ },
},
};새로운 vendor번들을 확인하기 위해 다른 빌드를 실행해 보겠습니다.
...
Asset Size Chunks Chunk Names
runtime.cc17ae2a94ec771e9221.js 1.42 KiB 0 [emitted] runtime
vendors.a42c3ca0d742766d7a28.js 69.4 KiB 1 [emitted] vendors
main.abf44fedb7d11d4312d7.js 240 bytes 2 [emitted] main
index.html 353 bytes [emitted]
...이제 main 번들에 node_modules 디렉터리의 vendor 코드가 포함되어 있지 않고 크기가 240 bytes로 줄어든 것을 볼 수 있습니다!
Module Identifiers
프로젝트에 다른 모듈print.js를 추가해 보겠습니다.
프로젝트
webpack-demo
├── package.json
├── package-lock.json
├── webpack.config.js
├── /dist
├── /src
│ ├── index.js
+│ └── print.js
└── /node_modulesprint.js
+ export default function print(text) {
+ console.log(text);
+ };src/index.js
import _ from 'lodash';
+ import Print from './print';
function component() {
const element = document.createElement('div');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element. 'Hello webpack!');
return element;
}
document.body.appendChild(component());다른 빌드를 실행하면 main 번들의 해시만 변경 될 것으로 예상합니다, 하지만...
...
Asset Size Chunks Chunk Names
runtime.1400d5af64fc1b7b3a45.js 5.85 kB 0 [emitted] runtime
vendor.a7561fb0e9a071baadb9.js 541 kB 1 [emitted] [big] vendor
main.b746e3eb72875af2caa9.js 1.22 kB 2 [emitted] main
index.html 352 bytes [emitted]
...... 세 가지 모두가 변경된 것을 볼 수 있습니다. 이는 각 module.id가 해석 순서에 따라 할당되기 때문입니다(optimization.moduleIds: 'natural'). webpack 5는 mode: 'none'인 경우에만 기본적으로 이 방식을 사용합니다. 해석 순서가 변경되면 ID도 변경됩니다. 요약하자면:
- 새로운 콘텐츠로 인해
main번들이 변경되었습니다. module.id가 바뀌어vendor번들이 변경되었습니다.- 그리고,
runtime번들은 이제 새로운 모듈에 대한 참조를 포함하기 때문에 변경되었습니다.
첫 번째와 마지막은 예상된 결과이며, 수정하고 싶은 것은 vendor 해시입니다. production 모드는 이미 optimization.moduleIds를 기본적으로 'deterministic'으로, development 모드는 'named'으로 설정하며, 두 가지 모두 빌드 간 안정적입니다. 'deterministic'을 명시적으로 설정하면 mode에 관계없이 ID가 안정적으로 유지됩니다.
webpack.config.js
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import HtmlWebpackPlugin from 'html-webpack-plugin';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
+ moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};이제 새로운 로컬 의존성에도 불구하고 vendor해시는 빌드간에 일관성을 유지해야합니다.
...
Asset Size Chunks Chunk Names
main.216e852f60c8829c2289.js 340 bytes 0 [emitted] main
vendors.55e79e5927a639d21a1b.js 69.5 KiB 1 [emitted] vendors
runtime.725a1a51ede5ae0cfde0.js 1.42 KiB 2 [emitted] runtime
index.html 353 bytes [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.216e852f60c8829c2289.js
...그리고 src/index.js를 수정하여 추가 의존성을 일시적으로 제거해 보겠습니다.
src/index.js
import _ from 'lodash';
- import Print from './print';
+ // import Print from './print';
function component() {
const element = document.createElement('div');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- element. 'Hello webpack!');
+ // element. 'Hello webpack!');
return element;
}
document.body.appendChild(component());마지막으로 빌드를 다시 실행합니다.
...
Asset Size Chunks Chunk Names
main.ad717f2466ce655fff5c.js 274 bytes 0 [emitted] main
vendors.55e79e5927a639d21a1b.js 69.5 KiB 1 [emitted] vendors
runtime.725a1a51ede5ae0cfde0.js 1.42 KiB 2 [emitted] runtime
index.html 353 bytes [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.ad717f2466ce655fff5c.js
...두 빌드 모두 55e79e5927a639d21a1b를 vendor 번들 파일 이름으로 표시한 것을 알 수 있습니다.
Deploying a new build
콘텐츠 해시는 배포할 때마다 새로운 파일명 세트를 생성하며, 이는 코드 스플리팅과 상호 작용하므로 미리 계획할 필요가 있습니다. 배포 전에 페이지를 로드한 방문자는 이전 엔트리 청크를 실행 중이며, 해당 청크는 비동기 청크의 이전 파일명을 요청합니다. 해당 파일이 서버에서 사라졌다면 import()가 거부되고 애플리케이션은 ChunkLoadError를 겪게 됩니다. 보통 탭이 열려 있는 동안 라우트 또는 지연 로드된 위젯이 실패하는 형태로 나타납니다.
이 문제의 해결은 설정이 아닌 배포 측에서 이루어집니다. 두 가지 방법으로 해결할 수 있습니다.
이전 빌드의 애셋을 유지합니다. 오래된 파일을 제공하는 비용은 크지 않으며, 배포 전에 페이지를 로드한 사람들의 오류를 없애줍니다. 디렉터리를 교체하는 대신 새 빌드를 이전 빌드 위에 복사하세요. 빌드 시 출력 디렉터리를 정리하는 경우, output.clean은 keep 조건자를 허용하며, CDN은 다음 배포 시가 아니라 하루 정도 후에 오래된 객체를 만료시킬 수 있습니다.
거부를 처리합니다. 어떤 보존 기간도 주말 내내 열려 있는 탭을 커버하지 못하므로, 실패한 청크 로드를 "이 페이지가 오래되었습니다"로 처리하고 리로드합니다.
async function loadWidget() {
try {
return await import("./widget.js");
} catch {
// the deploy removed the chunk this build asks for
globalThis.location.reload();
}
}관련 옵션: output.chunkLoadTimeout은 webpack이 거부하기 전에 기다리는 시간을 설정하고, output.crossOriginLoading은 청크가 다른 출처에서 제공될 때 불투명한 오류 대신 유용한 오류를 얻기 위해 필요합니다.
Conclusion
캐싱은 복잡할 수 있지만, 애플리케이션이나 사이트 사용자에게 주는 이점으로 그만한 가치가 있습니다. 자세한 내용은 아래의 추가 자료 섹션을 참고하세요.