당부의 말씀

이 글은 리액트 프로젝트를 구축하는데 필요한 Webpack4(웹팩4)에 대한 글 입니다. 포스팅의 전체적인 흐름은
Esau Silva의 learn-webpack-for-react을 따라가고 있습니다. 더 자세한 사항은 링크를 참고해주시길 바랍니다.

  • 리액트 사용자를 대상으로 하고 있습니다.

Webpack(웹팩)을 사용하는 이유 ?

스크린샷 2018-11-22 오전 12.15.03.png

웹 페이지 개발은 현재, 자바스크립트 개발이라고 봐도 무방할 정도로(리액트) 자바스크립트의 비중이 큽니다. 페이지 로딩에 있어서 많은 스크립트를 가져오게 될 경우 http 통신 병목현상이 발생할 수 있습니다. 웹팩은 네트워크 병목현상을 없애는 것입니다. 네트워크 병목현상을 없애기 위해서 웹팩은 js파일을 하나의 bundle파일로 만듭니다.

세상엔 공짜가 없죠. 웹팩을 이용해 http 통신 병목현상을 줄였지만 모든 js파일을 하나로 합치면서 파일의 크기가 커졌습니다. js파일이 커지면서 사용자는 첫 페이지 진입시 화면이 늦게 뜨는 불편한 경험을 하게됩니다. 이 불편한 경험을 해소하기 위해 코드스플릿팅이라는 기술이 나왔습니다. 코드스플릿팅에 대한 내용은 다음화에 진행됩니다.

웹팩을 사용하는 이유에대한 자세한 사항은 여기를 참고해주세요.

위 내용을 요약하자면 웹팩을 사용하는 이유는

http 커넥션을 최소화 하여 성능상의 이점을 챙기자입니다.

위 이미지는 js파일 및 assets파일들을 웹팩하여 하나의 .js 및 asset파일로 만들어주는 과정을 대략적으로 보여줍니다.

사실 리액트 개발을 하는데 있어서 웹팩을 할줄 몰라도됩니다. create-react-app을 이용해 프로젝트를 생성할 경우 이미 잘 만들어진 웹팩 설정이 있기때문에 웹팩을 막 열심히 하지 않아도 됩니다. 그래도 할줄 아는게 좋습니다.

목표

이 글은 Babel(바벨)설정, Webpack(웹팩) 설정, Code Splitting(코드 스플릿팅), Hot Module Replacement(핫 리로딩), 배포 설정을 목적으로 합니다.

개발 의존성 설치

이 글은 패키지 매니저로 npm이 아닌 yarn을 사용하고 있습니다. 꼭 yarn을 쓰지 않으셔도 됩니다. yarn 커맨드와 대응되는 npm 커맨드로 동일하게 실행 가능합니다.

먼저 프로젝트를 시작할 디렉토리를 접근합니다.
리액트를 위한 웹팩4답게 리액트와 웹팩을 이용하는데 필요한 라이브러리 설치를 시작합니다.

yarn init -y
yarn add react react-dom react-router-dom
yarn add @babel/core babel-loader @babel/polyfill @babel/preset-env @babel/preset-react @babel/plugin-proposal-class-properties html-webpack-plugin css-loader style-loader url-loader webpack webpack-cli webpack-dev-server -D (개발용 설치)
  • 설치한 라이브러리 설명

react : 리액트를 사용하기 위하 필수 라이브러리 입니다.
react-dom: 리액트 라이브러리 입니다. 브라우저를 위한 DOM 메소드를 제공합니다.
react-router-dom: 브라우저를 위한 라우팅 기능을 제공합니다.
@babel/core: 바벨을 사용하기 위한 필수 라이브러리입니다.

  • 바벨은 ES6/ES7 코드를 ES5 코드로 트랜스파일링 하기 위한 도구입니다.

