Aria Fallah씨는 Webpack을 시작하기가 쉽지만은 않았다고 합니다. 그래서 친절하고 개괄적인 초보자용 Webpack 입문서를 만들었습니다. 그는 이 튜토리얼을 통해 Webpack의 사용법을 쉽게 배울 수 있기를 바란다고 했습니다.

1-1. Webpack을 왜 사용하나요?

여기에 Webpack을 사용해야 할 몇 가지 현실적인 이유가 있습니다.

  • 하나의 파일로 js 파일을 번들할 수 있습니다.
  • 프론트엔드 코드에 npm 패키지를 사용할 수 있습니다.
  • ES6/ES7 자바스크립트 코드를 작성할 수 있습니다. (Babel을 이용하여)
  • 코드를 압축 또는 최적화할 수 있습니다.
  • LESS/SCSS를 CSS로 돌릴 수 있습니다.
  • HMR(Hot Module Replacement)을 사용할 수 있습니다.
  • 자바스크립트로 모든 유형의 파일을 포함할 수 있습니다.
  • 이 글에서 다루지 못한 아주 많은 고급기능이 있습니다.
왜 이러한 기능이 필요한가요?
  • js 파일 번들 - 자바스크립트를 모듈로 작성할 수 있습니다, 그래서 각각의 파일에 대해서 <script> 태그를 별도로 작성할 필요가 없습니다. (상황에 따라서 둘 이상의 js 파일이 필요한 경우 구성 가능함)
  • npm 패키지 사용 - npm은 인터넷상에서 오픈소스 코드의 커다란 생태계입니다. npm 코드를 저장할 기회가 주어지며, 원하는 프론트엔드 패키지를 가져다 쓸 수 있습니다.
  • ES6/ES7 - 많은 기능을 추가되어 더 강력하고 더 쉽게 자바스크립트를 작성할 수 있습니다. 여기에 소개하는 글이 있습니다.
  • 코드 압축/최적화 - 배포되는 파일의 크기를 줄입니다. 페이지 로딩이 빨라지는 등의 장점을 포함합니다.
  • LESS/SCSS를 CSS로 돌리기 - CSS를 작성하는 더 좋은 방법입니다. 여기에 소개하는 글이 있습니다.
  • HMR 사용 - 생산성이 향상됩니다. 코드를 저장할 때 마다 페이지의 리프레시가 자동으로 이루어집니다. 코드를 작성하는 동안 페이지의 상태를 최신으로 유지해야 하는 경우 정말 편리합니다.
  • 자바스크립트로 모든 유형의 파일을 포함 - 추가적인 빌드 도구의 수를 줄일 수 있고, 프로그램적으로 파일을 사용 및 수정할 수 있습니다.

1-2. 기본 익히기

1-2-1. 설치하기

Webpack의 모든 기능을 사용하려면 전역으로 설치해야 합니다:

npm install -g webpack

그러나 Webpack의 일부 기능이나 최적화 플러그인 정도만 필요한 경우라면 로컬에 설치합니다:

npm install --save-dev webpack
실행 명령

Webpack을 실행하려면:

webpack

Webpack에서 파일의 상태가 변경되면 자동으로 빌드하려는 경우:

webpack --watch

특정한 이름의 사용자가 정의한 Webpack 설정 파일을 사용하려면:

webpack --config myconfig.js

1-2-2. 번들하기

예제 1

Official Dependency Tree

Webpack은 공식적으로 모듈 번들러라고 합니다. 다음의 두 가지 훌륭한 글은 모듈 액세스에 대한 깊이 있는 설명과 명확한 모듈 번들링에 대하여 다루고 있습니다: 이것이것.

간단하게 봅시다. 작동시키는 방법은 하나의 파일을 진입점으로 지정하는 것입니다. 이 파일은 트리의 루트가 될 것입니다. 그러면 require에 의해 다른 파일이 트리에 추가됩니다. webpack 명령을 실행하면, 모든 파일과 모듈은 하나의 파일에 번들로 제공됩니다.

다음은 간단한 예제입니다:

Dependency Tree

이 그림은 다음과 같은 디렉터리 구조를 가진다고 가정합니다:

MyDirectory
|- index.js
|- UIStuff.js
|- APIStuff.js
|- styles.css
|- extraFile.js

이것은 파일의 내용입니다.

// index.js
require('./styles.css')
require('./UIStuff.js')
require('./APIStuff.js')

// UIStuff.js
var React = require('React')
React.createClass({
  // stuff
})

// APIStuff.js
var fetch = require('fetch') // fetch polyfill
fetch('https://google.com')
/* styles.css */
body {
  background-color: rgb(200, 56, 97);
}

webpack 명령을 실행하면, 이 트리의 내용을 번들로 얻을 수 있겠지만, 같은 디렉터리에 있는 extraFile.jsrequire에 참조되지 않았기 때문에 결코 번들의 일부가 되지 않습니다.

bundle.js는 다음과 같이 표시됩니다:

// contents of styles.css
// contents of UIStuff.js + React
// contents of APIStuff.js + fetch

즉, 번들로 제공되는 것들은 파일의 참조를 통한 경우의 것들입니다.

1-2-3. 로더란?

이미 눈치챘겠지만, 위의 예제에서 이상한 일을 저질렀습니다. 바로 CSS 파일을 자바스크립트 파일에 require를 사용한 것입니다. 이것은 정말 멋집니다, Webpack의 흥미로운 점은 require에 자바스크립트 파일 말고도 다른 것을 더 할 수 있다는 것입니다.

Webpack에는 로더라는 것이 있습니다. 이 로더를 사용하면, require를 이용하여 .css.html, .png 등을 불러올 수 있습니다.

위 그림의 예를 들어 보겠습니다.

// index.js
require('./styles.css')

Webpack의 구성에 스타일-로더CSS-로더를 포함하는 경우, 이것은 단지 완전히 유효하지만 않을 뿐, 실제로는 페이지에 CSS를 적용하게 됩니다.

