CRA의 원리(babel, webpack 빌드하기)

이 글은 제로초님의 웹 게임 강좌를 듣고 정리한 글입니다.

react 프로젝트를 시작할 때 가장 편리한 방법인 create-react-app이 어떻게 동작하는지에 대해서 알아보자.

CRA로 react를 시작하면 온전히 react의 문법에 집중할 수 도 있겠지만 webpack같은 번들링 도구들이 어떻게 동작하는지에 대해서는 알 수 없다. 따라서 오늘은 CRA 없이 손수 프로젝트를 만들면서 어떤 방식으로 react 프로젝트가 생성되는지 배워봅시다.

먼저, 웹 게임을 react로 만들어볼 예정이기 때문에 웹 게임 폴더를 생성한다.

react-webgame > lecture + 1.구구단 + 2.끝말잇기 + 3.숫자야구…

스크린샷 2020-12-13 오후 8 18 47 (이해를 돕기위한 파일 구조)

우리는 lecture 폴더 안의 index.html안에서 여러 가지 컴포넌트를 작성하고 랜더링하는 방식을 취했었다. 그런데, 이러한 방법은 컴포넌트 수가 늘어나면 유지 보수가 불가능에 가까워진다. 따라서 컴포넌트를 분리할 필요가 생겼다. 이렇게 분리한 여러 개의 자바스크립트 파일을 webpack이라는 도구로 하나의 자바스크립트 파일로 번들링할 수 있게 되었다. 파일을 하나로 합치게 되면서 바벨을 사용할 수 도 있게 되었고 반복되는 코드도 삭제할 수 있게 되었다.

webpack을 사용하려면 node를 알아야 한다. 많이 오해하는 것들 중 하나가 node를 서버라고 생각한다. node는 서버가 아니라 자바스크립트 실행기일 뿐이다. 자바스크립트인 webpack을 실행하려면 자바스크립트 실행기인 node가 필요하다.

npm init

node를 설치하고 npm init을 구동해보자.

$ npm init -y

npm = Node Package Manager

말 그대로 node의 패키지를 관리해주는 매니저이다. npm init을 하면 딸려오는 package.json 파일에 패키지에 대한 정보가 담겨있다.

우리는 react를 사용할 예정이기 떄문에 react와 react dom을 설치한다.

$ npm install react react-dom

설치가 완료되면 package.json에 바로 반영되는 것을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "lecture",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "jaimejam",
"license": "MIT",
"dependencies": {
"react": "^17.0.1",
"react-dom": "^17.0.1"
}
}

webpack도 설치해보자.

$ npm install -D webpack webpack-cli

-D는 개발용으로만 쓰겠다는 뜻이다. webpack은 실서비스에서는 사용하지 않고 개발용으로만 사용하기 때문에 -D 써서 설치해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"name": "lecture",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "jaimejam",
"license": "MIT",
"dependencies": {
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
"devDependencies": {
"webpack": "^5.10.1",
"webpack-cli": "^4.2.0"
}
}

devDependencies에 포함된 것들은 개발용으로만 설치된것들이고, dependencies는 실서비스에서도 사용될 것들이 들어가 있다.

webpack.config.js & client.jsx 파일 생성

lecture 폴더에 webpack.config.js 라는 파일을 생성해준 후, 다음 코드를 작성한다.

1
module.exports = {};

또, client.jsx 라는 파일 생성한 후, 다음 코드를 작성한다. 확장자 이름이 jsx인 이유는 해당 파일에 jsx 문법을 쓴다면 확장자 이름을 jsx라고 해주는 것이 좋다.(굳이 파일을 확인하지 않아도 이 파일이 react 파일인 것을 알 수 있기 때문)

1
2
3
4
5
// npm으로 설치한 react와 react dom을 불러온다.
const React = require('react');
const ReactDom = require('react-dom');

ReactDom.render(<WordRelay />, document.querySelector('#root'));

npm으로 설치한 react와 react dom을 불러오게 되면, CDN으로 파일(react, babel)을 불러오지 않아도 된다.

lecture 디렉토리 안의 index.html로 가보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="ko">

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<title>끝말잇기</title>
</head>

<body>
<div id="root"></div>

<script src="./dist/app.js"></script>
</body>

</html>

불러온 CDN을 모두 지우고 app.js파일 하나만 불러온다. 이 app.js가 바로 우리가 필요한 여러 개의 자바스크립트 파일을 하나로 합친 파일이 될 것이다.

파일을 분리할 때 주의할 점은, 필요한 패키지나 라이브러리를 항상 불러와야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// wordRelay.js

// 필요한 패키지인 React 불러오기
const React = require('react');
const { Component } = React;

class WordRelay extends Component {
state = {
text: 'Hello webpack',
};

render() {
return <h1>{this.state.text}</h1>
}
}

// 다른 파일에서 불러올 수 있도록 내보내주는 것도 잊지말기
modult.exports = WordRelay;

1
2
3
4
5
6
7
8
// client.jsx
const React = require('react');
const ReactDom = require('react-dom');

// 쓰고싶은 js 파일만 불러올 수 있다.
const WordRelay = require('./WordRelay');

ReactDom.render(<WordRelay />, document.querySelector('#root'));

이제 이 두개의 파일(client.jsx & wordRelay.js)을 webpack을 사용해서 하나의 파일인 app.js로 합쳐보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// webpack.config.js

// node에서 path를 가져온다.
const path = require('path');