@babel-polyfill: ES2015의 새로운 객체와 메소드를 사용할 수 있도록 도와줍니다.
@babel/preset-env: 최신 자바스크립트 기능을 ES5로 트랜스파일 해주는 라이브러리입니다.

  • 바벨 7버전부터 사용 가능한 라이브러리입니다. 바벨 7버전 아래의 경우 stage-0, stage-1, stage-2, stage-3을 설치하여 트랜스파일 해줘야합니다.

@babel/preset-react: 리액트 환경(JSX)을 위한 라이브러리입니다.
@babel/plugin-proposal-class-properties: 클래스 프로퍼티를 사용할 수 있도록 도와주는 바벨 플러그인입니다.
babel-loader: 바벨과 웹팩을 이용해 자바스크립트 파일을 트랜스파일링 합니다.
html-webpack-plugin: 웹팩 번들에 html파일을 제공하는 웹팩 라이브러리입니다.
css-loader: css 파일을 import 또는 require할 수 있도록 도와주는 웹팩 라이브러리입니다.
style-loader: 읽은 css파일을 style태그로 만들어 head태그에 삽입해주는 웹팩 라이브러리입니다.
webpack: 웹팩을 사용하기 위한 필수 라이브러리입니다.
webpack-cli: 웹팩 커맨드라인 인터페이스 라이브러리입니다.
webpack-dev-server: 웹팩 개발서버 라이브러리입니다.


참고자료

babel
@babel/polyfill
@babel/preset-env


Babel(바벨) 세팅

설치한 Babel(바벨)을 어플리케이션에 적용하기 위해 Babel(바벨) 설정 파일을 설정합니다.

/
touch .babelrc

.babelrc파일은 preset(react, env)과 plugin(class-properties)을 어플리케이션에 적용하기 위한 연결고리입니다.
설치한 Babel(바벨)어플리케이션을 적용시켜줍니다.

/.babelrc
{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    "@babel/plugin-proposal-class-properties"
  ]
}

.babelrc만 설정해서는 사용할 수 없습니다. 설정한 .babelrc를 뒤에 나올 webpack에 적용시켜야합니다.

Webpack(웹팩) 세팅

Webpack(웹팩)을 세팅하기 위해 Webpack(웹팩) 설정파일을 생성합니다.

/
touch webpack.config.js

Webpack(웹팩) 설정 코드 뼈대를 작성합니다.

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const port = process.env.PORT || 8080;

module.exports = {
  // webpack 설정 코드 작성. 작성된 코드는 module.export로 내보냅니다.
};

본격적으로 Webpack(웹팩) 설정 코드를 작성합니다.

  • mode

module.exports = {
      mode:'development'
}

웹팩 설정이 development(개발)모드인지 production(프로덕션) 모드인지 정합니다.
development 모드는 개발자 경험에 초점이 맞춰진 모드이며 production모드는 배포에 초점이 맞춰진 모드입니다.

  • entry, output

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

entry 옵션은 앱이 있는 위치와 번들링 프로세스가 시작되는 지점입니다.

  • Webpack4(웹팩4)부터는 entry 옵션을 생략할 수 있습니다. 생략할 경우 ./src/index.js를 기본으로 봅니다.

output 옵션은 번들링 프로세스가 끝난 뒤 번들링된 파일을 저장할 장소와 이름을 지정합니다.

  • 번들링된 파일 이름을 bundle.[hash].js로 하고 그 파일을 ./dist에 저장하라는 의미입니다.

  • filename의 [hash]는 어플리케이션이 수정되어 다시 컴파일될 때마다 Webpack(웹팩)에서 생성된 해시로 변경해주어 캐싱에 도움이 됩니다.

  • module

module.exports = {
      ...
      module:{
          rules:[
          { // 첫번째 룰
            test:\.(js)$/,
            exclude:/node_modules/,
            use:['babel-loader']
          },
          { // 두번째 룰
            test: /\.css$/,
            use: [
              {
                loader: 'style-loader'
              },
              {
                loader: 'css-loader',
                options: {
                  modules: true,
                  camelCase: true,
                  sourceMap: true
                }
              }
            ]
          }
        ]
    }
}