이것은 Webpack과 함께 사용할 수 있는 수많은 로더들 중 하나의 사용 예제일 뿐입니다.

1-2-4. 플러그인

플러그인은 이름에서 알 수 있듯이, Webpack에 사용할 수 있는 추가 기능입니다. 자주 사용하는 플러그인 중 하나는 UglifyJsPlugin입니다. 이는 자바스크립트 코드를 압축(minify)해 줍니다. 이 사용법에 대해서는 나중에 다룰 것입니다.

1-3. 설정 파일 구성

Webpack은 박스(?) 밖에서 작동하지 않기 때문에 필요에 맞게 작성해야 합니다. 이를 위해 다음과 같은 파일을 생성할 수 있습니다.

webpack.config.js

이것은 Webpack이 기본적으로 인식하는 파일명입니다. 다른 이름을 사용하려면 해당 파일의 이름을 지정할 수 있는 --config 플래그를 사용해야 합니다.

1-3-1. 최소한의 예제

예제 2

디렉터리 구조는 다음과 같습니다:

MyDirectory
|- dist
|- src
   |- index.js
|- webpack.config.js

다음으로 최소한의 Webpack 설정이 있습니다.

// webpack.config.js
var path = require('path')

module.exports = {
  entry: ['./src/index'], // file extension after index is optional for .js files
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  }
}

새롭게 보이는 속성을 각각 살펴봅시다:

  • entry - 번들의 엔트리 포인트로써 번들하기 색션에서 이미 논의했습니다. Webpack은 여러 번들을 생성하는 진입점을 허용하기 때문에 배열입니다.
  • output - Webpack의 최종 결과물이 되는 형태를 명시합니다.
    • path - 어디에 번들 파일을 위치시킬 것인지를 지정합니다.
    • filename - 번들 파일의 이름을 지정합니다.

이제 webpack 명령을 실행하면, dist라는 폴더에 bundle.js 파일을 생성합니다.

1-3-2. 플러그인 이해

예제 3

모든 파일의 번들에 Webpack을 사용했고 모두 합쳐서 900KB 짜리 파일을 얻었다고 가정해 봅시다. 덩치가 큰 문제는 번들 파일의 압축으로 개선될 수 있습니다. 이 작업을 수행하려면 앞서 언급했던 UglifyJsPlugin라는 플러그인을 사용합니다.

또한 실제로 플러그인을 사용할 수 있도록 Webpack을 로컬에 설치해야 합니다.

npm install --save-dev webpack

이제 Webpack에서 필요로 하는 코드를 압축할 수 있습니다.

// webpack.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },

  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    })
  ]
}

새롭게 보이는 속성을 각각 살펴봅시다:

  • plugins - 보유 중인 플러그인의 배열입니다.

이제, webpack 명령을 실행하면, UglifyJsPlugin에 의해 모든 공백을 제거하는 등의 과정을 거쳐 900KB 짜리 파일을 200KB로 줄일 수 있습니다.

또한 OccurrenceOrderPlugin을 추가할 수도 있습니다.

이 플러그인은 발생 횟수에 따라서 모듈 및 청크 id를 할당합니다. 자주 사용되는 id가 낮은(짧은) id를 얻습니다. 이 id는 예측(predictable)이 가능하며, 전체 파일 크기를 줄이는데(역자주: 파일 용량을 줄이는 것과는 무관함) 추천됩니다.

솔직히 말해서 기반 메커니즘이 어떻게 작동하는지 잘 모르지만, Webpack2 베타 버전에는 기본으로 포함 되어 있다고 합니다.

// webpack.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurenceOrderPlugin()
  ]
}

여기까지 자바스크립트의 번들을 압축하는 설정을 작성했습니다. 이 번들을 다른 프로젝트의 디렉터리에 붙여넣고 <script> 태그에 대입할 수 있습니다. 여기에서 결론으로 바로 넘어가도 좋습니다. 오직 자바스크립트 에 대한 기본적인 Webpack 사용법만 필요하다면 말이죠.

1-4. 조금 더 복잡한 예제

추가적으로, Webpack은 자바스크립트에 관련한 단순 작업보다 더 많은 일을 할 수 있으므로, 수동으로 복사-붙여넣기 하는 일을 없애고 Webpack으로 전체 프로젝트를 관리할 수 있습니다.

다음 섹션에서는, Webpack을 사용하여 아주 간단한 웹사이트를 만들 것입니다. 예제를 수행하고자 하는 경우, 다음과 같은 구조의 디렉터리를 생성하세요.

MyDirectory
|- dist
|- src
   |- index.js
   |- index.html
   |- styles.css
|- package.json
|- webpack.config.js
학습 내용
  1. 로더 이해하기 - 번들에 CSS를 추가할 수 있도록 로더를 추가해 볼 것입니다.
  2. 플러그인 추가하기 - HTML 파일을 생성하고 사용할 수 있도록 도와주는 플러그인을 추가해 볼 것입니다.
  3. 개발서버 구성하기 - developmentproduction을 구분한 Webpack의 구성 파일을 분할하고 webpack-dev-server를 이용하여 HMR을 활성화해 볼 것입니다.
  4. 코딩 시작하기 - 실제로 자바스크립트의 일부를 작성해 볼 것입니다.

1-4-1. 로더 이해하기

예제 4

이전 튜토리얼에서 로더에 대해 언급했습니다. 이제 자바스크립트가 아닌 파일을 다루어 보기로 하겠습니다. 스타일 로더와 CSS 로더가 필요하게 되었습니다. 먼저 로더를 설치해 봅시다:

npm install --save-dev style-loader css-loader

설치된 CSS 로더를 포함하도록 설정 파일을 조정해 봅시다:

// webpack.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurenceOrderPlugin()
  ],
  module: {
    loaders: [{
      test: /\.css$/,
      loaders: ['style', 'css']
    }]
  }
}

