Webpack을 서비스에 적용해 보기위한 시도

이전에 Webpack과 관련된 글을 쓴것 처럼 패기를 가지고 시니어 개발자님을 설득해 보았으나 설득하지 못했습니다. 제가 생각해도 미흡한 점들이 많았고 설득할 만한 요소를 가지고 주장했다고 볼 수 없었기 때문입니다. 하지만 일부 소규모 서비스에 webpack을 적용해 봐도 괜찮다는 의견을 듣게 되었어요! Yay!

Yay!

제가 바꾸고자 하는 서비스에서 CSS Preprocessor를 컴파일 할 때는 gulp, Javascript 파일을 minify 할 때는 grunt 등 상황에 따라 실행해야 하는 task runner가 달라서 헷갈리기도 했고 webpack을 공부한 김에 하나로 통합하고 정리하는 작업을 하게 되었습니다. 서비스에 맞게 사용해야 했기 때문에 webpack 가이드를 전체적으로 다 훑어 보았어요.

처음 webpack 기본 튜토리얼을 접하면서 낯선 사람을 만난 것 처럼 어색했는데 서비스에 어떻게 적용시켜 볼까 고민을 하고 해결해 나가는 과정을 겪다 보니 예전보다는 친해진 기분이 들었습니다(아직 더 친해져야 하지만;;). 진행을 하면서 제가 고민했던 대표적인 두가지 요소는 다음과 같아요.

  1. CSS코드도 함께 번들링하는 것은 좋은데 javascript파일을 실행하는 것에 따라서 스타일 시트를 적용하는것은 과연 좋은 것일까? 렌더링도 지연 될수 있고, 심하면 FOUC문제도 일어날 수 있을텐데…
  2. 어플리케이션 코드에 비해 라이브러리 코드는 자주 바뀌지 않는데 함께 다 묶어서 번들러로 만드는 작업은 불필요하지 않을까?

그리고 각각 고민을 어떤식으로 해결해나갔는지 설명해보도록 하겠습니다.

1. CSS코드도 함께 번들링하는 것은 좋은데 javascript파일을 실행하는 것에 따라서 스타일 시트를 적용하는것은 과연 좋은 것일까? 렌더링도 지연 될수 있고, 심하면 FOUC문제도 일어날 수 있을텐데…

→ 이 고민은 Code Splitting-CSS 로 해결하였어요.

module.exports = {
module: {
rules: [{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
}]
}
}

위와 같이 css-loader와 style-loader를 이용해서 설정을 하면 javascript와 함께 번들러로 만들어 지는데, 스타일을 위해 자바스크립트 번들이 모두 로드될때까지 대기하는 방식이 과연 좋은 방법일까라는 생각이 들었습니다. 실제로 webpack 문서를 찾아보니 이 방식은 비동기식 및 병렬적으로 CSS를 로드를 할 수 없는 단점이 있다고 해요. 그럴 경우 렌더링이 지연되거나 심하면 FOUC 문제가 생길 수 있다고 합니다. 이럴 경우 ExtractTextPlugin을 사용하여 css 코드를 따로 분리해서 번들링 하면 된다고 가이드라인에 명시되어 있었습니다. 아래와 같이 코드를 수정하게 되었어요.

const ExtractTextPlugin = require('extract-text-webpack-plugin');
module: {
rules: [
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [ 'css-loader', 'less-loader' ]
})
},
]
},
new ExtractTextPlugin('../css/index.css'),

webpack을 실행하면 아래와 같은 메시지를 볼 수 있었습니다.

2. 어플리케이션 코드에 비해 라이브러리 코드는 자주 바뀌지 않는데 함께 다 묶어서 번들러로 만드는 작업은 불필요하지 않을까?

→ 이 고민은 Code Splitting-Library으로 해결하였어요.

var path = require('path');