module옵션은 번들링과정에서 진행할 규칙을 정의하는 지점입니다.

  • ES6, ES7문법으로 작성된 javascript 파일을 ES5로 바꾸기 위해 작성한 .babelrc를 babel-loader를 이용해 규칙에 적용합니다.

    node_moudules를 폴더아래에 존재하는 .js파일을 제외한
    모든 .js파일에 babel-loader(.babelrc에 설정한 파일)을 적용합니다.
    
  • .js파일에서 import 또는 require로 .css파일을 가져올수 있게 해주는 css-loader와 .css파일을 style태그로 만든뒤 head태그 안에 선언해주는 style-loader를 규칙에 적용합니다.

    어플리케이션의 모든 .css파일을 style-loader와 css-loader를 적용합니다.
    css-loader의 options는 css-loader에 적용할 옵션입니다. 
       modules - CSS Module 사용합니다.
       camelCase - CamelCase로 CSS를 사용합니다.
       sourceMap - Sourcemaps을 사용합니다.
    

  • 예시

    /styles/padalog.css
    .pada-log{
    background-color:#fff
    }
    
    라는 .css파일이 있다면
    /src/App.js
    import { padaLog } from '../styles/padalog.css
    
    이렇게 불러와 사용할 수 있습니다.

참고

각 loader에는 고유의 options가 있습니다.
필요한 옵션이 있다면 loader의 공식 홈페이지에 접속하여 DOCS를 확인해보세요.

css-loader
style-loader
babel-loader


  • plugins

module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ],
};

plugins옵션은 웹팩 번들과정에 적용할 플러그인들입니다.

  • HtmlWebpackPlugin은 생성된 .html파일과 .favicon파일을 번들링과정에 포함시키는 플러그인입니다.

  • 이 플러그인이 없으면 dist 폴더에 bundle.[hash].js가 추가된 .html 파일을 매번 넣어줘야하는 불편함이 생기게 됩니다.


참고

htmlwebpackplugin


  • devtool

module.exports = {
  ...
  devtool: 'inline-source-map',
};

devtool옵션은 소스맵을 생성해 디버깅을 도와주는 옵션이다.

  • 소스맵에는 여러가지 유형이 있다.

참고

sourcemap


  • devServer

module.exports = {
  ...
  devServer: {
    host: 'localhost',
    port: port,
    open: true,
    historyApiFallback: true
  }
};

devServer 개발서버를 정의하는 옵션이다.

  • host는 로컬호스트로 지정하고 port는 상단에서 정의한 값으로 설정

  • open은 서버를 실행했을 때 자동으로 브라우저를 열어주는 옵션으로 true, false 어느 값을 줘도 상관없다.

  • historyApiFallback은 브라우저에서 URL을 변경할 수 있도록 도와주는 옵션입니다. false일 경우 브라우저에서 url을 변경하면 적용되지 않습니다.


참고

dev-server


완성된 코드

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const port = process.env.PORT || 8080;

module.exports = {
  mode:'development',
  entry:'./src/index.js',
  output:{
    path: __dirname + '/dist',
    filename: 'bundle.[hash].js'
  },
  module:{
    rules:[
      { // 첫번째 룰
        test:\.(js)$/,
        exclude:/node_modules/,
        use:['babel-loader']
      },
      { // 두번째 룰
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              camelCase: true,
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  devtool: 'inline-source-map',
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      // favicon: 'public/favicon.ico' 파비콘은 준비가 되어있지 않아 주석처리합니다.
    })
  ],
  devServer: {
    host: 'localhost',
    port: port,
    open: true,
    historyApiFallback: true
  }
};

리액트를 실행하기 위한 기본적인 개발서버용 웹팩설정은 끝이 났습니다.
이제 만들어진 웹팩설정을 이용해 리액트 어플리케이션을 만들도록 하겠습니다.

리액트

먼저 HtmlWebpackPlugin을 사용하기 위한 html파일과 favicon파일을 생성합니다.

/
mkdir public && cd $_ && touch index.html