새롭게 보이는 속성을 각각 살펴봅시다:

  • module - 이 옵션은 파일에 영향을 줍니다.
    • loaders - 애플리케이션에서 사용할 로더를 배열로 지정합니다.
      • test - 정규식을 이용해서 로더에 사용될 파일을 검출합니다.
      • loaders - 일치하는 파일에 사용되는 로더를 호출합니다.

webpack 명령을 실행하면, .css로 확장자를 가진 파일을 require하는 경우, 이 파일은 stylecss 로더에 적용되고, 번들에 CSS가 추가됩니다.

로더를 가지고 있지 않은 경우, 다음과 같은 오류를 보게 될 것입니다:

ERROR in ./test.css
Module parse failed: /Users/Developer/workspace/tutorials/webpack/part1/example1/test.css
Line 1: Unexpected token {
You may need an appropriate loader to handle this file type.

선택사항

만약 CSS 대신 SCSS를 사용하는 경우 다음과 같이 실행해야 합니다:

npm install --save-dev sass-loader node-sass webpack

그리고 로더는 다음과 같이 작성되어야 합니다.

{
  test: /\.scss$/,
  loaders: ["style", "css", "sass"]
}

이 과정은 LESS도 비슷합니다.

중요한 것은 지정할 순서가 존재한다는 것입니다. 위의 예제에서 sass 로더에 가장 먼저 .scss 파일을 적용하고, 그다음으로 css 로더, 마지막에 style 로더에 적용합니다. 즉, 순서 패턴은 오른쪽에서 왼쪽으로 로더에 적용되는 것입니다.

1-4-2. 플러그인 추가하기

예제 5

이제 웹사이트의 스타일링을 위한 인프라를 구축했으니, 스타일을 적용할 실제 페이지가 필요하게 되었습니다. HTML 페이지를 생성하거나 기존의 것을 그대로 사용할 수 있는 html-webpack-plugin을 이용하여 이 작업을 수행할 수 있습니다. 여기서는 기존의 index.html를 사용할 것입니다.

먼저 플러그인을 설치합니다:

npm install --save-dev html-webpack-plugin@2

다음으로 설정을 추가 합니다.

// webpack.config.js
var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurenceOrderPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  module: {
    loaders: [{
      test: /\.css$/,
      loaders: ['style', 'css']
    }]
  }
}

이번에 webpack 명령을 실행하면, HtmlWebpackPlugin./src/index.html 지정하기 때문에 ./src/index.html 파일의 내용을 dist 폴더에 index.html파일로 생성할 것입니다.

index.html의 내용이 비어 있다면 기본 템플릿을 사용하기 때문에 아무런 문제가 없습니다. 하지만 그 내용을 채워 보도록 하겠습니다.

<html>
<head>
  <title>Webpack Tutorial</title>
</head>
<body>
  <h1>Very Website</h1>
  <section id="color"></section>
  <button id="button">Such Button</button>
</body>
</html>

이제 bundle.js의 내용을 HTML의 <script> 태그에 직접 넣지 않아도 됩니다. 이 플러그인이 자동으로 넣어주기 때문입니다. 만약 스크립트 태그에 직접 넣은 경우라면, 같은 코드를 두 번 로드하게 될 것입니다.

이 시점에서 styles.css에 몇 가지 기본 스타일을 추가해 봅시다.

h1 {
  color: rgb(114, 191, 190);
  text-align: center;
}

#color {
  width: 300px;
  height: 300px;
  margin: 0 auto;
}

button {
  cursor: pointer;
  display: block;
  width: 100px;
  outline: 0;
  border: 0;
  margin: 20px auto;
}

1-4-3. 개발서버 구성하기

예제 6

이제 실제로 브라우저에서 웹사이트를 볼 수 있도록, 지금까지 만들어진 코드를 제공하는 웹서버가 필요하게 되었습니다. 편리하게도, Webpack은 webpack-dev-server를 제공합니다. 로컬과 글로벌에 모두 설치합니다.

npm install -g webpack-dev-server
npm install --save-dev webpack-dev-server

개발 서버는 작업 된 웹사이트를 브라우저에서 바로 확인할 수 있어 매우 유용하며, 더 빠른 개발을 할 수 있습니다. 기본적으로 http://localhost:8080를 방문할 수 있습니다. 아쉽지만, 핫-리로드 기능은 박스(?) 밖에서는 작동하지 않아서 약간의 추가 구성이 필요합니다.

이 시점에서 개발용(development)과 제품용(production)을 구분해 보겠습니다. 이 튜토리얼은 간단함을 유지하고 있으므로 큰 차이는 없지만, Webpack의 단적인 기능 설정에 관한 예입니다. webpack.config.dev.jswebpack.config.prod.js를 호출할 수 있도록 합니다.

// webpack.config.dev.js
var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  devtool: 'cheap-eval-source-map',
  entry: [
    'webpack-dev-server/client?http://localhost:8080',
    'webpack/hot/dev-server',
    './src/index'
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  module: {
    loaders: [{
      test: /\.css$/,
      loaders: ['style', 'css']
    }]
  },
  devServer: {
    contentBase: './dist',
    hot: true
  }
}

바뀐점

  1. 개발 설정에서 지속해서 다시 구축하거나 최적화하는 일은 불필요한 오버헤드가 발생하기 때문에 생략합니다. 그래서 webpack.optimize 플러그인이 없습니다.
  2. 개발 설정은 개발 서버에 필요한 것만 작성합니다. 더 자세한 내용을 여기에서 볼 수 있습니다.

요약:

  • entry: 두 개의 새로운 엔트리 포인트는 HMR이 가능하도록 브라우저에 서버를 연결합니다.
  • devServer
    • contentBase: 브라우저에서 접근하는 파일의 위치입니다.
    • hot: HMR 사용 여부입니다.

제품용 설정의 구성은 별로 변경되지 않습니다.

// webpack.config.prod.js
var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  devtool: 'source-map',
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurenceOrderPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  module: {
    loaders: [{
      test: /\.css$/,
      loaders: ['style', 'css']
    }]
  }
}