module.exports = {
name: 'wordrelay-setting',
// 실서비스는 production이라고 해준다. 지금은 개발용
mode: 'development',
// eval = 빠르게 하겠다
devtool: 'eval',

// 입력(제일 중요)
entry: {
app: ['./client.jsx', 'wordRelay.js'],
},
// 출력(제일 중요)
output: {
// path.join : 경로 합치기 (현재폴더경로, 현재 폴더안의 dist)
path: path.join(__dirname, 'dist'),
filename: 'app.js'
}.
};

입력 = 두개의 파일 , 출력 = app.js (두 개의 파일을 하나의 파일로 합치겠다.) 그런데, client에서 wordRelay를 불러오기 때문에 client.jsx만 작성해도 된다.

추가적으로 resolve를 작성한다면 entry에서 파일 확장자 이름을 생략해도 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
name: 'wordrelay-setting',
// 실서비스는 production이라고 해준다.
mode: 'development',
devtool: 'eval',
resolve: {
extensions: ['.jsx', '.js']
}


entry: {
app: ['./client'],
},

output: {
path: path.join(__dirname, 'dist'),
filename: 'app.js'
}.
};

extensions에 사용할 파일의 확장자명을 적어주면 webpack이 알아서 파일을 찾아준다.

babel 설치

이제 terminal에 npx webpack만 입력하면 합쳐진다. dist폴더 아래에 app.js가 생긴다. 이제, index.html을 열면 에러가 있다.

스크린샷 2020-12-13 오후 8 45 26

jsx 문법을 이해하지 못한다고 하는데, 바벨을 설치하지 않아서 그렇다. 바벨 설치해주자.

바벨의 기본 기능 설치

$ npm i -D @babel/core

최신 문법 js를 다운그레이드 시켜주는 기능

$ npm i -D @babel/preset-env

jsx 변환해주는 기능

$ npm i -D @babel/preset-react

바벨과 webpack 연결해주는 기능

$ npm i -D babel-loader

바벨은 개발할 때만 필요하므로 -D로 설치

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js

// module을 추가해준다.
// entry에서 입력을 받아서 module을 적용한 후, output으로 보내겠다는 의미이다.
module: {
rules: [{
// js 파일과 jsx 파일에 적용하겠다. 어떤 것을? babel-loader를
test: /\.jsx?/,
// babel=loader : 최신 문법을 다운그레이드
loader: 'babel-loader',
// babel의 옵션들
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
// class 문법을 사용하기 위해 추가해주었다.
plugins: ["@babel/plugin-proposal-class-properties"],
},
}],
},

이제 npx webpack해주면 진짜 끝! app.js 파일을 들어가보면 내가 작성하지 않은 코드들이 있는 것을 알 수 있다.

스크린샷 2020-12-14 오전 10 30 10

index.html을 live server로 구동하면 WordRelay.js에서 react로 만든 태그가 짠하고 나타난다.

스크린샷 2020-12-14 오전 10 39 13

이것이 바로 웹팩을 사용하는 이유다!

참고로 알아야 할 webpack 지식

webpack.config.js에서 모듈의 babel-loader 설정에서 presetsd와 plugins에 대해서 추가적으로 살펴보자.

presets는 plugin들의 모임이다. 여러 개의 plugin들을 preset이라고 한다. 여러 개의 플러그들의 모임이다 보니 하나의 preset이더라도 그 안에서 설정해주어야 하는 것들이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module: {
rules: [{
test: /\.jsx?/,
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: {
browsers: ['last 2 chrome versions'],
}
}],
['@babel/preset-react'],
],
plugins: ["@babel/plugin-proposal-class-properties"],
},
}],
},

preset-env가 최신 자바스크립트 문법을 여러 브라우저에서 호환가능 하도록 다운그레이드 시켜주는 기능이었는데 이를 구체화해서 적용할 수 도 있다.

target을 설정해서 위 코드처럼 browsers: ['last 2 chrome versions']이렇게 작성한다면 IE 지원 하지 않고, 크롬 최신 2개 버전만 지원하도록 설정한 것이다. 이걸 설정해주는것이 중요한데, 왜냐하면 더 옛날 문법으로 변환할수록 바벨이 할일이 많아져 더 느려질 수 있기 때문이다.

이 target에 어떤 걸 적어야 하는지 알려주는 페이지가 있다.

https://github.com/browserslist/browserslist

1
2
3
4
5
6
7
8
9
10
"browserslist": [
"last 1 version",
// 세계 브라우저 점유율 1%이상 인것만 설정
"> 1%",
// 나라 이름을 설정해주는 것도 가능하다.
"> 5% in KR"
"IE 10",
// 죽지 않은 브라우저 모두 지원
"not dead"
]

또 하나 알아야할 것이 있는데, 바로 plugins다. webpack에서 babel-loader에도 plugins가 들어가 있지만 웹팩에서 기본적으로 합쳐주는 module 이외에 추가적으로 설치하고 싶은게 있다면 plugins을 또 사용할 수 있다. plugin은 그냥 확장 프로그램이다 라고 이해해도 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  module: {
rules:
[{
test: /\.jsx?/,
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: {
browsers: ['last 2 chrome versions'],
}
}],
['@babel/preset-react'],
],
plugins: ["@babel/plugin-proposal-class-properties"],
},
}],
},

plugins:
[
new webpack.LoaderOptionsPlugin({ debug: true}),
],

실무에서는 이 플러그인에 10줄정도 들어가있는 코드를 만나게 될 수 있다. 그럴때엔 최소한의 플러그인들만 남겨놓고(rules도 최소한의 것만 다 남긴다.) 지운 후에 에러 코드가 필요하다고 하는 것들을 추가하면서 각각의 것들이 왜 필요한지 알아가는 것이 좋다.