public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>webpack4_for_react</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

이제 다시 루트로 돌아와서 리액트 루트 파일을 하나 생성합니다.

/
mkdir src && cd $_ && touch index.js

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
ReactDOM.render(<App />, document.getElementById('root'));

components/App.js와 함께 몇개의 컴포넌트 파일들을 생성해줍니다.

/
mkdir components && cd $_ && touch App.js Layout.js Home.js DynamicPage.js NoMatch.js

components/App.js

import React from 'react';
import { Switch, BrowserRouter as Router, Route } from 'react-router-dom';
import Home from './Home';
import DynamicPage from './DynamicPage';
import NoMatch from './NoMatch';
const App = () => {
  return (
    <Router>
      <div>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/dynamic" component={DynamicPage} />
          <Route component={NoMatch} />
        </Switch>
      </div>
    </Router>
  );
};
export default App;
  • 코드에 대한 설명은 따로하지 않겠습니다.

components/Layout.js

import React from 'react';
import { Link } from 'react-router-dom';
const Layout = ({ children }) => {
  return (
    <React.Fragment>
      <Link to="/">
        <h1>리액트를 위한 웹팩4</h1>
      </Link>
      {children}
    </React.Fragment>
  );
};
export default Layout;

components/Home.js

import { Link } from 'react-router-dom';
import Layout from './Layout';
const Home = () => {
  return (
    <Layout>
      <p>안녕하세요 리액트를 위한 웹팩4입니다.</p>
      <p>
        <Link to="/dynamic">Dynamic Page로 이동</Link>
      </p>
    </Layout>
  );
};

components/DynamicPage.js

import React from 'react';
import Layout from './Layout';
const DynamicPage = () => {
  return (
    <Layout>
      <h2>Dynamic Page</h2>
      <p>코드스플릿팅을 이용한 비동기페이지입니다.</p>
    </Layout>
  );
};
export default DynamicPage;

components/NoMatch.js

import React from 'react';
import Layout from './Layout';
const NoMatch = () => {
  return (
    <Layout>
      <strong>페이지 없습니다.</strong>
    </Layout>
  );
};
export default NoMatch;

리액트 어플리케이션이 다 완성됐습니다.

완성된 리액트 어플리케이션 구동을 위해 package.json을 수정합니다.

package.json

...
"scripts": {
    "start": "webpack-dev-server"
  },
...

완성된 프로젝트의 트리구조는 아래와 같습니다.

|-- public
    |-- index.html
|-- src
    |-- components
        |-- App.js
        |-- Home.js
        |-- DynamicPage.js
        |-- Layout.js
        |-- NoMatch.js
   |-- index.js
|-- node_modules
|-- .babelrc
|-- package.json
|-- webpack.config.js
|-- yarn.lock

이제 터미널에서 yarn start로 개발 서버를 시작합니다.

ezgif.com-video-to-gif (1).gif

bundle.3f3a1cbb5ba33d6.js파일은 웹팩의 결과물입니다. 파일 이름이 난수인 이유는 웹팩 설정 output - filename 설정 때문입니다.

이로써 기본적인 리액트 어플리케이션을 위한 웹팩4 설정은 끝났습니다. 이것만으로도 개발 서버를 운영하는데는 큰 문제가 없습니다만.. 여러가지 불편한점이 있습니다. 가장 큰 문제는 코드를 수정하고 저장하더래도 자동으로 코드 반영이 되지 않는다는 건데요. 코드 수정 후 웹팩을 다시 실행시켜야지 반영됩니다. 현재는 파일크기가 작아서 금방 웹팩 번들링이 진행되지만 sementic-ui라던지 material-ui와 같은 큰 라이브러리를 도입할 경우 번들링 속도는 느려지게 됩니다.

따라서 이 문제를 해결하기 위해 HMR을 도입할 예정인데요. HMR도입과 함께 코드스플릿팅도 도입하도록 하겠습니다.

HMR과 코드스필릿트를 적용한 프로젝트는 다음편을 기대해주세요.