또한, 개발용 구성과 제품용 구성 모두 새로운 속성을 추가했습니다:

  • devtool - 디버깅을 지원합니다. 오류가 발생하는 경우, 크롬 개발자 콘솔과 같은 도구를 이용하여 실수한 위치를 확인하는 데 도움됩니다. source-mapcheap-eval-source-map의 차이에 대해서는 문서를 읽고도 이해하기가 조금 어려웠지만, 확실히 알 수 있었던 것은 source-map이 제품용 모드에서 오버헤드가 많다는 점과, cheap-eval-source-map이 더 작은 오버헤드를 가지며, 이것은 단지 개발을 위한 것이라는 점입니다.

개발 서버는 다음과 같이 실행합니다.

webpack-dev-server --config webpack.config.dev.js

제품용 코드를 구축하기 위해서는 다음과 같이 실행합니다.

webpack --config webpack.config.prod.js

이와 같은 명령을 매번 입력하지 않아도 되도록 package.json에 약간의 기능을 작성하는 것으로 더 간단하게 명령을 수행할 수 있습니다.

설정에 scripts 속성을 추가합니다.

// package.json
{
  //...
  "scripts": {
    "build": "webpack --config webpack.config.prod.js",
    "dev"  : "webpack-dev-server --config webpack.config.dev.js"
  }
  //...
}

이제 더욱 간단하게 명령을 실행할 수 있습니다.

npm run build
npm run dev

npm run dev 명령을 실행하고 http://localhost:8080으로 이동하여 작업 된 웹사이트를 볼 수 있습니다.

노트: 이 부분을 테스트하는 동안 index.html 파일을 수정할 때 서버가 핫-리로드 되지 않는 것을 깨달았습니다. 이 문제에 대한 해결책은 html-reload에 있습니다. 이것은 Webpack 옵션에 대하여 조금 더 유용한 정보를 얻을 수 있어서 읽어보길 추천합니다. 너무 사소한 내용이고 튜토리얼을 너무 길게 쓰는 느낌이 들었기 때문에 별도로 구분했습니다.

1-4-4. 코딩 시작하기

예제 7

많은 사람이 Webpack에 당황해할 것 같습니다. 이유는 실제 작업용 자바스크립트 코드를 작성하기까지 여태것 공부했던 여러 과정을 모두 숙지해야 하기 때문입니다; 다행히도 이 튜토리얼의 클라이막스에 도달했습니다.

npm run dev 명령 실행 및 http://localhost:8080를 하지 않았으면 수행합니다. 개발 서버에 핫-리로드를 설정하는 것은 단순히 보기만을 위한 것이 아닙니다. 프로젝트 일부를 편집하고 저장하는 매시간 변경사항을 표시하도록 브라우저는 다시 로드합니다.

이제 이것을 프론트엔드에서 사용할 수 있는 방법을 보여주기 위해 몇몇 npm 패키지가 필요합니다.

npm install --save pleasejs

PleaseJS는 임의 색상 발생기입니다. 특정 버튼을 이용해 div의 색상을 변경해 보겠습니다.

// index.js

// Accept hot module reloading
if (module.hot) {
  module.hot.accept()
}

require('./styles.css') // The page is now styled
var Please = require('pleasejs')
var div = document.getElementById('color')
var button = document.getElementById('button')

function changeColor() {
  div.style.backgroundColor = Please.make_color()
}

button.addEventListener('click', changeColor)

흥미롭게도, HMR이 작동하려면 다음과 같은 코드를 포함해야 합니다:

if (module.hot) {
  module.hot.accept()
}

모듈 또는 상위 모듈에서요.

이제 마지막입니다!

노트: 예제를 통해 CSS를 적용하면서 꺼림칙한 부분은, CSS가 자바스크립트 파일에 있다는 사실입니다. 다른 파일에 CSS를 넣는 방법에 대한 자세한 설명을 별도로 작성했습니다. css-extract를 확인하세요.

1-5. 결론

축하합니다! div의 색상을 변경하는 버튼을 만들기까지 모두 학습했습니다! Webpack, 참 훌륭하죠?

Webpack은 처음 접한 모듈 번들러입니다. 그리고 매우 유용한 도구였습니다. 파트 1에서는 가장 일반적인 사용 사례를 다루었지만, 아직 ES6과 React를 연결하여 사용하는 방법에 대해서는 다루지 않았습니다.

나중에

  • 파트 2에서는 Webpack과 Babel을 함께 사용하여 ES6을 ES5로 transpile 하는 방법을 살펴보겠습니다.
  • 파트 3에서는 Webpack과 React + Babel을 함께 사용하는 방법을 살펴보겠습니다.

도움이 되었기를 바랍니다.

Comments

최근 들어 더욱 가속화되어 발전하는 자바스크립트 트렌드는 이제 지켜보는 것조차 버겁네요. 최근 신규 프로젝트의 개발환경을 구축하면서 최종적으로 반영된 구성에 관해서 얘기해 보겠습니다. 브라우저에서 Node.js의 모듈 인프라를 이용할 수 있게 하고 실시간으로 트랜스파일(Transpile)이 되도록 개발환경을 구성해 보았습니다. 이 개발환경에 적응하면서 느끼는 것은 "이제 브라우저에서도 자바스크립트로 개발하기가 정말 좋은 세상이 되었구나!" 입니다. 그래서 다른 프로젝트에도 이를 적용할 수 있도록 사내에 전파를 시도하면서 정리했던 내용 일부를 공유합니다.

우리는 Node.js를 기반으로 한 개발환경에 BrowserifyWebpack과 같은 모듈 번들러를 이용하여 자바스크립트로 작성된 기능의 모듈화를 효율적이고 안정적으로 구현한 바 있습니다. 그리고 개발환경에서는 와쳐(Watcher)를 이용하여 파일이 변동되면 번들링한 후 브라우저에서 새로 고침까지 자동으로 되도록 했습니다.