module.exports = {
entry: './app/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};

이런식으로 코드를 적다 보니 이런 의문점이 들었습니다. 라이브러리 코드는 자주 바뀌지 않는데 매번 이렇게 빌드해서 번들러로 만들어 줘야 하나?

보통 어플리케이션은 서드파티 라이브러리를 사용하게 되는데, 특정 버전의 라이브러리를 쓸경우 코드가 자주 바뀌지 않지만 어플리케이션 코드는 자주 바뀌게 되는데요. 서드파티 코드와 함께 어플리케이션 코드를 번들링하는것은 비효율적인 작업입니다. 브라우저는 캐시 헤더에 기반하여 asset file를 캐시할 수 있고, 파일들은 해당 콘텐츠가 변경되지 않았다면 cdn을 다시 호출할 필요 없이 캐시가 가능해요. 이러한 이점때문에, 어플리케이션 코드의 변경에 상관없이 vendor file의 hash를 일정하게 유지 해야 합니다. 이러한 고민을 해결하기 위해 일단 webpack가이드에 나온 것 처럼 어플리케이션코드를 위한 번들과 라이브러리코드를 위한 번들을 아래와 같이 나누는 작업을 했어요!

entry: {
js_custom: './entry.js',
js_lib: ["jquery","bootstrap", "underscore"],
},

위에 코드로만 돌리면 js_custom.js 와 js_lib.js 이렇게 두개의 번들 파일이 만들어 지는 것을 볼 수 있지만 entry.js에서 underscore를 require한다면 번들링된 js_custom파일에도 js_lib파일에도 라이브러리 코드가 모두 보입니다.아래와 같이요!

js_lib.js
js_cusom.js

각 entry point는 의존성을 가지고 있는 것들도 함께 묶어버리기 때문입니다. 이러한 문제를 해결해기 위해 아래와 같이 CommonsChunkPlugin 을 사용하게 되었어요.

new webpack.optimize.CommonsChunkPlugin({
name: 'js_lib', // Specify the common bundle's name.
minChunks: function (module) {
return module.context && module.context.indexOf('node_modules') !== -1;
}
}),

CommonsChunkPlugin는 이름처럼 공통(common)으로 사용되는 모듈을 빼내어서 공통(common) 번들에 추가하고, 공통 번들이 존재하지 않으면 새로운 번들을 만들어 줍니다. 이렇게 하면 위에 코드에서 name에 js_lib라고 명시해 준것 처럼 js_lib 파일에만 라이브러리 코드가 존재하는 것을 볼 수 있습니다. 여기까지 하면 고민을 해결할수 있다고 생각했는데! 또 살펴 봐야 할 것이 있더군요!

파일을 분리 시켰는데도 불구하고 js_custom 코드를 즉 어플리케이션 코드를 변경하고 webpack을 다시 돌리면 js_lib file의 hash값이 바뀌어 있는 것을 볼 수 있었습니다. 이렇게 js_lib 파일의 해시가 매번 빌드 할 때마다 바뀌게 되면, 브라우저가 그 파일을 재로딩 해야 하기 때문에 브라우저 캐시의 이점을 얻지 못하게 됩니다.

1차
custom 코드를 변경 한 후 lib코드의 hash 값이 바뀌어 있음.

매번 build 할 때마다, webpack이 제대로 일할수 있도록 runtime code가 만들어 지는데 번들 파일이 하나일때는 그 안에 생기지만, 여러개의 번들 파일이 있는 경우 공통 모듈이 있는 js_lib 파일에 runtime code가 들어가게 됩니다.그런이유로 매번 hash값이 바뀌게 된다고 합니다.

이러한 문제를 해결하기 위해 아래와 같이 manifest파일을 따로 하나 더 만들었어요! 그렇게 되면 runtime code는 manifest.js 파일로 따로 분리가 되고 브라우저 캐시의 이점을 얻을 수 있습니다.

new webpack.optimize.CommonsChunkPlugin({
name: 'js_lib', // Specify the common bundle's name.
minChunks: function (module) {
return module.context && module.context.indexOf('node_modules') !== -1;
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest'
})

앗 그리고 앞에 설명을 안했는데 webapck에서는 아래와 같이 간단한 설정 만으로 각각의 번들 파일에 유니크한 hash값을 만들어 낼 수 있어요.

output: {
filename: '[name].[chunkhash].min.js',
path: path.join(__dirname, '../public/business/js/')
},

또한 코드가 수정되었을 경우 hash값이 바뀌게 되는데 이런식으로 html에 매번 적어주기 불편합니다. 아래와 같이 말이죠.

<script src="js_lib.50cfb8f89ce2262e5325.js"></script>
<script src="js_custom.70b594fe8b07bcedaa98.js"></script>

그래서 이 문제를 해결하기 위해 webpack-manifest-plugin 을 사용했습니다.

const ManifestPlugin = require('webpack-manifest-plugin');
module: {
rules: [
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [ 'css-loader', 'less-loader' ]
})
},
]
}
plugins: [
new ManifestPlugin(),
]

webpack을 실행하면 ManifestPlugin으로 인해 아래와 같이 manifest.json파일이 생성되고 최신 번들링된 파일의 이름으로 업데이트가 됩니다. 단 서버에서 조금 설정을 해줘야 해요.

{
"js_custom.css": "../css/index.css",
"js_custom.js": "js_custom.75fc747f21fcc02dd6c7.min.js",
"js_lib.js": "js_lib.c1580831b39d59615228.min.js",
"manifest.js": "manifest.5bbdaa6efdd3e9b39722.min.js"
}

두번째 고민은 이렇게 해결했던 것 같아요.

마지막으로..

제가 작업한 부분의 최종본 일부는 아래와 같아요.

const webpack = require('webpack');
const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');

module.exports = {
entry: {
js_custom: './entry.js',
js_lib: ["jquery", "bootstrap", "underscore"],
},
output: {
filename: '[name].[chunkhash].min.js',
path: path.join(__dirname, '../public/b/js/')
},
devtool: "cheap-module-eval-source-map",
resolve: {
modules: ["./app/node_modules"]
},
module: {
rules: [
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [ 'css-loader', 'less-loader' ]
})
},
]
},
plugins: [
    new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery"
}),
new ManifestPlugin(),
new UglifyJSPlugin({
comments: false
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'js_lib',
minChunks: function (module) {
return module.context && module.context.indexOf('node_modules') !== -1;
}
}),

new ExtractTextPlugin('../css/index.css'),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest'
})
]
}

적다 보니 제가 고민했던 부분이 code splitting과 관련된 내용이었던 것 같습니다. 현재 실제 코드는 코드리뷰를 받는 중이에요. 고민했던 부분은 저의 방식으로 해결해 보았지만 아직 webpack에 대해 제대로 한다고 할 수 있는 수준이 아니라서 좀더 친해 졌으면 좋겠네요. ㅠㅠ

이후 글은 dev환경과 prod환경일 경우의 webpack config 파일을 어떻게 구성할 것인지에 고민을 했던 내용과 해결했던 방법을 글을 쓰려고 합니다.

그럼 뿅!

reference : https://webpack.js.org/guides/

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.