브라질 출신 풀스택 개발자인 에수 실바(Esau Silva)의 How to use Webpack with React: an in-depth tutorial 튜토리얼을 번역한 글이다. 리액트에 익숙한 경험자를 대상으로 웹팩(webpack)과 바벨(babel)을 사용해 리액트 개발 환경을 만드는 과정을 소개한다. 완성된 코드는 react-starter-boilerplate-hmr에서 확인할 수 있다.
리액트 라우터(React Router), 핫 모듈 리플레이스먼트(HMR, Hot Module Replacement), 경로(Route)와 벤더(Vendor)로 코드 분할 및 배포 설정 구성까지, 웹팩(Webpack)과 리액트(React) 사용법의 모든 것을 알아보자.
아래는 앞으로 사용할 라이브러리 목록이다.
- React 16
- React Router 4
- Semantic UI : CSS 프레임워크
- Hot Module Replacement (HMR)
- CSS Autoprefixer
- CSS Modules
- Stage 1 Preset
- Webpack 4
- Route과 Vendor로 코드 분할
- Webpack Bundle Analyzer
준비 사항
- yarn과 Node.js 설치가 되어 있는지 확인한다.
- 리액트(React)와 리액트 라우터(React Router) 기초 지식이 필요하다.
- 역자: 리액트 기초지식이 없다면, 이 튜토리얼을 멈추고
create-react-app
으로 간단한 리액트 앱을 만들어보는 것을 추천한다.create-react-app
은 리액트 개발 도구와 환경 설정이 이미 세팅되어 있기 때문에 애플리케이션 구현에만 신경쓰면 된다. 리액트 도움닫기 - create-react-app 장부터 읽는 것을 추천한다.
📌 yarn
대신 npm
을 사용할 수 있으나 명령어가 다를 수 있으니 유의하길 바란다.
1. 의존성 초기화
제일 먼저 새 디렉터리를 만들고 그 안에 package.json
파일을 만든다.
mkdir webpack-for-react && cd $_
yarn init -y
package.json
파일은 아래와 같을 것이다.
{
"name": "webpack-for-react",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
초기 프로덕션 의존성(production dependencies) 과 개발 의존성(development dependencies)을 설치한다. 개발 의존성은 개발 단계에서만 사용되는 의존 라이브러리이며, 프로덕션 의존성은 배포 단계에서 사용되는 라이브러리를 말한다.
yarn add react react-dom react-prop-types react-router-dom semantic-ui-react
yarn add babel-core babel-loader babel-preset-env babel-preset-react babel-preset-stage-1 css-loader style-loader html-webpack-plugin webpack webpack-dev-server webpack-cli -D
📌 볼드체는 변경된 코드를 뜻한다. 의존성 버전은 다를 수 있다.
{
"name": "webpack-for-react",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-prop-types": "^0.4.0",
"react-router-dom": "^4.2.2",
"semantic-ui-react": "^0.77.1"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.3",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-1": "^6.24.1",
"css-loader": "^0.28.10",
"html-webpack-plugin": "^3.0.4",
"style-loader": "^0.19.1",
"webpack": "^4.0.0",
"webpack-cli": "^2.0.14",
"webpack-dev-server": "^3.0.0"
}
}
설치된 라이브러리를 간략히 알아보자.
- react: 리액트
- react-dom: 브라우저 DOM 메서드를 제공한다.
- react-prop-types: React props 타입을 체크한다.
- react-router-dom: Provides routing capabilities to React for the browser
- semantic-ui-react: CSS 프레임워크
- babel-core: Babel 핵심 의존성 라이브러리이다. Babel(바벨)은 자바스크립트 ES6를 ES5로 컴파일하여 현재 브라우저가 이해할 수 있도록 변환하는 도구다.
- babel-loader: babel과 webpack을 사용해 자바스크립트 파일을 컴파일한다.
- babel-preset-env: ES2015, ES2016, ES2017 버전을 지정하지 않아도 바벨이 자동으로 탐지해 컴파일한다.
- babel-preset-react: 리액트를 사용한다는 것을 바벨에게 말해준다.
- babel-preset-stage-1: TC39에서 검토 중인 Stage 1 스펙을 사용한다. (stage-0부터 3까지는 EcmaScript 스펙 중에서 비공식 실험적인 기술들을 사용할 수 있게 해주는 프리셋으로 Stage 2와 Stage 3도 사용 가능하다.)
- css-loader:
import/require()
처럼@import
와url()
해석한다. - html-webpack-plugin: 애플리케이션을위한 HTML 파일을 생성하거나 템플릿을 제공한다.
- style-loader:
<style>
태그를 삽입하여 CSS에 DOM을 추가한다. - webpack: 모듈 번들러(Module bundler)
- webpack-cli: Webpack 4.0.1 이상에서 필요한 커맨드라인 인터페이스다.
- webpack-dev-server: 애플리케이션 개발 서버를 제공한다.
2. Babel 설정
최상위 디렉터리 webpack-for-react
에 바벨 설정 파일을 만든다.
touch .babelrc
.babelrc
파일을 열어 아래 코드를 추가한다.
{
"presets": ["env", "react", "stage-1"]
}
바벨이 프리셋(preset) 플러그인을 사용할 수 있게 됐다. 나중에 Webpack에서 babel-loader
를 호출할 때 어떤 역할을 하는지 이해하게 될 것이다.
3. Webpack 설정
지금부터 본격적으로 시작해보자. Webpack 설정 파일을 만들어보자.
터미널에서 아래 명령어를 입력해 webpack.config.js
을 만든다.
touch webpack.config.js
webpack.config.js
파일을 열고 아래 코드를 작성한다.
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const port = process.env.PORT || 3000;
module.exports = {
// webpack 설정 부분
};
웹팩 기본 설정을 마쳤다. 다음으로 webpack
과 html-webpack-plugin
이 필요하다. 환경 변수 PORT가 없으면 기본 포트를 제공하고 모듈을 내보내는 일을 한다.
webpack.config.js
파일을 다시 열어 아래 코드를 추가한다.
...
module.exports = {
mode: 'development',
};
설정 사항이 개발 환경(development
)인지 프로덕션(production
)인지를 알려줬다.
"개발 모드는 속도와 개발자 경험에 최적화되어 있다. 프로덕션 모드는 애플리케이션을 배포와 관련된 유용한 집합을 제공한다." 웹팩 4: mode and optimization 에서 발췌
...
module.exports = {
...
entry: './src/index.js',
output: {
filename: 'bundle.[hash].js'
},
};
웹팩 인스턴스를 실행하기 위해 entry
, output
, filename
, devtool
, module
, rules
값을 설정해보자.
entry
- 애플리케이션의 진입점(entry point)이다. 리액트 앱이 있는 위치와 번들링 프로세스가 시작되는 지점이다. (웹팩 공식 문서 - entry point)
웹팩4에서는 웹팩3과 반대로 entry
를 생략할 수 있다. entry
가 없으면 웹팩은 시작점이 ./src
디렉토리 아래에 있다고 가정한다. 그러나 이 튜토리얼에서는 entry
를 설정해 시작점을 분명하게 표시하기로 한다. 나중에 이 부분을 삭제해도 된다.
output
- 컴파일된 파일을 저장할 경로를 알려준다.filename
- 번들된 파일 이름을 말한다.[hash]
는 애플리케이션이 수정되어 다시 컴파일 될 때마다 웹팩에서 생성된 해시로 변경해주어 캐싱에 도움이 된다.
...
module.exports = {
...
devtool: 'inline-source-map',
};
devtool
은 소스 맵(source maps)을 생성해 애플리케이션 디버깅을 도와준다. 소스 맵에는 여러 가지 유형이 있으며 그 중 inline-source-map은 은 개발시에만 사용된다. (이외 옵션은 공식 문서를 참고한다.)
...
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
- 애플리케이션 내 포함되는 모듈을 정의한다. 우리의 경우 ESNext(바벨), CSS 모듈에 해당한다.rules
- 각 모듈을 처리하는 방법을 설정한다.
첫 번째 룰
node_modules
디렉터리를 제외한 자바스크립트 파일을 찾은 다음 babel-loader
를 통해 바벨을 사용해 바닐라 자바스크립트로 변환한다. 바벨은 .babelrc
파일에서 설정 내용을 읽는다.
두 번째 룰
css 파일을 찾고 style-loader
와 css-loader
로 css를 처리한다. 그 다음 css-loader
에게 CSS 모듈, 카멜 케이스(camel case), 소스 맵을 사용할 것을 지시한다.
CSS 모듈과 카멜 케이스
이제 import Styles from ‘./styles.css
또는 import { style1, style2 } from './styles.css'
와 같이 구조 해체 문법으로 스타일 정의를 할 수 있다.
...
<div className={Style.style1}>Hello World</div>
// 또는 구조해체 문법을 사용할 수 있다.
<div className={style1}>Hello World</div>
...
아래와 같은 CSS 클래스가 있다면,
.home-button {...}
아래 코드와 같이 카멜 케이스를 정의해 CSS 클래스를 가져온다.
import { homeButton } from './styles.css'
이제 플러그인을 구성해보자.
html-webpack-plugin
은 다른 옵션을 가진 객체를 받는다. HTML 템플릿과 favicon을 지정한다. 이후 Bundle Analyzer 및 HMR 용 플러그인을 추가할 것이다.
webpack.config.js
파일에 아래 코드를 추가하자.
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
template: 'public/index.html',
favicon: 'public/favicon.ico'
})
],
};
마지막으로 개발 서버를 설정한다.
...
module.exports = {
...
devServer: {
host: 'localhost',
port: port,
historyApiFallback: true,
open: true
}
};
host
는 localhost로, port
는 기본 port로 할당했다. 현재 기본 port 번호는 3000이다. historyApiFallback
을 true
로, open
을 true
로 설정한다. 서버를 실행하면 브라우저가 자동으로 열리고 http://localhost:3000 에서 애플리케이션이 자동으로 실행된다.
완성된 웹팩 설정 코드는 아래와 같다.
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const port = process.env.PORT || 3000;
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.[hash].js'
},
devtool: 'inline-source-map',
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
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'public/index.html',
favicon: 'public/favicon.ico'
})
],
devServer: {
host: 'localhost',
port: port,
historyApiFallback: true,
open: true
}
};
3. 리액트 애플리케이션 만들기
간단한 리액트 애플리케이션을 만들어보자. home, page not found, dynamic 각 세 웹 페이지 경로를 만들고 비동기로 로딩하게 만들 것이다.
📌리액트와 리액트 라우터 기초 지식이 있다고 생각하고 설명한다.
현재 프로젝트 구조는 아래와 같다.
|-- node_modules
|-- .babelrc
|-- package.json
|-- webpack.config.js
|-- yarn.lock
터미널을 열고 public
폴더를 만들고 index.html
을 만든다.
mkdir public && cd $_
touch index.html
public
디렉터리 안에 favicon도 추가한다. 이 곳에서 기본 리액트 favicon 샘플 이미지 파일을 다운받을 수 있다.
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">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.13/semantic.min.css"></link>
<title>webpack-for-react</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
간단한 HTML 템플릿이다. Semantic UI 스타일시트 URL를 추가하고 div
도 추가했다. 이 부분이 리액트 앱이 렌더링되는 곳이다.
다시 터미널로 돌아와 명령어로 index.js
파일을 만든다.
cd ..
mkdir src && cd $_
touch index.js
index.js
파일을 열고 아래 코드를 넣는다.
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
ReactDOM.render(<App />, document.getElementById('root'));
터미널에서 명령어로 리액트 컴포넌트 파일을 만든다.
mkdir components && cd $_
touch App.js Layout.js Layout.css Home.js DynamicPage.js NoMatch.js
프로젝트 구조가 아래와 같을 것이다.
|-- node_modules
|-- public
|-- index.html
|-- favicon.ico
|-- src
|-- components
|-- App.js
|-- DynamicPage.js
|-- Home.js
|-- Layout.css
|-- Layout.js
|-- NoMatch.js
|-- index.js
|-- .babelrc
|-- package.json
|-- webpack.config.js
|-- yarn.lock
리액트 라우터로 페이지 경로와 컴포넌트를 연결할 차례다.
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;
이제 Layout 컴포넌트 스타일과 컴포넌트를 정의하자.
Layout.css
파일을 열고 아래 코드를 넣는다.
.pull-right {
display: flex;
justify-content: flex-end;
}
.h1 {
margin-top: 10px !important;
margin-bottom: 20px !important;
}
Layout.js
파일을 열고 아래 코드를 작성한다.
import React from 'react';
import { Link } from 'react-router-dom';
import { Header, Container, Divider, Icon } from 'semantic-ui-react';
import { pullRight, h1 } from './layout.css';
const Layout = ({ children }) => {
return (
<Container>
<Link to="/">
<Header as="h1" className={h1}>
webpack-for-react
</Header>
</Link>
{children}
<Divider />
<p className={pullRight}>
Made with <Icon name="heart" color="red" /> by Esau Silva
</p>
</Container>
);
};
export default Layout;
웹 사이트의 레이아웃을 만들었다. CSS Modules를 통해 layout.css
에서 pullRight
, h1
두 CSS 룰을 가져왔다. 카멜 케이스로 가져와야 한다.
Home.js
를 열고 아래 코드를 작성한다.
import React from 'react';
import { Link } from 'react-router-dom';
import Layout from './Layout';
const Home = () => {
return (
<Layout>
<p>Hello World of React and Webpack!</p>
<p>
<Link to="/dynamic">Navigate to Dynamic Page</Link>
</p>
</Layout>
);
};
export default Home;
DynamicPage.js
를 열고 아래 코드를 작성한다.
import React from 'react';
import { Header } from 'semantic-ui-react';
import Layout from './Layout';
const DynamicPage = () => {
return (
<Layout>
<Header as="h2">Dynamic Page</Header>
<p>This page was loaded asynchronously!!!</p>
</Layout>
);
};
export default DynamicPage;
NoMatch.js
를 열고 아래 코드를 작성한다.
import React from 'react';
import { Icon, Header } from 'semantic-ui-react';
import Layout from './Layout';
const NoMatch = () => {
return (
<Layout>
<Icon name="minus circle" size="big" />
<strong>Page not found!</strong>
</Layout>
);
};
export default NoMatch;
필요한 모든 컴포넌트를 만들었다. 마지막으로 애플리케이션을 실행하는 스크립트를 package.json
에 정의한다.
{
"name": "webpack-for-react",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "webpack-dev-server"
},
"dependencies": {
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-prop-types": "^0.4.0",
"react-router-dom": "^4.2.2",
"semantic-ui-react": "^0.77.1"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.3",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-1": "^6.24.1",
"css-loader": "^0.28.10",
"html-webpack-plugin": "^3.0.4",
"style-loader": "^0.19.1",
"webpack": "^4.0.0",
"webpack-cli": "^2.0.14",
"webpack-dev-server": "^3.0.0"
}
}
scripts
와 start
키(key)를 추가했다. 이제 웹팩 개발 서버와 리액트가 함께 실행된다. 환경 설정 파일을 지정하지 않으면 webpack-dev-server
는 webpack.config.js
파일을 최상단(루트) 디렉터리 기본 구성 항목으로 찾는다.
터미널에서 직접 확인해보자! 프로젝트 최상단 디렉터리로 가서 yarn
명령어로 서버를 실행한다.
yarn start
이제 리액트 앱은 웹팩으로 작동한다. 번들된 자바스크립트 해쉬인 bundle.d505bbab002262a9bc07.js
로 파일명이 표시되는 것을 확인할 수 있다.
4. Hot Module Replacement (HMR) 설정
터미널로 돌아가, react-hot-loader 라이브러리를 개발 의존성으로 설치한다.
yarn add react-hot-loader -D
.babelrc
파일을 열고 볼드체로 된 부분을 추가한다. 2번째 줄 끝에 (,)를 추가하는 것도 잊지 말자.
{
"presets": [["env", { "modules": false }], "react", "stage-1"],
"plugins": ["react-hot-loader/babel"]
}
코드를 간결하게 만들기 위해 기존에 입력한 코드는 생략했다.
...
module.exports = {
entry: './src/index.js',
output: {
...
publicPath: '/'
},
...
plugins: [
new webpack.HotModuleReplacementPlugin(),
...
],
devServer: {
...
hot: true
}
};
추가한 부분을 살펴보자.
publicPath: ‘/’
— Hot reloading 은 중첩된 경로에서 동작하지 않는다.webpack.HotModuleReplacementPlugin
— HMR 업데이트시 브라우저 터미널에 표시해 알아보기 쉽게 한다.hot: true
— 서버에 HMR 작동을 허락한다.
index.js
파일을 열고 <AppContainer>
으로 애플리케이션 전체 컴포넌트를 감싼 다음module.hot
을 체크하도록 설정한다.
import { AppContainer } from 'react-hot-loader';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
const render = Component =>
ReactDOM.render(
<AppContainer>
<Component />
</AppContainer>,
document.getElementById('root')
);
render(App);
// Webpack Hot Module Replacement API 부분
if (module.hot) module.hot.accept('./components/App', () => render(App));
이제 HMR을 테스트해보자! 터미널로 다시 돌아와 서버를 재 실행해보자.
yarn start
새로고침 없이 앱이 업데이트 되는 것을 볼 수 있다.
브라우저에서 변경 사항을 표시하려면 크롬 개발자 도구(Chrome DevTools)에서 Rendering -> Paint flashing 을 선택 후 변경된 페이지마다 녹색 부분으로 강조 표시되는 것을 볼 수 있다. 터미널에서도 웹팩이 브라우저에 전송한 변경 사항을 확인할 수 있다.
5. 코드 분할
코드 분할(code splitting)을 사용하면 큰 번들 대신에 비동기 또는 병렬로 로드되는 여러 개의 번들을 만들 수 있다. 또한 Vendor 코드를 분리시켜 앱에서 로딩 시간을 줄일 수 있게 된다.
경로 지정
경로 별로 코드 분할을 수행할 수 있는 방법은 많다. 이 중 우리는 react-imported-component를 사용할 것이다.
사용자가 다른 경로로 이동할 때 로드 스피너를 보여주어 새 페이지가 로드될 때까지 대기하는 동안 사용자가 빈 화면을 보지 않게 만들어보자. 이를 위해 로딩 컴포넌트(Loading component)를 만들 것이다.
그러나 새 페이지가 실제로 아주 빨리 로딩되는 경우, 사용자는 몇 밀리 초 동안 깜빡이는 로드 스피너를 보지 못하게 되므로 300 밀리 초 정도 로딩되는 컴포넌트를 지연시키고자 한다. 이를 위해 react-delay-render 라이브러리를 사용할 것이다.
먼저 터미널에서 아래 명령어를 입력해 두 의존성을 추가하자.
yarn add react-imported-component react-delay-render
이제 Loading 컴포넌트를 만들어보자.
touch ./src/components/Loading.js
Loading.js
파일을 열고 아래 코드를 입력한다.
import React from 'react';
import { Loader } from 'semantic-ui-react';
import ReactDelayRender from 'react-delay-render';
const Loading = () => <Loader active size="massive" />;
export default ReactDelayRender({ delay: 300 })(Loading);
이제 Loading 컴포넌트를 만들었으니, App.js
파일을 열어 아래와 같이 수정한다.
import React from 'react';
import { Switch, BrowserRouter as Router, Route } from 'react-router-dom';
import importedComponent from 'react-imported-component';
import Home from './Home';
import Loading from './Loading';
const AsyncDynamicPAge = importedComponent(
() => import(/* webpackChunkName:'DynamicPage' */ './DynamicPage'),
{
LoadingComponent: Loading
}
);
const AsyncNoMatch = importedComponent(
() => import(/* webpackChunkName:'NoMatch' */ './NoMatch'),
{
LoadingComponent: Loading
}
);
const App = () => {
return (
<Router>
<div>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/dynamic" component={AsyncDynamicPAge} />
<Route component={AsyncNoMatch} />
</Switch>
</div>
</Router>
);
};
export default App;
이로서 DynamicPage 컴포넌트, NoMatch 컴포넌트, 기본 애플리케이션, 각각 하나의 번들 또는 청크(chunck)가 만들어진다.
이제 번들 파일 이름을 변경해보자. webpack.config.js
를 열고 아래와 같이 수정한다.
...
module.exports = {
...
output: {
filename: '[name].[hash].js',
...
},
}
이제 앱을 실행해 경로를 기준으로 코드 분할이 잘 실행되는지 확인해보자.
yarn start
위 이미지 파일에 웹팩으로 생성된 세 가지 청크(chunk) 파일을 확인해보자. 앱이 실행되면 주요 번들만 로드된다는 것을 알 수 있다. 마지막으로 동적 페이지로 이동을 클릭하면 이 페이지에 해당하는 번들이 비동기적으로 로딩되는 것을 확인할 수 있다.
404 not found: 페이지를 찾을 수 없음 페이지는 번들이 로드되지 않기 때문에 사용자 대역폭(user bandwith)을 절약할 수 있게 된다.
vendor
vendor로 애플리케이션 코드를 쪼개보자. webpack.config.js
파일을 열고 아래와 같이 파일을 변경한다.
...
module.exports = {
entry: {
vendor: ['semantic-ui-react'],
app: './src/index.js'
},
...
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: 'initial',
test: 'vendor',
name: 'vendor',
enforce: true
}
}
}
},
...
};
각 설정 부분을 살펴보자.
entry.vendor: [‘semantic-ui-react’]
— 메인 앱에서 특정 라이브러리를 빼내어 vendor로 만든다.optimization
— 이 부분을 생략하면 Webpack은 vendor로 에플리케이션을 분할할 것이다. 이 부분을 추가하면 큰 번들 용량이 대폭 줄어든다. (참고: CommonsChunkPlugin -> Initial vendor chunk)
📌 웹팩3에서는 CommonsChunkPlugin
을 사용해 vendor와 common으로 코드 분할을 했지만, 웹팩4에서는 삭제된 기능이다. 따라서 CommonsChunkPlugin
을 제거했다. 캐싱 제어가 필요할 수도 있기에 optimization.splitChunks
를 추가했다. 자세한 내용은 이 곳을 참고하길 바란다.
터미널에서 다시 앱을 실행하자.
yarn start
터미널에서 이전 세 청크와 더해진 새 vendor 청크를 표시했다. HTML을 보면 vendor와 앱 청크 둘다 모두 로드 되었음을 알 수 있다.
지금까지 웹팩 구성을 업데이트한 webpack.config.js
코드는 아래와 같다.
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const port = process.env.PORT || 3000;
module.exports = {
mode: 'development',
entry: {
vendor: ['semantic-ui-react'],
app: './src/index.js'
},
output: {
filename: '[name].[hash].js'
},
devtool: 'inline-source-map',
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
}
}
]
}
]
},
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: 'initial',
test: 'vendor',
name: 'vendor',
enforce: true
}
}
}
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
template: 'public/index.html',
favicon: 'public/favicon.ico'
})
],
devServer: {
host: 'localhost',
port: port,
historyApiFallback: true,
open: true,
hot: true
}
};
배포 설정
webpack.config.js
파일 이름을 webpack.config.development.js
로 바꾸고 webpack.config.production.js
파일을 새로 만든다.
mv webpack.config.js webpack.config.development.js
cp webpack.config.development.js webpack.config.production.js
새로 개발 의존성 라이브러리인 Extract Text Plugin(텍스트 추출 플러그인)을 설치한다. 공식 문서에서 "엔트리 청크(entry chunks)의 모든 필수 *.css
모듈을 별도 CSS 파일로 이동시킨다. 스타일은 JS에 번들링되지 않고 별도의 CSS 파일(styles.css
)로 인라인된다. 스타일 시트 파일 용량이 크더라도 CSS 번들이 JS 번들과 병렬로 로드되기 때문에 더 빠르게 로드되는 장점이 있다."라고 말하고 있다.
yarn add extract-text-webpack-plugin@next -D
📌 웹팩4 사용을 위해 extract-text-webpack-plugin 선 배포판을 설치해야 한다.
webpack.config.production.js
파일을 열고 아래와 같이 변경한다.
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
vendor: ['semantic-ui-react'],
app: './src/index.js'
},
output: {
// 'static' 디렉터리 안에 자바스크립트 번들을 생성한다.
filename: 'static/[name].[hash].js',
// 출력 디렉터리는 'dist' 이다.
// '__dirname' 은 현재 디렉터리의 절대 경로를 제공하는 Node 변수다.
// 'path.resolve'로 디렉터리를 합친다.
// 웹팩4는 출력 경로를 './dist'로 가정하기 때문에 이부분을 그대로 둘 수 있다.
path: path.resolve(__dirname, 'dist'),
publicPath: '/'
},
// 프로덕션 소스맵
devtool: 'source-map',
module: {
rules: [
{
test: /\.(js)$/,
exclude: /node_modules/,
use: ['babel-loader']
},
{
test: /\.css$/,
// 'Extract Text Plugin' 구성한다.
use: ExtractTextPlugin.extract({
// CSS가 추출되지 않으면 loader가 실행된다.
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: {
modules: true,
// @import(ed) 리소스에 css-loader를 적용하기 전 로더를 구성한다.
importLoaders: 1,
camelCase: true,
// CSS 파일을 위해 소스 맵을 생성한다.
sourceMap: true
}
},
{
// css-loader 전에 PostCSS이 실행되어 압축(minify)하고 CSS 룰을 적용하고
// 자동 전처리(autoprefixer)를 실행한다.
// 자동 전처리 단계에서 최신 브라우저 2 사양을 사용한다.
loader: 'postcss-loader',
options: {
config: {
ctx: {
autoprefixer: {
browsers: 'last 2 versions'
}
}
}
}
}
]
})
}
]
},
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: 'initial',
test: 'vendor',
name: 'vendor',
enforce: true
}
}
}
},
plugins: [
new HtmlWebpackPlugin({
template: 'public/index.html',
favicon: 'public/favicon.ico'
}),
// 'styles' 디렉터리 내 스타일시트를 생성한다.
new ExtractTextPlugin({
filename: 'styles/styles.[hash].css',
allChunks: true
})
]
};
port 변수, HMR 및 devServer
와 관련된 플러그인을 제거했다. 또한 프러덕션 구성에 PostCSS를 추가했다. postcss-loader를 설치하고 새 설정 파일을 만든다.
yarn add postcss-loader -D
touch postcss.config.js
postcss.config.js
파일을 열고 아래 코드를 입력한다.
module.exports = {
plugins: [require('autoprefixer')]
};
Postfix가 autoprefixer 플러그인을 사용하도록 했다. (자세한 옵션은 공식 문서를 참고한다.)
배포 빌드하기 전 마지막으로 package.json
에 build
스크립트를 추가해야 한다.
파일을 열고 scripts
부분에 아래 내용을 추가한다.
...
"scripts": {
"dev":"webpack-dev-server --config webpack.config.development.js",
"prebuild": "rimraf dist",
"build": "cross-env NODE_ENV=production webpack -p --config webpack.config.production.js"
},
...
스크립트 명령어에서 start
를 dev
로 수정했고, prebuild
와 build
두 항목을 추가했다.
마지막으로 개발 및 배포 시 어떤 설정 구성을 따라야 하는지를 알려줬다.
-
prebuild
— rimraf 라이브러리를 사용해 build 스크립트 전에 실행하여 마지막 프로덕션 빌드로 생성된dist
디렉토리를 삭제한다. -
build
- 먼저 윈도우 환경에서 cross-env 라이브러리를 사용하고NODE_ENV
으로 환경 변수를 설정한다. 그 다음 웹팩을-p
플래그로 호출해 프로덕션을 위해 빌드 최적화를 지시하고 마지막으로 프로덕션 구성을 지정한다.
package.json
에 명시된 두 의존성을 추가하자.
yarn add rimraf cross-env -D
프로덕션 빌드 생성 전, 새 프로젝트 구조를 살펴보자.
|-- node_modules
|-- public
|-- index.html
|-- favicon.ico
|-- src
|-- components
|-- App.js
|-- DynamicPage.js
|-- Home.js
|-- Layout.css
|-- Layout.js
|-- Loading.js
|-- NoMatch.js
|-- index.js
|-- .babelrc
|-- package.json
|-- postcss.config.js
|-- webpack.config.development.js
|-- webpack.config.production.js
|-- yarn.lock
빌드 명령어로 프로덕션 번들을 생성할 수 있다.
yarn build
위 이미지에서 확인할 수 있듯이, build
스크립트가 실행 후 웹팩은 프로덕션이 준비된 애플리케이션이 들어있는 dist
디렉터리를 만든다. 그리고 생성된 파일을 검사해 압축된 파일인지를 확인하고 원본 소스가 있는지 확인한다. PostCSS
는 CSS 파일에 자동 접두어(autoprefixing)를 추가한다.
이제 프로덕션 파일을 가져와 노드 서버를 실행해 서비스되는 화면을 볼 수 있다.
📌프로덕션 파일 실행을 위해 quick-node-server을 사용했다.
개발 구성 웹팩과 프로덕션 구성 웹팩 두 가지가 있다. 그러나 두 파일 설정 항목이 비슷하여 서로 공유 가능하게 만들어보자. 이를 위해 다음 단계에서 웹팩 구성을 다시 수정해보자.
6. 웹팩 구성
이 부분은 션 라킨(Sean Larkin)의 웹팩 아카데미(Webpack Academy)을 바탕으로 작성됐다. 웹팩 아카데미에서 리액트와 웹팩에 대해 자세히 배우기를 권한다.
webpack-merge와 Chalk를 개발 의존성으로 설치하자.
yarn add webpack-merge chalk -D
build-utils/addons
디렉터리를 만들고 새 파일을 추가한다.
mkdir -p build-utils/addons
cd build-utils
touch build-validations.js common-paths.js webpack.common.js webpack.dev.js webpack.prod.js
현재 프로젝트 폴더 구조는 아래와 같을 것이다.
|-- build-utils
|-- addons
|-- build-validations.js
|-- common-paths.js
|-- webpack.common.js
|-- webpack.dev.js
|-- webpack.prod.js
|-- node_modules
|-- public
|-- index.html
|-- favicon.ico
|-- src
|-- components
|-- App.js
|-- DynamicPage.js
|-- Home.js
|-- Layout.css
|-- Layout.js
|-- Loading.js
|-- NoMatch.js
|-- index.js
|-- .babelrc
|-- package.json
|-- postcss.config.js
|-- webpack.config.development.js
|-- webpack.config.production.js
|-- yarn.lock
common-paths.js
파일을 열고 아래 코드를 작성한다.
const path = require('path');
const PROJECT_ROOT = path.resolve(__dirname, '../');
module.exports = {
projectRoot: PROJECT_ROOT,
outputPath: path.join(PROJECT_ROOT, 'dist'),
appEntry: path.join(PROJECT_ROOT, 'src')
};
파일명에서 유추할 수 있듯이 웹팩 구성의 공통 경로를 정의했다. PROJECT_ROOT
는 build-utils
디렉터리 (프로젝트의 실제 최상단(root) 경로에서 한 레벨 아래에 위치) 내에서만 작업하는 것과 같이 단일 디렉터리만 확인해야 한다.
build-validations.js
파일을 열고 아래 코드를 작성한다.
const chalk = require('chalk');
const ERR_NO_ENV_FLAG = chalk.red(
`You must pass an --env.env flag into your build for webpack to work!`
);
module.exports = {
ERR_NO_ENV_FLAG
};
앞으로 package.json
을 수정할 때 스크립트에 -env.env
플래그가 필요하다. 플래그가 있는지를 유효성 판단한다. 플래그가 없으면, 오류를 발생시킨다.
다음으로 웹팩 환경 설정 파일을 개발과 프로덕션 공통, 개발, 프로덕션 세 파일로 나눠보자.
먼저 webpack.common.js
파일을 열고 아래 코드를 입력한다.
const commonPaths = require('./common-paths');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const config = {
entry: {
vendor: ['semantic-ui-react']
},
output: {
path: commonPaths.outputPath,
publicPath: '/'
},
module: {
rules: [
{
test: /\.(js)$/,
exclude: /node_modules/,
use: ['babel-loader']
}
]
},
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: 'initial',
test: 'vendor',
name: 'vendor',
enforce: true
}
}
}
},
plugins: [
new HtmlWebpackPlugin({
template: 'public/index.html',
favicon: 'public/favicon.ico'
})
]
};
module.exports = config;
webpack.config.development.js
와 webpack.config.production.js
에서 공통 항목을 빼내서 common-paths.js
파일을 만들었다. 이를 위해 output.path
경로를 common-paths.js
로 설정했다.
webpack.dev.js
파일을 열고 개발 설정 내용을 떼어 옮긴다.
commonPaths = require('./common-paths');
const webpack = require('webpack');
const port = process.env.PORT || 3000;
const config = {
mode: 'development',
entry: {
app: `${commonPaths.appEntry}/index.js`
},
output: {
filename: '[name].[hash].js'
},
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
modules: true,
camelCase: true,
sourceMap: true
}
}
]
}
]
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: {
host: 'localhost',
port: port,
historyApiFallback: true,
hot: true,
open: true
}
};
module.exports = config;
webpack.prod.js
파일을 열고 프로덕션 설정 내용을 떼어 옮긴다.
const commonPaths = require('./common-paths');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const config = {
mode: 'production',
entry: {
app: [`${commonPaths.appEntry}/index.js`]
},
output: {
filename: 'static/[name].[hash].js'
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
camelCase: true,
sourceMap: true
}
},
{
loader: 'postcss-loader',
options: {
config: {
ctx: {
autoprefixer: {
browsers: 'last 2 versions'
}
}
}
}
}
]
})
}
]
},
plugins: [
new ExtractTextPlugin({
filename: 'styles/styles.[hash].css',
allChunks: true
})
]
};
module.exports = config;
이제 모든 것을 통합할 차례다. 프로젝트 루트 디렉터리에서 기존 웹팩 구성을 삭제하고 새로운 웹팩 구성을 만든다. 새 파일인 webpack.config.js
로 만든다.
cd ..
rm webpack.config.development.js webpack.config.production.js
touch webpack.config.js
webpack.config.js
을 설정하기 전에, package.json
파일을 열어 scripts
부분을 업데이트한다.
...
"scripts": {
"dev": "webpack-dev-server --env.env=dev",
"prebuild": "rimraf dist",
"build": "cross-env NODE_ENV=production webpack -p --env.env=prod"
},
...
-config
플래그를 제거했기 때문에 웹팩은 webpack.config.js
에서 기본 구성을 찾는다. 이제 -env 플래그를 사용해 웹팩 환경 변수를 전달하고 개발 환경에서는 env=dev
를, 프로덕션 환경에서는 env = prod
를 전달한다.
webpack.config.js
파일을 열고 아래 내용을 입력한다.
const buildValidations = require('./build-utils/build-validations');
const commonConfig = require('./build-utils/webpack.common');
const webpackMerge = require('webpack-merge');
// 애드온(addon)으로 웹팩 플러그인을 추가할 수 있다.
// 개발할 때마다 실행할 필요가 없다.
// '번들 분석기(Bundle Analyzer)'를 설치할 때가 대표적인 예다.
const addons = (/* string | string[] */ addonsArg) => {
// 애드온(addon) 목록을 노멀라이즈(Normalized) 한다.
let addons = [...[addonsArg]]
.filter(Boolean); // If addons is undefined, filter it out
return addons.map(addonName =>
require(`./build-utils/addons/webpack.${addonName}.js`)
);
};
// 'env'는 'package.json' 내 'scripts'의 환경 변수를 포함한다.
// console.log(env); => { env: 'dev' }
module.exports = env => {
// 'buildValidations'를 사용해 'env' 플래그를 확인한다.
if (!env) {
throw new Error(buildValidations.ERR_NO_ENV_FLAG);
}
// 개발 또는 프로덕션 모드 중 사용할 웹팩 구성을 선택한다.
// console.log(env.env); => dev
const envConfig = require(`./build-utils/webpack.${env.env}.js`);
// 'webpack-merge'는 공유된 구성 설정, 특정 환경 설정, 애드온을 합친다.
const mergedConfig = webpackMerge(
commonConfig,
envConfig,
...addons(env.addons)
);
// 웹팩 최종 구성을 반환한다.
return mergedConfig;
};
리액트 개발 환경 구축을 위해 설정할 내용이 너무 많아보이지만 장기적으로는 매우 유용한 방법이다.
애플리케이션을 실행해 프로덕션 파일이 빌드되면서 모든 기능이 예상대로 잘 작동되는지 확인하자.
yarn dev
yarn build
6. 보너스: 웹팩 번들 분석기 (Webpack Bundle Analyzer) 설치하기
웹팩 번들 분석기(Webpack Bundle Analyzer)를 꼭 설치할 필요는 없지만 손쉽게 번들을 최적화할 수 있어 편리하다.
webpack-bundle-analyzer를 설치하고 설정 파일을 만든다.
yarn add webpack-bundle-analyzer -D
touch build-utils/addons/webpack.bundleanalyzer.js
webpack.bundleanalyzer.js
파일을 열고 아래 코드를 입력한다.
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'server'
})
]
};
플러그인 부분을 뺴서 웹팩 번들 분석기에 추가했다. webpack-merge
이 플러그인을 결합하여 마지막으로 웹팩 구성 설정인 webpack.config.js
로 전달한다.
마지막으로 package.json
파일을 열어 새 스크립트를 추가한다.
"scripts": {
"dev": "webpack-dev-server --env.env=dev",
"dev:bundleanalyzer": "yarn dev --env.addons=bundleanalyzer",
"prebuild": "rimraf dist",
"build": "cross-env NODE_ENV=production webpack -p --env.env=prod",
"build:bundleanalyzer": "yarn build --env.addons=bundleanalyzer"
},
dev:bundleanalyzer
—dev
스크립트를 불러 새 환경 변수인addons=bundleanalyzer
를 전달한다.build:bundleanalyzer
—build
스크립트를 불러 새 환경 변수인addons=bundleanalyzer
를 전달한다.
이제 웹팩 번들 분석기를 실행해보자.
yarn dev:bundleanalyzer
웹팩 번들 분석기 외에도 많은 플러그인이 있다. 여러 플러그인을 웹팩 구성 애드온(addon)에 추가하여 유용하게 사용할 수 있다.
맺는 말
여기까지 오느라 정말 수고했다. 그리고 여러분은 모두 해냈다! 🎉 리액트 개발을 위한 웹팩 설정 기초에 대해 배웠다. 앞으로 심화된 테크닉과 기술들을 탐험하고 사용할 수 있을 것이다. 이 튜토리얼 코드는 깃허브에서 다운받을 수 있다.
이 튜토리얼을 읽으면서 재미를 느꼈길 바란다. 질문이나 제안은 언제나 환영한다. 미디엄, 트위터, 깃허브로 언제든지 연락하길 바란다.