그러나 답답합니다. 코드량이 많아질수록 눈에 띄게 느려지는 번들링 시간과 마이너 픽스에도 꼭 거쳐야만 하는 이 과정은 이제 지긋지긋합니다. 그냥 예전처럼 브라우저에서 새로 고치면 즉시 그 결과를 확인하고 싶습니다. 그래서 이처럼 불편하고 느려터진 번들링 과정을 개발하는 동안에는 하지 않아도 되도록 하는 것이 컨셉이라 하겠습니다. 즉, 실시간 트랜스파일이 되도록 한다는 것은, import 또는 require 문을 브라우저에서 직접 사용할 수 있도록 하는 것입니다.

Requirements

개발환경을 구성하는 주요한 설치 요구사항입니다. 테스트 라이브러리나 태스크 매니저는 취향에 맞게 사용하면 됩니다.

  • Node.js - 실시간으로 트랜스파일이 가능한 개발환경의 기반이 됩니다.
  • Electron - 구글 크롬 브라우저에서 import(require)문을 직접 이용하는 데 필요합니다.
  • Babel - ES6, ES7, JSX등 차세대 자바스크립트 코드를 구사하기 위해 사용합니다.
  • ESLint - 코드 스타일을 안내해주고, 빈번히 발생하는 개발 실수를 줄여줍니다.
  • Webpack - 프로덕션 빌드 과정에서 모듈 패키징에 사용됩니다.
  • Jest - 자바스크립트 유닛 테스트의 고통을 덜 수 있습니다.
  • Nightwatch.js - 구글 크롬 외 다른 브라우저에서의 작동 여부를 테스트하고 자동화를 위해 사용합니다.
  • Grunt - 이 모든 과정을 수월하게 관리할 수 있도록 도와주는 태스크 매니저입니다.

노트: Electron을 개발환경에 적합하다고 판단한 이유가 하나 더 있습니다. Node.js에 내장된 V8 버전(가장 최근에 나온 Node.js 5.1.1의 V8 버전은 4.6) 보다 Electron에 내장된 V8 버전이 4.8로 한참 앞서있기 때문입니다. 또한, V8의 블로그를 보면 ECMAScript 2015(ES6) 스펙을 구현하는 작업이 한창인 것을 알 수 있는데, 안정적인 버전의 크롬이 나오면 1, 2주 내로 Electron에 반영되는 장점은 덤입니다.

Configuration

이제 설정 파일을 작성해 봅시다. React와 jQuery를 사용하는 웹앱을 만든다고 가정합니다. 임의의 프로젝트 폴더에 package.json을 비롯한 프로젝트를 구성하는 파일과 폴더를 생성합니다.

$ mkdir my-project
$ cd my-project
$ touch .babelrc
$ touch .eslintrc
$ touch .gitignore
$ touch package.json
$ touch main.js
$ touch index.html
$ touch LICENSE.md
$ touch README.md
$ mkdir scripts
$ mkdir src
$ cd src
$ touch index.js

.babelrc Babel 트랜스파일러에서 사용할 플러그인을 지정합니다. 일반적으로 프리셋(babel-preset-*)을 사용하지만, 프리셋은 아주 많은 Babel 플러그인들을 포함하고 있어서 실시간으로 트랜스파일 하는 데에는 적합하지 않습니다. 오래 걸리기 때문입니다. 아래의 구성은 Electron 현재 버전 0.36.x(Node.js 5.1.1, 구글 크롬 47)에 내장된 V8 버전 4.7에서 ES2015 스펙이 미구현 되거나 일부만 구현되어 필요하게 된 플러그인을 개별적으로 로드하는 내용입니다. 이렇게 했을 때 프리셋을 사용할 때보다 매 새로 고침 마다 6초에서 4초 정도 시간을 절약할 수 있습니다. 보통 2초 정도면 페이지 로드가 완료됩니다.

노트: Node.js의 require를 훅(Hook)하는 babel-register는 캐시(Cache) 옵션이 기본으로 활성화되어 있습니다. 첫 로딩보다 그 다음 로딩이 훨씬 빠릅니다.

{
  "plugins": [
    "transform-es2015-destructuring",
    "transform-es2015-for-of",
    "transform-es2015-modules-commonjs",
    "transform-es2015-object-super",
    "transform-es2015-parameters",
    "transform-es2015-shorthand-properties",
    "transform-object-rest-spread",
    "transform-react-jsx"
  ]
}

노트: sticky-regexunicode-regex 그리고 typeof-symbol 플러그인은 테스트를 통과하지 못해 로드하지 않았습니다. 그리고 크롬의 최신 버전인 48(아마도 Electron 0.37.x)에서는 object-supertypeof-symbol 플러그인이 필요하지 않게 됩니다. 지난 26일 릴리즈된 V8 버전 4.9(크롬 49)에서는 destructuring, parameters, sticky-regex등이 구현되었습니다.

Electron 버전 0.35.x(Node 4.1.1, 크롬 45, v8 버전 4.5)에서는 아래의 3개 플러그인을 추가로 로드하여 ES6을 정상적으로 사용할 수 있습니다. Babel의 Github 리파지토리에 있는 380여 개 테스트 케이스를 돌려서 확인했으며 속도 역시 그럭저럭 나옵니다.

    ...
    // Require if using electron version 0.35.x
    "transform-es2015-block-scoping",
    "transform-es2015-classes",
    "transform-es2015-spread"
    ...

object-rest-spread는 ES2015의 공식 스펙은 아니지만 ES7의 꽃이라 할만한 멋진 연산자입니다. 이미 널리 사용되고 있으며, ESLint의 object-shorthand 룰에 영향을 받습니다.

.eslintrc 파일은 ESLint의 설정입니다. ESLint는 코드를 작성하는 과정에서 빈번하게 발생하는 실수를 예방하고, 엘레강스한 코드 스타일을 추천해 주며, 미래에 발생할 수 있는 잠재적 오류를 수정할 수 있도록 도와줍니다. 제가 사용하는 코드 편집기는 CODA 2인데, 여기에 ESLint JS Validator 플러그인을 추가하면 아래 설정에 기반을 두어 코드 검증기를 통해 꾸역꾸역 잔소리(?)해 대도록 꾸몄습니다. 만약 아톰(Atom) 편집기를 사용한다면, linter-eslint 패키지를 설치하여 사용할 수 있고 Sublime Text에도 비슷한 녀석이 있습니다.

