안녕하세요. 이번 시간에는 웹팩에 대해서 알아보겠습니다! 특히 최근에 나온 웹팩5에 대해서 다룰 겁니다. 웹팩2부터 웹팩을 사용했던 것 같은데 어느새 5까지 나왔습니다.
웹팩3에서는 ModuleConcatenationPlugin과 import()
에 청크 네임을 넣을 수 있는 부분이 추가되었습니다. 웹팩4는 생각보다 많이 바뀌었습니다. 경쟁자인 parcel을 의식하는 느낌도 드네요. 웹팩은 앞으로도 빠르게 메이저 버전을 내놓으면서 주요 기능들을 추가한다고 하네요.
요즘 웹팩이 매우 핫하지만 사람들은 웹팩에 대해 매우 어려워합니다. 저도 어려웠습니다. 왜냐면 공식 문서가 친절하지 않거든요(지금 이것도 매우 친절해진 겁니다). 그리고 모든 파일을 하나로 합친다는 개념도 잘 와닿지 않습니다. 왜 파일을 하나로 합쳐야 할까요? 바로 http 요청이 비효율적이기 때문입니다.
웹 페이지는 수 많은 구성요소로 이루어져 있습니다. 기본적인 html, js, css 파일 외에도, 웹폰트, favicon, 이미지, json 데이터 등등 수 많은 파일들을 받아와야 합니다. 다행히 http/2에서는 하나의 커넥션에 동시에 여러 파일들을 요청할 수 있습니다. http/2가 점점 더 보편화되어가고 있지만 여전히 파일 개수가 적어야 좋은 경우도 많습니다. 심지어 http/2를 못 쓴다면 http/1.1에서는 커넥션 하나를 열어 하나씩 요청을 보내야합니다. 하나의 요청이 끝나야 다음 요청을 보낼 수 있기 때문에 요청이 많을수록 비효율적이죠. (물론 브라우저에서 파이프라이닝이라는 꼼수로 요청을 여러 개씩 보내기는 하는데 그것도 동시에 6개 정도 뿐이고, 근본적인 해결 방법은 아닙니다)
http/2는 아직은 무리고(망할 IE에서 지원하지 않습니다) http/1은 너무 느리다면 어떻게 해야 할까요? 네... 방법이 없습니다. 개발자인 저희가 희생해야죠. 바로 요청 수를 줄이는 겁니다! 그래서 이미지는 스프라이트로 만들어 한 번에 받고, 걸프, 그런트같은 번들러로 js파일이나 css파일을 하나로 합치곤 했죠. 그러다가 이제 번들러 끝판왕 웹팩이 나왔습니다. 하나로 합쳐주면서 크로스 브라우징 대응도 해주고 압축도 해주는 등 여러모로 편한 점이 많습니다. 아래의 그림처럼 여러 파일들을 하나로 합쳐줍니다.
자바스크립트 생태계를 이용하면서 자주 접하는 또 하나의 문제는 JS 패키지 간의 의존 관계입니다. ES2015 모듈, RequireJS, CommonJS, UMD같은 JS 모듈 시스템들이 나오면서 JS 파일도 다른 프로그래밍 언어처럼 모듈 개념이 생겼습니다. import나 require로 js끼리 서로 의존합니다. 특히 노드로 만들다 보면 모듈이 기본 수 백개에서 많게는 수 만개까지 갑니다. 이런 것을 하나의 JS로 합쳐주는 거죠.
하나의 파일로 합치기엔 너무 크다면 여러 개의 파일로 나눌 수도 있습니다. 보통 라이브러리들은 자주 수정되지 않기 때문에 라이브러리만 모아둔 JS 파일 하나를 만들고, 코드 수정이 자주되는 핵심 페이지는 따로 하나 만들어서 두 개의 JS가 생성됩니다. 다 여러분이 설정하기에 따라 달려있습니다.
주의할 점은 import랑 require을 쓰지 않고 그냥 옛날처럼 스크립트 태그들을 주르륵 불러오는 방식으로 코딩하신 분은 웹팩의 장점을 누릴 수 없다는 겁니다. 그런 분들은 모듈 시스템 부터 공부하셔야 합니다.
gulp, grunt같은 것을 사용해보셨다면 태스크러너나 번들러의 개념을 아시겠지만, 일단 아예 처음 사용하는 분이라고 생각하고 알려드리겠습니다. 일단 기본적인 Node.js랑 npm을 사용할 줄 알아야합니다. 모르신다면... NodeJS 강좌 1강을 참고하세요.
npm i -D webpack webpack-cli
명령프롬프트에서 npm으로 위와 같이 개발 환경으로 설치해줍니다. 웹팩3까지는 webpack만 설치해도 되었는데 웹팩4부터는 webpack-cli를 같이 설치해야 커맨드라인에 webpack이란 명령어를 사용할 수 있습니다. 웹팩은 하나의 설정 파일로 모든걸 해결합니다. 이 점은 살짝 grunt랑 비슷합니다. 그러면 이제 설정 파일을 만들어야겠죠? package.json이 있는 위치에 다음 파일을 만들어줍니다.
webpack.config.js
const webpack = require('webpack');
module.exports = {
mode: 'development',
entry: {
app: '',
},
output: {
path: '',
filename: '',
publicPath: '',
},
module: {
},
plugins: [],
optimization: {},
resolve: {
modules: ['node_modules'],
extensions: ['.js', '.json', '.jsx', '.css'],
},
};
파일명이 webpack.config.js여야 웹팩이 바로 인식합니다. 이름을 다르게 하고 싶다면 (예를 들면 webpack.config.prod.js)라면 명령 프롬프트에서 실행할 때 webpack --config webpack.config.prod.js라고 --config 플래그를 사용해 경로를 알려주면 됩니다. 코드를 따라 치기보다는 원리만 알고 가시면 됩니다.
타입스크립트라면 webpack.config.ts 파일을 만들면 됩니다. 이 때는 @types/webpack을 설치해야 웹팩에 대한 타이핑이 인식됩니다.
아직까지는 빈 껍데기만 있습니다. 핵심적인 부분은 entry, output, module, plugins 이렇게 네 개입니다. 다른 부분까지 모두 알려드리기는 힘들 것 같고요. 이 위주로 설명드리겠습니다.
마지막 resolve만 먼저 설명드리자면 웹팩이 알아서 경로나 확장자를 처리할 수 있게 도와주는 옵션입니다. modules에 node_modules를 넣으셔야 디렉토리의 node_modules를 인식할 수 있습니다. 그리고 extensions에 넣은 확장자들은 웹팩에서 알아서 처리해주기 때문에 파일에 저 확장자들을 입력할 필요가 없어집니다.
mode
웹팩4에서 추가되었습니다. mode가 development면 개발용, production이면 배포용입니다. 배포용일 경우에는 알아서 최적화가 적용됩니다. 따라서 기존 최적화플러그인들이 대량으로 호환되지 않습니다.
entry
entry 부분이 웹팩이 파일을 읽어들이기 시작하는 부분입니다. app이 객체의 키로 설정되어 있는데 이 부분 이름은 자유롭게 바꾸시면 됩니다. 저 키가 app이면 결과물이 app.js로 나오고, zero면 zero.js로 나옵니다.
{
entry: {
app: '파일 경로',
zero: '파일 경로',
}
}
위와 같이 하면 app.js, zero.js 두 개가 생성됩니다. 결과물로 여러 JS를 만들고 싶을 때 저렇게 구분해주면 됩니다. 보통 멀티페이지 웹사이트에서 위와 같이 entry를 여러 개 넣어줍니다. 하나의 entry에 여러 파일들을 넣고 싶을 때는 아래처럼 배열을 사용하면 됩니다.
{
entry: {
app: ['a.js', 'b.js'],
},
}
위의 경우는 a.js랑 b.js가 한 파일로 엮여 app.js라는 결과물로 나옵니다. 이렇게 웹팩은 entry의 js 파일부터 시작해서 import, require 관계로 묶여진 다른 js, css, json까지 알아서 파악한 뒤 모두 entry에 기재된 키 개수만큼으로 묶어줍니다.
js 파일 대신 npm 모듈들을 넣어도 됩니다. 보통 @babel/polyfill이나 eventsource-polyfill같은 것들을 적용할 때 다음과 같이 합니다.
아래는 리액트에서 주로 사용하는 예시입니다. app.js와 vendor.js가 결과물로 나옵니다(웹팩4에서부터는 vendor를 자동으로 만들어줍니다. 다다음 강좌 를 참고하세요).
{
entry: {
vendor: ['@babel/polyfill', 'eventsource-polyfill', 'react', 'react-dom'],
app: ['@babel/polyfill', 'eventsource-polyfill', './client.js'],
},
}
이렇게 하면 각각의 엔트리가 polyfill들이 적용된 상태로 output으로 나옵니다. IE 환경에서 최신 자바스크립트를 사용해 개발하고 싶다면 저 두 폴리필을 npm에서 다운 받은 후 저렇게 모든 엔트리에 넣어주셔야 합니다.
참고로 @babel/polyfill은 deprecated되었으니 core-js와 regenerator-runtime으로 대체하는 것이 좋습니다. 또한 entry에 넣기보다는 ./client.js같은 파일 최상단에 import나 require하는 것이 좋습니다.
npm i core-js regenerator-runtime;
./client.js 최상
import "core-js/stable";
import "regenerator-runtime/runtime";
밑에 loader에서 설명하겠지만 @babel/preset-env에서 useBuiltIns: 'entry'를 하면 import한 것들이 target 속성에 맞춰 자동으로 변환됩니다.
output
이제 결과물이 어떻게 나올지 설정을 해야 합니다.
{
output: {
path: '/dist',
filename: '[name].js',
publicPath: '/',
},
}
path랑 publicPath가 헷갈릴 수 있겠네요. path는 output으로 나올 파일이 저장될 경로입니다. publicPath는 파일들이 위치할 서버 상의 경로입니다. Express에 비유하면 express.static
경로와 비슷한 겁니다. filename을 보시면 좀 이상하게 생긴 게 있습니다. [name].js라고 되어 있는데요. 이렇게 써줘야 [name]에 entry의 app이나 vendor 문자열이 들어가 app.js, vendor.js로 결과물이 나옵니다. [name].js가 아니라 result.js라고 적으면 그대로 result.js로 결과물이 나옵니다.
다른 옵션으로는 [hash]나 [chunkhash]가 있습니다. filename에 [name].[hash].js처럼 쓸 수 있습니다.
[hash]는 매번 웹팩 컴파일 시 랜덤한 문자열을 붙여줍니다. 따라서 캐시 삭제 시 유용합니다. [hash]가 컴파일할 때마다 랜덤 문자열을 붙여준다면 [chunkhash]는 파일이 달라질 때에만 랜덤 값이 바뀝니다. 이것을 사용하면 변경되지 않은 파일들은 계속 캐싱하고 변경된 파일만 새로 불러올 수 있습니다. 저는 chunkhash를 애용합니다.
loader
이제부터 막강한 웹팩의 기능들이 나옵니다. 바로 로더(loader)입니다. 보통 웹팩을 사용하면 babel을 주로 같이 사용합니다. ES2015 이상의 문법이나 타입스크립트 그리고 리액트의 JSX같은 문법을 브라우저에서 사용하기 위함인데요. IE같은 구형 브라우저랑 호환시킬 수도 있습니다. babel을 웹팩5와 연결시켜 볼까요? 일단 설치부터 해봅니다.
npm i -D babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
일단 babel-loader와 @babel/core는 필수이고요. 나머지 preset들은 선택입니다. preset-react는 react 하시는 분만 설치하면 되고요. preset-env는 브라우저에 필요한 ecmascript 버전을 자동으로 파악해서 알아서 polyfill을 넣어줍니다. 정말 놀라운 기술입니다. preset-typescript는 타입스크립트를 사용하신다면 넣으세요. 바벨에 대해서는 바벨 강좌 를 참고하시면 좋습니다.
{
module: {
rules: [{
test: /\.jsx?$/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env', {
targets: { node: 'current' }, // 노드일 경우만
modules: 'false',
useBuiltIns: 'usage'
}
],
'@babel/preset-react', // 리액트를 쓴다면
'@babel/preset-typescript' // 타입스크립트를 쓴다면
],
},
exclude: ['/node_modules'],
}],
},
}
env만 좀 독특하죠? 'env' 다음에 target과 modules: false, useBuiltIns: 'usage'라는 옵션이 들어 있습니다. option의 option인 셈이죠.
target은 지원하길 원하는 환경을 적는 곳입니다. 현재 최신 버전 노드로 되어있는데 구 버전 노드 버전을 적어주면 구 버전 문법을 지원하기 위해 폴리필들이 추가됩니다. 노드 대신 브라우저를 타겟으로 할 수도 있습니다.
modules를 false로 해야 최신모듈 시스템이 그대로 유지되어서 트리 쉐이킹이 됩니다. ES2015 모듈 시스템에서 import되지 않은 export들을 정리해주는 기능이죠. 용량이 많이 줄어들기 때문에 꼭 트리 쉐이킹을 사용하세요! 단, commonJS나 AMD, UMD같은 모듈 시스템을 사용해야하는 클라이언트에서는 쓰면 제대로 처리되지 않습니다.
useBuiltIns: 'entry'는 위에 core-js를 언급할 때 설명했습니다. entry 파일 최상단에 넣어둔 import 'core-js/stable'과 import 'regenerator-runtime/runtime'을 자신의 target에 맞게 바꾸어줍니다. 'entry' 외에 'usage'와 false도 있는데 개인적으로 'usage'를 제일 선호합니다. 'usage'는 알아서 사용코드를 파악하여 폴리필을 import해줍니다. entry 파일 최상단에 import문을 적을 필요가 없습니다. false는 entry 최상단에 넣은 import문을 그냥 그대로 사용합니다. 즉 환경에 따라 polyfill을 다르게 적용하지 않는 것입니다.
알아서 폴리필을 추가하겠다는 @babel/preset-env 컨셉 상 usage가 제일 적합해보입니다.
위의 예시는 서버에서 웹팩을 사용할 때의 예시이기 때문에 클라이언트 상황을 알아보려면 다음 강좌 를 참고하세요!
혹시 다른 사이트에서 rules나 use 대신 loaders를 쓰고, options 대신 query를 쓰는 곳이 있다면, 웹팩1에 대한 강좌입니다. 웹팩2에서 속성명이 바뀌었습니다. 이제는 그렇게 쓰면 에러가 발생합니다.
위와 같이하면 test 정규식조건(js나 jsx 파일)에 부합하는 파일들을 loader에 지정한 로더가 컴파일해줍니다. options는 로더에 대한 옵션으로 아까 설치한 presets들을 적용하고 있는 게 보입니다. exclude는 제외할 폴더나 파일로, 바벨로 컴파일하지 않을 것들을 지정해줍니다. 보통 node_modules를 exclude합니다. node_modules 내부의 소스는 대부분 라이브러리가 배포될 때 이미 컴파일되어 있거든요. 바벨로는 컴파일하지 않지만 웹팩으로는 번들(포함)합니다. 반대로 include로 꼭 이 로더를 사용해서 컴파일할 것들을 지정해줄 수도 있습니다.
웹팩2에서 변경된 점은 loader 옵션에 babel 대신 babel-loader 전체 이름을 적어주어야 한다는 점(resolve 옵션에서 적절히 세팅한다면 예전처럼 babel만 쓸 수 있긴 합니다)과 json-loader가 내장되어 따로 적어줄 필요가 없다는 점입니다.
plugins
플러그인은 약간 부가적인 기능입니다. 다양한 플러그인들이 나와있는데 이를 사용하면 효과적으로 번들링을 할 수 있습니다. 예를 들면 압축을 한다거나, 핫리로딩을 한다거나, 파일을 복사하는 등의 부수적인 작업을 할 수 있습니다. 다양한 플러그인들이 패키지로 존재하기 때문에 쇼핑하듯 골라보세요!
{
plugins: [
new webpack.LoaderOptionsPlugin({
minimize: true,
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'), // 아래 EnvironmentPlugin처럼 할 수도 있습니다.
}),
new webpack.EnvironmentPlugin({ 'NODE_ENV': 'production' }), // 요즘은 DefinePlugin보다 이렇게 하는 추세입니다.
],
}
대표적인 웹팩 기본 제공 플러그인들입니다. LoaderOptionsPlugin은 로더들에게 옵션을 넣어주는 플러그인이고요. ModuleConcatenationPlugin은 웹팩3에서 새로 나왔는데 웹팩이 변환하는 자바스크립트 코드를 조금이나마 더 줄여줍니다. UglifyJsPlugin이 압축, console 제거, 소스맵 보존 등을 하는 플러그인이고, DefinePlugin은 JS 변수를 치환해주는 플러그인입니다. 참고로 UglifyJsPlugin은 es6 이상의 코드를 컴파일하지 못하는 버그가 있기 때문에 uglifyjs-webpack-plugin을 직접 npm에서 설치하여 대신 사용하면 됩니다. 이외에도 BannersPlugin, IgnorePlugin, EnvironmentPlugin, ContextReplacementPlugin 등 기본 제공 플러그인도 어마어마합니다.
웹팩3에서 플러그인들의 변경점이 있습니다. DedupePlugin은 사라졌고, OccurrenceOrderPlugin은 기본으로 켜져 있으니 더 이상 추가하지 마세요.
웹팩4에서는 ModuleConcatenationPlugin과 UglifyJsPlugin, NoEmitOnErrorsPlugin, NamedModules 플러그인이 모두 사라지고 wepback.config.js의 optimization 속성으로 대치되었습니다.
optimization
웹팩4에서 최적화 관련 플러그인들이 모두 이쪽 속성으로 통합되었습니다. 나중에 나오는 CommonsChunkPlugin도 사라지고 여기에 병합되었습니다.
{
optimization: {
minimize: true/false,
splitChunks: {},
concatenateModules: true,
}
}
예를 들자면 minimize가 UglifyJsPlugin을 계승하고, splitChunks가 CommonsChunkPlugin을 계승합니다. 또한 mode가 production일 때는 자동으로 이 두 속성이 켜집니다. concatenateModules 옵션은 ModuleConcatenationPlugin을 계승합니다.
용량 관계로 다음 강좌에서 계속 이어집니다! 이번 시간에 주로 js를 번들링하는 방법을 살펴봤다면, 다음 시간에는 css랑 기타 파일들 번들링 방법을 알아보겠습니다.