웹팩 2, 웹팩 1, 바벨, 리액트 등등에 대해서 기본적인 부분은 설명하지 않는다. 또한 (Webpack 2) 트리 쉐이킹을 해보자!를 보고 나서 이 포스팅을 읽는 걸 추천한다.
코드를 왜 분할하지?
SPA(Single Page Application)은 한 번에 모든 리소스를 로딩해서 초기 로딩 이후에 페이지 이동이 매우 빠르다는 장점을 가지고 있다. 하지만 앱의 규모가 커지면 모든 리소스를 한 번에 로딩하므로 초기 로딩이 느려져 사용자 이탈을 유발하는 양날의 검을 가지고 있다.
1단계: 내 코드와 서드 파티(라이브러리/프레임워크) 코드를 분리해보자.
HTTP 1.1 프로토콜은 2개의 http 요청을 병렬로 수행하게 돼있지만, 모던 브라우저는 4개의 http 요청을 병렬로 수행한다. 아래 링크를 참조하자. 브라우저의 리소스 병렬 다운로드를 가로막는 자바스크립트 | 감성 프로그래밍 따라서 내 코드와 서드 파티 코드를 동시에 다운로드 받으면 더 빠른 로딩이 가능하다. 따라서 내 코드(app)와 서드 파티 코드(vendor)를 하나의 bundle.js에서 분리시키는 단계가 필요하다. 리액트 대신에 다른 서드 파티로 진행해도 무방하다.
일단 프로젝트를 생성하자.
1 2 3
npm init --y npm i -S react react-dom npm i -D babel-core babel-preset-env babel-preset-react babel-loader webapck
소스 코드는 src 폴더를 만들어 그 안에서 관리하도록 하겠다. 엔트리의 진입점인 main.js를 만들자.
module.exports = { entry: { app: './src/main.js', // 아래와 같이 수동적으로 서드 파티들을 다 추가해줘야한다. // 장점으로는 자기가 빼고 싶은 서드 파티만 지정할 수 있다는 점이다. // 자신의 앱과 벤더의 크기를 균형있게 맞출 수가 있다. vendor: ['react', 'react-dom'] }, output: { // entry에 존재하는 app.js, vendor.js로 뽑혀 나온다. filename: '[name].js', path: './dist/', }, plugins: [ new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false, // 터미널 창에 출력되는 게 보기 귀찮아서 추가. unused: true// tree shaking } }),
// 로더들에게 옵션을 넣어주는 플러그인이다. new webpack.LoaderOptionsPlugin({ minimize: true }),
모든 사용자가 우리 앱의 모든 페이지를 돌아다니지 않는다. 하지만 app.js에는 우리 앱의 모든 페이지 코드가 담겨있다. 라우터를 통해 구분했던 페이지대로 코드를 분할시켜보자!
hash vs chunkhash
hash가 뭐지?? 기본적으로 브라우저에는 임시 파일, 캐시 데이터라고 불리는 임시 저장공간이 존재한다. 이 임시 저장공간은 자신의 하드 공간의 일부에 해당한다. 브라우저 속성에서 찾아보면 나올 것이다. 만약 파일에 대한 요청이 있으면 처음에는 웹서버에서 다운 받고 임시 저장공간에 저장한다. 하지만 동일한 요청이 또 오면 웹서버를 거치지 않고 하드에 있는 임시 저장공간에서 뒤져서 해당 파일을 응답해줘서 더 빠른 응답을 하기 위한 기법이다. 하지만 파일의 내용이 바뀌었는데도 임시 저장 공간에 있는 내용을 내려줘서 변경된 파일이 보이지 않아 당황한 적이 많을 것이다. 이렇게 파일이 변경 됐음에도 반영되지 않는 걸 방지하고자 파일 이름에 hash라는 걸 붙이는 방법이다. hash는 복잡한데 그냥 암호화된 문자라고 대충 생각하면 될 것 같다. 하지만 파일이 변경되지 않았을 때도 계속해서 다른 해쉬를 생성해서 캐시 데이터의 장점을 전혀 이용할 수가 없다. 이래서 나온 게 chunkhash다.
chunkhash 짱짱맨! chunkhash는 해당 파일이 변경 됐을 때만 파일에 hash를 바꿔서 저장하는 것이다. 즉 파일이 바뀌지 않았으면 똑같은 파일 이름에 대한 요청이므로 캐시 데이터를 쓰고, 파일이 바뀌었으면 다른 해쉬가 파일 이름에 들어가 웹서버에 새로 요청해서 수정된 내용을 즉각적으로 볼 수 있는 것이다. 그렇담 chunk는?? 나도 잘 모르는데 그냥 페이지 별로 소스를 나눈 게 청크인 것으로 안다.
hash의 사용법은 어렵지 않으므로 chunkhash만 설명하겠다. 일단 chunkhash를 테스트하기 위해 리액트 라우터를 설치하자. 또한 HTML5의 History API(리액트 라우터의 browserHistory)를 사용하기 위해 node.js의 http 모듈을 사용해서 서버를 띄워보자. 쌩으로 코딩하면 번거로우니까 express 모듈을 사용하도록 하자. react-router v4는 너무 변경사항이 많아서 일단은 3 버전을 토대로 설명한다.
import(module) Dynamic import 귀찮다. 바벨 플러그인(babel-plugin-syntax-dynamic-import)을 설치하고 설정해줘야 한다. 청크의 이름을 지정할 수 없다. 하지만 오류가 났을 때 catch()를 써서 처리 할 수 있다는데, 뭐 그렇게 처리할만한 상황이 얼마나 있을까 싶다.
require.ensure 다른 거 설치 안 해도 되고, 청크의 이름을 지정할 수 있다. 이 포스팅에서는 3번을 통해 청크 스플리팅을 해보겠다.
render( // HTML5의 History API를 쓰기 위해 hashHistory 대신에 browserHistory를 사용하였다. <Routerhistory={browserHistory}> // component 대신에 getComponent를 사용하는 점을 주목하자. <Routepath="/"getComponent={(location,callback) => { // 아래 코드 부분이 핵심이다. // [] 부분 안에 디펜던시가 들어간다는데 언제 쓰게 되는지는 잘 모르겠다. require.ensure([], (require) => { callback(null, require('./Comp').default); // 두 번째 인자로 청크의 이름이 들어간다. }, 'Comp'); }} /> <Routepath="/aa/bb/cc"getComponent={(location,callback) => { require.ensure([], (require) => { callback(null, require('./Comp2').default); }, 'Comp2'); }} /> </Router>, document.getElementById('app') );
이번엔 index.html를 수정하자. 어떤 청크해쉬가 들어갈지 모르므로 script 태그를 빼버렸다.
module.exports = { entry: { app: './src/main.js', }, output: { // 요 놈은 저 위에 엔트리의 app에 대한 내용 filename: '[name].[chunkhash].js', // 요 놈은 페이지 별 청크에 대한 내용 chunkFilename: '[name].[chunkhash].js', path: `./dist`, }, plugins: [ new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false, // 콘솔 창에 출력되는 게 보기 귀찮아서 추가. unused: true// tree shaking } }),
// 로더들에게 옵션을 넣어주는 플러그인이다. new webpack.LoaderOptionsPlugin({ minimize: true }),
// app.js에 들어갈만한 내용을 vendor로 빼주는 플러그인 new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: function (module) { // this assumes your vendor imports exist in the node_modules directory returnmodule.context && module.context.indexOf('node_modules') !== -1; }, // 요 놈은 vendor에 대한 내용 fileName: '[name].[chunkhash]' }),
module.exports = { entry: { app: './src/main.js', }, output: { // 요 놈은 저 위에 엔트리의 app에 대한 내용 filename: '[name].[chunkhash].js', // 요 놈은 페이지 별 청크에 대한 내용 chunkFilename: '[name].[chunkhash].js', path: './dist', // HTML5의 History API를 쓰다보면 라우터가 // http://localhost/aa/bb/cc 와 같이 뎁스가 깊어지는데 // 그럴 때 js 파일은 localhost를 기준으로 잡아야하므로 // 루트를 기준으로 잡아준 것이다. publicPath: '/' }, plugins: [ new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false, // 콘솔 창에 출력되는 게 보기 귀찮아서 추가. unused: true// tree shaking } }),
// 로더들에게 옵션을 넣어주는 플러그인이다. new webpack.LoaderOptionsPlugin({ minimize: true }),
// app.js에 들어갈만한 내용을 vendor로 빼주는 플러그인 new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: function (module) { // this assumes your vendor imports exist in the node_modules directory returnmodule.context && module.context.indexOf('node_modules') !== -1; }, // 요 놈은 vendor에 대한 내용 fileName: '[name].[chunkhash]' }),
// 브라우저의 콘솔 창에 프로덕션 모드로 빌드하라는 오류가 뜨는데 그걸 없애주는 플러그인 new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } }),
// htmlWebpackPlugin을 쓰면 html 파일 복사 및 js, css inject를 할 수 있다. // 물론 minify도 가능하다. newHtmlWebpackPlugin({ template: './src/index.html' }) ], module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', options: { presets: [ [ "env", { browsers: ['last 2 versions', '> 10%', 'ie 9'], // tree shaking "modules": false } ], "react" ] } } ] } };
해쉬가 정상적으로 붙어서 들어갔고, vendor가 먼저 들어갔고, 다른 페이지 청크는 들어가지 않았다. 또한 루트 디렉토리를 뜻하는 /도 정상적으로 들어가있다. HTML5의 History API를 확인해보려면 실제 서버를 띄워야하므로 서버 코드를 작성해보자. server.js를 프로젝트의 최상위 디렉토리에 만들자.