Webpack과 코드 분할(Code Splitting)을 통한 자바스크립트 성능 최적화
이 가이드에서는 Webpack을 사용하여 현대적인 자바스크립트 프로젝트를 설정하고 코드 분할을 구현하는 과정을 단계별로 살펴봅니다. 애플리케이션을 더 작은 번들로 나누어 필요할 때만 로드함으로써 초기 페이지 로딩 시간을 줄이는 방법을 배우게 됩니다. 사전 요구 사항으로는 Node.js(v18 이상)가 설치되어 있어야 하며, 자바스크립트 모듈과 커맨드 라인 사용에 대한 기본적인 익숙함이 필요합니다.
프로젝트 설정 및 설치
먼저, 새로운 프로젝트 디렉토리를 생성하고 npm으로 초기화합니다. 이 과정에서 의존성과 스크립트를 관리할 package.json 파일이 생성됩니다.
mkdir webpack-splitting-demo
cd webpack-splitting-demo
npm init -y
Webpack과 Webpack CLI를 개발 의존성(development dependencies)으로 설치합니다. 이 도구들은 코드를 번들링하는 데 핵심적인 역할을 합니다.
npm install --save-dev webpack webpack-cli
소스 코드와 설정 파일을 위한 기본 폴더 구조를 만듭니다. 실제 애플리케이션 코드는 src 폴더에 위치하게 됩니다.
mkdir src
touch webpack.config.js
이제 프로젝트 구조는 다음과 같아야 합니다:
webpack-splitting-demo/ ├── node_modules/ ├── src/ ├── package.json └── webpack.config.js
기본 Webpack 빌드 설정
webpack.config.js 파일을 열고 기본 설정을 작성합니다. 이 설정은 Webpack에게 애플리케이션의 시작점과 번들링된 파일이 저장될 위치를 알려줍니다.
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
메인 엔트리 포인트 파일인 src/index.js를 생성합니다. 우선은 설정이 잘 작동하는지 확인하기 위해 간단한 메시지를 로그로 출력해 보겠습니다.
console.log('애플리케이션 엔트리 포인트가 로드되었습니다.');
function initApp() {
document.body.innerHTML = '메인 앱 로드 완료
';
console.log('앱이 초기화되었습니다.');
}
// DOM이 준비되었을 때 초기화 수행
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initApp);
} else {
initApp();
}
애플리케이션 페이지 역할을 할 간단한 HTML 파일을 src 디렉토리에 생성합니다. 이 파일은 Webpack이 생성한 번들을 로드하게 됩니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Webpack 코드 분할 데모</title>
</head>
<body>
<script src="../dist/main.js"></script>
</body>
</html>
커맨드 라인에서 Webpack을 쉽게 실행할 수 있도록 package.json에 빌드 스크립트를 추가합니다.
{
"name": "webpack-splitting-demo",
"version": "1.0.0",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "^5.90.0",
"webpack-cli": "^5.1.4"
}
}
첫 빌드를 실행합니다. Webpack이 index.js를 처리하고 main.js가 포함된 dist 폴더를 생성할 것입니다.
npm run build
⚠️ 참고: 브라우저에서 HTML 파일을 직접 열면file://프로토콜로 인해 CORS 에러가 발생할 수 있습니다. 제대로 테스트하려면npx serve dist와 같은 간단한 HTTP 서버를 사용하거나, 나중에 추가할 Webpack 개발 서버를 사용하세요.
분할을 위한 모듈 생성
코드 분할을 시연하기 위해, 크기가 크고 리소스를 많이 사용하는 기능을 포함한 모듈을 만듭니다. 이는 즉시 필요하지 않은 복잡한 컴포넌트나 라이브러리를 시뮬레이션합니다.
// src/feature.js
export function renderExpensiveFeature() {
const element = document.createElement('div');
element.innerHTML = `
<h2>무거운 기능 로드됨</h2>
<p>이 콘텐츠와 로직은 별도의 번들로 분할되었습니다.</p>
<p>대용량 라이브러리 크기를 시뮬레이션 중입니다.</p>
`;
console.log('무거운 기능 모듈이 실행되었습니다.');
return element;
}
// 큰 배열을 내보내어 대용량 라이브러리 상황을 시뮬레이션
export const largeDataArray = new Array(10000).fill(null).map((_, i) => `데이터 항목 ${i}`);
이제 메인 index.js를 수정하여 버튼을 클릭했을 때만 이 기능을 동적으로 임포트하도록 합니다. 여기서 사용되는 동적 import() 구문은 Webpack이 코드 분할 지점으로 인식합니다.
console.log('애플리케이션 엔트리 포인트가 로드되었습니다.');
function initApp() {
document.body.innerHTML = '<h1>메인 앱 로드 완료</h1><button id="loadFeature">무거운 기능 로드하기</button>';
const button = document.getElementById('loadFeature');
button.addEventListener('click', () => {
console.log('버튼 클릭됨, 기능을 로드하는 중...');
// 동적 임포트는 Promise를 반환합니다.
import('./feature.js')
.then(module => {
const featureElement = module.renderExpensiveFeature();
document.body.appendChild(featureElement);
console.log('기능 모듈 로드 성공.', module.largeDataArray.length);
})
.catch(err => {
console.error('기능 모듈 로드 실패:', err);
});
});
console.log('앱이 초기화되었습니다.');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initApp);
} else {
initApp();
}
프로젝트를 다시 빌드합니다. 이제 Webpack은 최소 두 개의 파일, 즉 엔트리 번들인 main.js와 동적으로 임포트된 기능을 위한 별도의 번들을 생성합니다.
npm run build
빌드 실행 후 dist 디렉토리를 확인해 보세요. 다음과 같이 숫자로 된 청크(chunk) ID가 포함된 출력물을 볼 수 있습니다.
dist/ ├── main.js └── src_feature_js.js
⚠️ 참고: 분할된 번들(청크)의 정확한 이름은 다를 수 있습니다. Webpack은 기본적으로 모듈 ID를 사용합니다. 다음 단계에서 더 예측 가능한 이름을 설정해 보겠습니다.
설정 최적화
출력물을 개선하기 위해 Webpack 설정을 업데이트합니다. 특정 청크 파일 이름 패턴을 설정하면 번들을 식별하기 쉬워지고 더 나은 캐싱 전략을 세울 수 있습니다.
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
새로 명명된 엔트리 번들을 로드하도록 HTML 파일을 업데이트합니다. [name] 자리 표시자는 엔트리 포인트 이름(기본값은 main)으로 대체됩니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Webpack 코드 분할 데모</title>
</head>
<body>
<script src="main.bundle.js"></script>
</body>
</html>
Webpack Dev Server를 설치하고 설정합니다. 이는 라이브 리로딩을 제공하며 개발 중에 파일을 올바르게 서빙하여 CORS 문제를 방지해 줍니다.
npm install --save-dev webpack-dev-server
package.json에 개발용 스크립트를 추가하고 Webpack 설정에서 개발 서버를 사용하도록 업데이트합니다.
{
"name": "webpack-splitting-demo",
"version": "1.0.0",
"scripts": {
"build": "webpack",
"dev": "webpack serve"
},
"devDependencies": {
"webpack": "^5.90.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
compress: true,
port: 8080,
},
};
테스트 및 검증
개발 서버를 시작합니다. 코드가 번들링되어 http://localhost:8080에서 호스팅됩니다.
npm run dev
브라우저에서 http://localhost:8080을 엽니다. 즉시 "메인 앱 로드 완료" 메시지와 버튼이 보일 것입니다. 브라우저의 개발자 도구(F12)를 열고 네트워크(Network) 탭으로 이동하세요.
처음에는 main.bundle.js만 로드됩니다. "무거운 기능 로드하기" 버튼을 클릭해 보세요. 네트워크 탭을 관찰하면 feature.chunk.js와 같은 파일에 대한 새로운 요청이 발생하는 것을 볼 수 있습니다. 페이지는 전체 새로고침 없이 해당 기능의 내용을 표시합니다.
개발자 도구의 콘솔(Console) 탭을 확인합니다. 동적 임포트가 확인되는 로그 순서를 볼 수 있습니다:
애플리케이션 엔트리 포인트가 로드되었습니다. 앱이 초기화되었습니다. 버튼 클릭됨, 기능을 로드하는 중... 무거운 기능 모듈이 실행되었습니다. 기능 모듈 로드 성공. 10000
프로덕션 빌드를 확인하려면 개발 서버를 중단(Ctrl+C)하고 프로덕션 빌드를 실행합니다. 모드를 변경하면 코드 압축(minification)과 같은 내장된 최적화가 활성화됩니다.
npx webpack --mode=production
dist 폴더를 조사해 보세요. .js 파일들이 압축되어 있을 것입니다. 분할된 번들도 여전히 존재하며, 이는 개발 및 프로덕션 모드 모두에서 코드 분할이 잘 작동함을 확인해 줍니다.
지금까지 Webpack으로 자바스크립트 프로젝트를 설정하고 동적 임포트 코드 분할을 구현해 보았습니다. 이 기술을 사용하면 사용자가 초기 페이지 뷰에 필요한 코드만 다운로드하게 되어 로딩 속도를 크게 개선할 수 있습니다. 다음 단계로는 서드파티 라이브러리 분할, SplitChunksPlugin 사용, 또는 React나 Vue와 같은 프레임워크와 이 설정을 통합하는 방법을 탐구해 볼 수 있습니다.