eslint.gif

요런 느낌입니다. 가장 우선순위에 있는 룰은 얼마 전에 번역한 바 있는 Airbnb 코드 스타일이고, 그다음으로 JavaScript Standard Style의 프리셋과 React 플러그인을 적용하고, 개인적으로 탐탁지 못한 몇몇 규칙을 "rules"에 재정의한 것입니다.

{
  "ecmaFeatures": {
    "jsx": true,
    "modules": true,
    "experimentalObjectRestSpread": true
  },
  "env": { "es6": true, "node": true, "browser": true },
  "extends": ["standard", "airbnb"],
  "globals": { "$": true },
  "parser": "babel-eslint",
  "plugins": ["standard", "react"],
  "rules": {
    "comma-dangle": [2, "never"],
    "default-case": 0,
    "func-names": 0,
    "new-cap": [2, { "newIsCap": true, "capIsNew": false }],
    "no-console": 0,
    "object-curly-spacing": 0,
    "react/prop-types": 0,
    "react/sort-comp": 0,
    "space-before-function-paren": [2, "never"],
    "strict": 0
  }
}

노트: 경우에 따라서는 ESLint관련 패키지를 글로벌에 설치해야 할 수도 있습니다.

$ npm install -g eslint
$ npm install -g eslint-config-airbnb eslint-config-standard
$ npm install -g eslint-plugin-react eslint-plugin-standard

package.json 파일의 내용은 다음과 같습니다. 프로젝트에서 필요한 모듈들의 정보를 포함한 여러 내용으로 구성됩니다. 더 자세한 내용은 이곳을 참고하세요.

{
  "name": "MyProject",
  "version": "0.0.1",
  "license": "MIT",
  "description": "My Awesome Project",
  "author": "firejune",
  "main": "main.js",
  "dependencies": {
    "jquery": "^2.2.0",
    "jquery-ui": "^1.10.5",
    "react": "^0.14.3",
    "react-dom": "^0.14.3"
  },
  "devDependencies": {
    "babel-core": "^6.4.5",
    "babel-eslint": "^5.0.0-beta6",
    "babel-loader": "^6.2.1",
    "babel-plugin-transform-es2015-destructuring": "^6.4.0",
    "babel-plugin-transform-es2015-for-of": "^6.3.13",
    "babel-plugin-transform-es2015-modules-commonjs": "^6.4.5",
    "babel-plugin-transform-es2015-object-super": "^6.4.0",
    "babel-plugin-transform-es2015-parameters": "^6.4.5",
    "babel-plugin-transform-es2015-shorthand-properties": "^6.3.13",
    "babel-plugin-transform-object-rest-spread": "^6.3.13",
    "babel-plugin-transform-react-jsx": "^6.4.0",
    "electron-prebuilt": "^0.36.5",
    "eslint": "^1.10.3",
    "eslint-config-airbnb": "^3.1.0",
    "eslint-config-standard": "^4.4.0",
    "eslint-plugin-react": "^3.15.0",
    "eslint-plugin-standard": "^1.3.1",
    "grunt": "^0.4.5",
    "jest-cli": "^0.8.2",
    "nightwatch": "^0.8.15",
    "webpack": "^1.12.11"
  },
  "scripts": {
    "start": "electron .",
    "lint": "eslint ./src",
    "test:unit": "npm run lint && jest -c ./scripts/unit-test.json",
    "test:ui": "npm run test:unit  && nightwatch --test ./scripts/ui-test.js",
    "build": "npm run test:ui  && webpack --config ./scripts/package.js --release"
  }
}

굳이 ES6 문법까지는 필요는 없고, JSX 트랜스파일만 필요한 상황이라면 "transform-react-jsx" 플러그인만 남기거나 Babel이 아닌 node-jsx 모듈을 이용하는 방법도 있습니다. node-jsx가 사용법도 간단하고 빠르긴 한데, Babel로 이관되면서 deprecated 되었습니다.

Electron Starter

이제 Electron에서의 작업환경을 구성할 차례입니다. main.js 파일은 package.json에 명시되어 Electron이 처음으로 접근하는 파일이며, Electron에 의해 브라우저 윈도를 만들어줍니다. 아쉽지만, 이 파일은 ES6으로 작성할 수 없습니다.

'use strict';

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

// 실행 준비를 마치면 브라우저 창 생성
app.on('ready', function () {
  // 브라우저 생성
  const mainWindow = new BrowserWindow({width: 800, height: 600});
  // 브라우저에서 처음으로 그려질 페이지
  mainWindow.loadURL('file://' + __dirname + '/index.html');
  // 브라우저의 개발자 도구 자동으로 열기
  mainWindow.webContents.openDevTools();
  // 창이 닫히면 프로세스 종료
  mainWindow.on('closed', function() {
    app.quit();
  });
});

index.html 파일은 main.js에 의해 브라우저(BrowserWindow)에서 처음으로 그려질 페이지입니다. 이 브라우저가 바로 앞으로 동고동락할 작업용 브라우저입니다. 일단 다음과 같이 내용을 작성합니다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    <div id="example">Hello</div>
    <script>
      // Install babel hooks to the browser process
      require('babel-core/register')();
      require('./src');
    </script>
  </body>
</html>

노트: Electron은 main과 browser로 구분된 프로세스 두 개를 실행합니다. 따라서 두 프로세스 간에는 IPC 스타일의 통신을 이용해야 하지만, Electron 앱이 아닌, 일반적인 웹앱 개발에 Electron을 이용하는 것이므로 main 프로세스에서 하는 일에 대해서는 크게 걱정하지 않아도 됩니다. main.js는 main 프로세서에서 작동하고 index.html 및 하위 참조 스크립트들은 browser 프로세스에서 작동합니다.

