Webpack과 코드 분할(Code Splitting)을 통한 자바스크립트 성능 최적화
Webpack의 코드 분할 기능을 사용하면 거대한 자바스크립트 번들을 더 작은 청크(chunk) 단위로 나눌 수 있습니다. 이를 통해 필요한 시점에만 코드를 로드하게 되어, 애플리케이션의 로딩 속도와 성능을 크게 개선할 수 있습니다. 이 가이드에서는 간단한 웹 앱을 구축하고, 코드 분할을 위한 Webpack 설정 및 최적화 확인 과정을 단계별로 살펴보겠습니다. 사전 준비 사항으로는 Node.js 20 버전 이상이 설치되어 있어야 하며, 자바스크립트 및 npm에 대한 기본적인 지식이 필요합니다.
프로젝트 설정
먼저 프로젝트를 위한 새 디렉터리를 생성하고 npm으로 초기화합니다. 이는 의존성 설치와 파일 관리를 위한 기초 작업입니다. 터미널에서 다음 명령어를 실행하여 준비를 마칩니다.
mkdir webpack-code-splitting
cd webpack-code-splitting
npm init -y
다음으로 Webpack과 CLI를 개발 의존성으로 설치합니다. Webpack은 모듈을 번들링하며, CLI는 커맨드 라인에서 이를 실행할 수 있게 해줍니다. 또한 로컬 개발 서버를 위해 webpack-dev-server도 추가합니다.
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin
소스 파일과 기본 HTML 템플릿을 포함한 초기 프로젝트 구조를 생성합니다. 여기에는 엔트리 포인트 스크립트와 분할을 테스트할 모듈이 포함됩니다. 이러한 구조는 관심사를 명확하게 분리해 줍니다.
mkdir src
touch src/index.js src/print.js index.html
index.html 파일을 열고 아래의 기본 마크업을 추가합니다. 동적 로딩을 트리거할 버튼이 포함된 간단한 페이지입니다. 이 설정을 통해 실제 코드 분할이 작동하는지 테스트할 것입니다.
<!DOCTYPE html>
<html>
<head>
<title>Webpack Code Splitting</title>
</head>
<body>
<h1>Hello, Webpack!</h1>
<button id="print-button">Print Message</button>
<script src="dist/bundle.js"></script>
</body>
</html>
src/index.js 파일에 함수를 동적으로 임포트(import)하여 사용하는 코드를 작성합니다. 이 파일은 메인 엔트리 포인트 역할을 하며, 버튼에 이벤트 리스너를 설정합니다.
function component() {
const element = document.createElement('div');
element.innerHTML = 'Initial content loaded.';
return element;
}
document.body.appendChild(component());
const button = document.getElementById('print-button');
button.onclick = () => import('./print.js').then(module => module.default());
src/print.js에서는 메시지를 로그로 남기는 간단한 함수를 정의합니다. 이 모듈은 별도의 청크로 분리될 것입니다. 쉬운 임포트를 위해 default로 내보냅니다.
export default function print() {
console.log('Printing from a split chunk!');
}
⚠️ 참고: 사용 중인 Node.js 버전이 동적 임포트를 지원하는지 확인하세요. 구버전의 경우 호환성을 위해 Babel이 필요할 수 있습니다.
Webpack 기본 설정
Webpack이 파일을 어떻게 처리할지 정의하려면 설정 파일이 필요합니다. 루트 디렉터리에 webpack.config.js를 생성합니다. 이 파일에서는 엔트리 포인트, 출력(output), 플러그인 등을 지정합니다.
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
],
mode: 'development',
};
엔트리 포인트는 Webpack이 번들링을 시작할 위치를 알려주며, 출력은 번들링된 파일이 저장될 위치를 정의합니다. HtmlWebpackPlugin은 번들링된 스크립트가 자동으로 삽입된 HTML 파일을 생성해 줍니다. mode를 development로 설정하면 테스트에 유용한 기본값들이 활성화됩니다.
빌드와 서버 실행을 위해 package.json에 스크립트를 추가합니다. 이를 통해 npm을 사용하여 간편하게 실행할 수 있습니다. scripts 섹션을 다음과 같이 업데이트하세요.
"scripts": {
"build": "webpack",
"start": "webpack serve --open"
}
npm run build를 실행하여 초기 번들을 생성합니다. dist 폴더에 bundle.js와 index.html이 생겼는지 확인하세요. 이를 통해 Webpack이 제대로 설정되었음을 확인할 수 있습니다.
예상 결과: 약 1~2KB 크기의 bundle.js 파일 하나와 index.html 파일이 포함된 dist 디렉터리가 생성됩니다. 터미널에 에러가 없어야 합니다.
⚠️ 참고: 만약 'module not found' 에러가 발생하면 설정 파일의 경로를 다시 확인하세요. 상대 경로는 정확해야 합니다.
코드 분할 구현하기
코드 분할을 활성화하려면 webpack.config.js를 최적화(optimization) 설정으로 업데이트해야 합니다. 이는 Webpack에 동적 임포트를 기반으로 청크를 나누도록 지시합니다. optimization 객체를 추가하세요.
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
splitChunks: {
chunks: 'all',
},
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
],
mode: 'development',
};
splitChunks 옵션은 벤더(vendor) 코드와 동적 임포트를 별도의 파일로 자동 분할합니다. 파일 이름에 [name]과 같은 플레이스홀더를 사용하면 청크를 식별하기 쉬워집니다. 이는 메인 번들 크기를 줄이고 일부를 지연 로딩(lazy-loading) 파일로 분산시킵니다.
npm run build로 다시 빌드합니다. 이제 dist 폴더에는 main.bundle.js와 print.chunk.js 같은 청크 파일이 포함되어야 합니다. 메인 번들은 더 작아지고, print 모듈은 별도로 로드됩니다.
예상 결과: dist 내에 여러 개의 JS 파일이 생성됩니다. 메인 번들은 1KB 미만이며, print 함수를 위한 별도의 청크가 존재합니다. 터미널에는 청크 정보가 포함된 빌드 성공 메시지가 표시됩니다.
⚠️ 참고: 프로덕션(production) 모드에서는 코드 압축(minification)이 이루어집니다. 더 작은 파일 크기를 원한다면 'production'으로 전환하되, 디버깅 시에는 'development'를 유지하세요.
동적 임포트 추가 및 최적화
분할할 모듈을 하나 더 추가하여 예제를 확장해 보겠습니다. 다른 기능을 가진 src/another.js 파일을 생성합니다. 이는 여러 청크로 분할되는 과정을 보여줍니다.
touch src/another.js
export default function another() {
console.log('Another message from a different chunk!');
}
두 번째 버튼과 동적 임포트를 포함하도록 src/index.js를 업데이트합니다. 여러 개의 동적 임포트가 각각 별도의 청크를 생성하는 방식을 확인할 수 있습니다. 코드를 다음과 같이 수정하세요.
function component() {
const element = document.createElement('div');
element.innerHTML = 'Initial content loaded.';
return element;
}
document.body.appendChild(component());
const printButton = document.getElementById('print-button');
printButton.onclick = () => import('./print.js').then(module => module.default());
const anotherButton = document.createElement('button');
anotherButton.innerText = 'Another Message';
anotherButton.onclick = () => import('./another.js').then(module => module.default());
document.body.appendChild(anotherButton);
동적 임포트는 프로미스(Promise)를 사용하여 비동기 로딩을 가능하게 합니다. 이는 사용자 상호작용이 있을 때까지 로딩을 지연시켜 초기 페이지 로드 속도를 최적화합니다. Webpack은 이를 감지하고 자동으로 분할합니다.
다시 npm run build를 실행합니다. dist 폴더에 추가 청크 파일이 생성된 것을 확인할 수 있습니다. 각 동적 임포트는 개별 청크가 됩니다.
예상 결과: dist 폴더에 main.bundle.js, print.chunk.js, another.chunk.js가 생성됩니다(이름은 해싱에 따라 다를 수 있음). 번들 크기는 작게 유지되며 청크는 필요할 때만 로드됩니다.
⚠️ 참고: 청크가 분할되지 않는다면 동적 임포트가 올바르게 사용되었는지 확인하세요. 정적 임포트(static import)는 분할을 트리거하지 않습니다.
테스트 및 검증
검증을 위해 npm start로 개발 서버를 실행합니다. 브라우저에서 localhost:8080을 엽니다. 버튼을 클릭하고 콘솔에 메시지가 찍히는지 확인하세요.
예상 동작: 페이지가 초기 콘텐츠와 함께 빠르게 로드됩니다. "Print Message"를 클릭하면 print 청크가 로드되고 메시지가 기록됩니다. "Another Message" 버튼도 마찬가지입니다.
브라우저 개발자 도구를 사용하여 네트워크 요청을 검사합니다. Network 탭으로 이동하여 페이지를 새로고침한 뒤 버튼을 클릭해 보세요. 버튼을 클릭할 때만 청크 파일에 대한 별도의 요청이 발생하는 것을 볼 수 있습니다.
터미널에서 ls -lh dist/*.js 명령어로 번들 크기를 확인하여 성능을 측정합니다. 메인 번들은 최소화되어야 하며, 청크는 각각 약 500바이트 정도여야 합니다. 프로덕션 환경의 경우, 설정에서 모드를 'production'으로 바꾸고 다시 빌드하여 압축된 크기를 확인하세요.
⚠️ 참고: 캐싱이 테스트 결과에 영향을 줄 수 있습니다. 정확한 검증을 위해 캐시를 삭제하거나 시크릿 모드를 사용하세요.
이 가이드에서는 성능 향상을 위해 코드 분할이 설정된 Webpack 앱을 구축하고, 모듈 지연 로딩을 통해 초기 로드 부하를 줄여보았습니다. 더 나아가 React Router를 사용한 라우트 기반 분할을 추가하거나 트리 쉐이킹(tree shaking)을 탐구하여 최적화를 극대화해 보세요.