자, 이제 기본적인 프로젝트 파일의 구성과 작업 실행 환경이 완료되었습니다. scripts 폴더는 정적 또는 동적 테스트 코드, 자동화 관련 코드, 작업 태스크 관리, 빌드 스크립트 등을 넣어둘 장소입니다. 이 글은 개발 환경을 구성하는 데 목적을 두기 때문에 이와 관련한 자세한 내용은 다루지 않을 것입니다. (엄청나게 다양하고 복잡하고 일일이 설명하기가 귀찮기도 하고 뭐 그렇습니다) LICENSE.md, README.md 파일에는 프로젝트와 관련된 내용을 작성하면 됩니다. 이제, 커멘드 라인에 개발에 필요한 모듈들을 설치하고 작업 결과를 확인할 수 있는 브라우저(Electron)를 실행해 봅시다. 'Hello'문자가 보이나요?

$ npm install
$ npm start

Enjoying Web Development with Electron

모든 준비는 끝났습니다. 멋들어지게 최신 자바스크립트 문법을 이용하여 본격적으로 개발을 시작해 봅시다. src/index.js 파일을 열고 간단한 React 기반의 'Hello, world!' 애플리케이션을 만들겠습니다.

import React from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
import 'jquery-ui/draggable';

class Title extends React.Component {
  constructor() {
    super();
    this.state = {
      value: 'Hello, world!'
    };
  }
  componentDidMount() {
    $(this.refs.el).draggable();
  }
  render() {
    return (
      <h1 ref="el">
        {this.state.value}
      </h1>
    );
  }
}

ReactDOM.render(
  <Title />,
  document.getElementById('example')
);

파일 작성 후 브라우저 창에서 새로 고칩니다. 단축키는 맥인 경우 CMD+R(윈도: Ctrl+R)입니다. 브라우저 개발자 도구는 CMD+ALT+I(윈도: Ctrl+Shift+I)로 열 수 있습니다. 'Hello, world!' 문자가 보이나요? 정상적으로 작동하는 것입니다. 놀랍죠? 문자의 드래그 앤 드롭 기능도 확인해 보세요!

크로스-브라우저는 어찌할까요? Browserify나 Webpack을 이용해서 번들링 한 후 실제 브라우저에서 동작하는 동적-테스트 도구(Nightwatch 와 같은)로 테스트 수행을 자동화하고 오류가 보고되면 그때 처리합니다.

Comments

기왕 번역한 김에 하나 더 했습니다. Airbnb에서 Reac와 JSX 스타일 가이드도 작성했더군요. ES6과 JSX기반의 React 코드를 작성하는 것을 마치 기본적인 것인 양 소개하고 있습니다. 요즘 Electron 기반 하이브리드 데스크탑 애플리케이션을 개발하고 있는데 전반에 걸쳐 React를 적용하는 중입니다. Electron은 구글 크롬 브라우저와 Node.js를 포함하고 있어서 하나의 브라우저에서 작동하는 것에만 집중할 수 있고 browserify또는 webpack와 같은 모듈 번들러(module bundler)의 도움 없이 네이티브 require를 브라우저에서도 사용할 수 있다 보니, 의존성(dependency)이나 네임스페이스(namespace) 관리는 물론이고, 실시간 트랜스파일(Transpile)이 가능해서 ES6 코드를 마구 내질러도 크게 문제 될 것이 없는 개발환경을 만들 수 있다는 것은... 이건 뭐 그냥 완전히 다른 세상이라고 밖에 표현하지 못하겠군요.

1. 기본 규칙(Basic Rules)

2. 클래스(Class) vs React.createClass

특별한 이유로 믹스인(mixin)하는 경우를 제외하고는 class extends React.Component를 사용하세요.

eslint rules: react/prefer-es6-class.

// bad
const Listing = React.createClass({
  render() {
    return <div />;
  }
});

// good
class Listing extends React.Component {
  render() {
    return <div />;
  }
}

3. 명명(Naming)

확장자: React 컴포넌트는 .jsx 확장자를 사용합니다.

파일명: 파일명에는 PascalCase(대문자로 시작)를 사용합니다. 예), ReservationCard.jsx.

참조명: React 컴포넌트의 참조 이름에는 PascalCase를 쓰고 그 인스턴스의 이름에는 camelCase(소문자로 시작)를 사용합니다.

eslint rules: react/jsx-pascal-case.

// bad
import reservationCard from './ReservationCard';

// good
import ReservationCard from './ReservationCard';

// bad
const ReservationItem = <ReservationCard />;

// good
const reservationItem = <ReservationCard />;

컴포넌트명: 컴포넌트명으로 파일명을 씁니다. 예), ReservationCard.jsx 파일은 ReservationCard라는 참조명을 가집니다. 그러나, 루트 컴포넌트가 디렉토리에 구성되었다면 파일명을 index.jsx로 쓰고 디렉토리명을 컴포넌트명으로 사용합니다:

// bad
import Footer from './Footer/Footer';

// bad
import Footer from './Footer/index';

// good
import Footer from './Footer';

4. 선언(Declaration)

displayName을 이용하여 컴포넌트명을 정하지 않습니다. 그대신, 참조에 의해 이름을 지정합니다.

// bad
export default React.createClass({
  displayName: 'ReservationCard',
  // stuff goes here
});

// good
export default class ReservationCard extends React.Component {
}

5. 조정(Alignment)

JSX 구문에 따른 정렬 스타일을 사용합니다.

eslint rules: react/jsx-closing-bracket-location.

// bad
<Foo superLongParam="bar"
     anotherSuperLongParam="baz" />

// good
<Foo
  superLongParam="bar"
  anotherSuperLongParam="baz"
/>

// if props fit in one line then keep it on the same line
<Foo bar="bar" />

// children get indented normally
<Foo
  superLongParam="bar"
  anotherSuperLongParam="baz"
>
  <Spazz />

6. 인용(Quotes)

JSX 속성(attributes)에는 항상 큰 따옴표(")를 사용합니다. 그러나 다른 모든 자바스크립트에는 작은 따옴표(single quotes)를 사용합니다.

왜죠? JSX 속성(attributes)은 따옴표(quotes)의 탈출(escaped)을 포함할 수 없습니다. 그래서 큰 따옴표를 이용하여 "don't"와 같은 접속사를 쉽게 입력할 수 있습니다. 일반적으로 HTML 속성(attributes)에는 작은 따옴표 대신 큰 따옴표를 사용합니다. 그래서 JSX 속성역시 동일한 규칙이 적용됩니다.

eslint rules: jsx-quotes.

// bad
<Foo bar='bar' />

// good
<Foo bar="bar" />

// bad
<Foo style={{ left: "20px" }} />

// good
<Foo style={{ left: '20px' }} />

7. 공백(Spacing)

자신을 닫는(self-closing) 태그에는 항상 하나의 공백만을 사용합니다.

// bad
<Foo/>

// very bad
<Foo                 />

// bad
<Foo
 />

// good
<Foo />

8. 속성(Props)

prop 이름은 항상 camelCase(소문자로 시작)를 사용합니다.

// bad
<Foo
  UserName="hello"
  phone_number={12345678}
/>

// good
<Foo
  userName="hello"
  phoneNumber={12345678}
/>

명시적으로 true 값을 가지는 prop은 그 값을 생략할 수 있습니다.

eslint rules: react/jsx-boolean-value.

// bad
<Foo
  hidden={true}
/>

// good
<Foo
  hidden
/>

9. 괄호(Parentheses)

JSX 태그가 감싸여(Wrap) 있어 한 줄 이상인 경우 괄호(parentheses)를 사용합니다.

eslint rules: react/wrap-multilines.

 // bad
  render() {
    return <MyComponent className="long body" foo="bar">
             <MyChild />
           </MyComponent>;
  }
  
  // good
  render() {
    return (
      <MyComponent className="long body" foo="bar">
        <MyChild />
      </MyComponent>
    );
  }
  
  // good, when single line
  render() {
    const body = <div>hello</div>;
    return <MyComponent>{body}</MyComponent>;
  }

10. 태그(Tags)

자식(children)을 가지지 않는다면 항상 자신을 닫는(self-close) 태그로 작성합니다.

eslint rules: react/self-closing-comp.

// bad
<Foo className="stuff"></Foo>

// good
<Foo className="stuff" />

만약, 컴포넌트의 속성(properties)을 여러 줄에 있는 경우, 닫는 태그는 다음 줄에 작성합니다.

eslint rules: react/jsx-closing-bracket-location.

// bad
<Foo
  bar="bar"
  baz="baz" />

// good
<Foo
  bar="bar"
  baz="baz"
/>

11. 메소드(Methods)

렌더(Render) 메소드에서 이벤트 핸들러에 바인드(Bind)가 필요한 경우에는 생성자(constructor)에서 합니다.

왜죠? 렌더러 메소드에서 바인드(bind)를 호출하게 되면 랜더링 할 때 마다 매번 새로운 함수를 생성하게 됩니다.

eslint rules: react/jsx-no-bind.

// bad
class extends React.Component {
  onClickDiv() {
    // do stuff
  }

  render() {
    return <div onClick={this.onClickDiv.bind(this)} />
  }
}

// good
class extends React.Component {
  constructor(props) {
    super(props);

    this.onClickDiv = this.onClickDiv.bind(this);
  }

  onClickDiv() {
    // do stuff
  }

  render() {
    return <div onClick={this.onClickDiv} />
  }
}

React 컴포넌트의 내부 메소드에 밑줄(underscore)을 접두사로 사용하지 않습니다.

// bad
React.createClass({
  _onClickSubmit() {
    // do stuff
  },

  // other stuff
});

// good
class extends React.Component {
  onClickSubmit() {
    // do stuff
  }

  // other stuff
}

12. 호출순서(Ordering)

class extends React.Component의 호출순서(Ordering):

  • constructor
  • 추가적인(optional) static 메소드
  • getChildContext
  • componentWillMount
  • componentDidMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • componentDidUpdate
  • componentWillUnmount
  • onClickSubmit()와 같은 clickHandlers 또는 eventHandlers 또는 onChangeDescription()
  • getSelectReason()와 같은 render를 위한 getter methods 또는 getFooterContent()
  • renderNavigation()와 같은 추가적인 렌더러 메소드 또는 renderProfilePicture()
  • render

React.createClass의 호출순서(Ordering):

  • displayName
  • propTypes
  • contextTypes
  • childContextTypes
  • mixins
  • statics
  • defaultProps
  • getDefaultProps
  • getInitialState
  • getChildContext
  • componentWillMount
  • componentDidMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • componentDidUpdate
  • componentWillUnmount
  • onClickSubmit()와 같은 clickHandlers 또는 eventHandlers 또는 onChangeDescription()
  • getSelectReason()와 같은 render를 위한 getter methods 또는 getFooterContent()
  • renderNavigation()와 같은 추가적인 렌더러 메소드 또는 renderProfilePicture()
  • render

eslint rules: react/sort-comp.

propTypes, defaultProps, contextTypes, 등을 어떻게 정의할까요...

import React, { PropTypes } from 'react';

const propTypes = {
  id: PropTypes.number.isRequired,
  url: PropTypes.string.isRequired,
  text: PropTypes.string,
};

const defaultProps = {
  text: 'Hello World',
};

class Link extends React.Component {
  static methodsAreOk() {
    return true;
  }

  render() {
    return <a href={this.props.url} data-id={this.props.id}>{this.props.text}</a>
  }
}

Link.propTypes = propTypes;
Link.defaultProps = defaultProps;

export default Link;

13. isMounted

isMounted는 사용하지 않습니다.

왜죠? isMounted안티-패턴(anti-pattern)입니다. ES6 클래스에서는 사용할수도 없습니다. 그리고 공식적으로 사용되지 않게(deprecated) 될 것입니다.

eslint rules: react/no-is-mounted.

Comments