이 글은 제로초님의 웹 게임 강좌를 듣고 정리한 글입니다.
react 프로젝트를 시작할 때 가장 편리한 방법인 create-react-app이 어떻게 동작하는지에 대해서 알아보자.
CRA로 react를 시작하면 온전히 react의 문법에 집중할 수 도 있겠지만 webpack같은 번들링 도구들이 어떻게 동작하는지에 대해서는 알 수 없다. 따라서 오늘은 CRA 없이 손수 프로젝트를 만들면서 어떤 방식으로 react 프로젝트가 생성되는지 배워봅시다.
먼저, 웹 게임을 react로 만들어볼 예정이기 때문에 웹 게임 폴더를 생성한다.
(이해를 돕기위한 파일 구조)react-webgame > lecture + 1.구구단 + 2.끝말잇기 + 3.숫자야구...
우리는 lecture 폴더 안의 index.html안에서 여러 가지 컴포넌트를 작성하고 랜더링하는 방식을 취했었다. 그런데, 이러한 방법은 컴포넌트 수가 늘어나면 유지 보수가 불가능에 가까워진다. 따라서 컴포넌트를 분리할 필요가 생겼다. 이렇게 분리한 여러 개의 자바스크립트 파일을 webpack이라는 도구로 하나의 자바스크립트 파일로 번들링할 수 있게 되었다. 파일을 하나로 합치게 되면서 바벨을 사용할 수 도 있게 되었고 반복되는 코드도 삭제할 수 있게 되었다.
webpack을 사용하려면 node를 알아야 한다. 많이 오해하는 것들 중 하나가 node를 서버라고 생각한다. node는 서버가 아니라 자바스크립트 실행기일 뿐이다. 자바스크립트인 webpack을 실행하려면 자바스크립트 실행기인 node가 필요하다.
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에 바로 반영되는 것을 알 수 있다.
{
"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 써서 설치해준다.
{
"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는 실서비스에서도 사용될 것들이 들어가 있다.
lecture 폴더에 webpack.config.js 라는 파일을 생성해준 후, 다음 코드를 작성한다.
module.exports = {};
또, client.jsx 라는 파일 생성한 후, 다음 코드를 작성한다. 확장자 이름이 jsx인 이유는 해당 파일에 jsx 문법을 쓴다면 확장자 이름을 jsx라고 해주는 것이 좋다.(굳이 파일을 확인하지 않아도 이 파일이 react 파일인 것을 알 수 있기 때문)
// 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로 가보자.
<!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가 바로 우리가 필요한 여러 개의 자바스크립트 파일을 하나로 합친 파일이 될 것이다.
파일을 분리할 때 주의할 점은, 필요한 패키지나 라이브러리를 항상 불러와야한다.
// 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;
// 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로 합쳐보자.
// 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에서 파일 확장자 이름을 생략해도 된다.
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이 알아서 파일을 찾아준다.
이제 terminal에 npx webpack
만 입력하면 합쳐진다. dist폴더 아래에 app.js가 생긴다. 이제, index.html을 열면 에러가 있다.
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로 설치
// 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 파일을 들어가보면 내가 작성하지 않은 코드들이 있는 것을 알 수 있다.
index.html을 live server로 구동하면 WordRelay.js에서 react로 만든 태그가 짠하고 나타난다.
이것이 바로 웹팩을 사용하는 이유다!
webpack.config.js에서 모듈의 babel-loader 설정에서 presetsd와 plugins에 대해서 추가적으로 살펴보자.
presets는 plugin들의 모임이다. 여러 개의 plugin들을 preset이라고 한다. 여러 개의 플러그들의 모임이다 보니 하나의 preset이더라도 그 안에서 설정해주어야 하는 것들이 있다.
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에 어떤 걸 적어야 하는지 알려주는 페이지가 있다.
"browserslist": [
"last 1 version",
// 세계 브라우저 점유율 1%이상 인것만 설정
"> 1%",
// 나라 이름을 설정해주는 것도 가능하다.
"> 5% in KR"
"IE 10",
// 죽지 않은 브라우저 모두 지원
"not dead"
]
또 하나 알아야할 것이 있는데, 바로 plugins다. webpack에서 babel-loader에도 plugins가 들어가 있지만 웹팩에서 기본적으로 합쳐주는 module 이외에 추가적으로 설치하고 싶은게 있다면 plugins를 또 사용할 수 있다. plugin은 그냥 확장 프로그램이다 라고 이해해도 좋다.
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도 최소한의 것만 남기고 다 남긴다.) 지운 후에 에러 코드가 필요하다고 하는 것들을 추가하면서 각각의 것들이 왜 필요한지 알아가는 것이 좋다.
webpack으로 빌드까지 했는데 한가지 불편한 점이있다. 수정 사항이 생길때마다 npx webpack
을 해줘야한다는 것이다. 수정 사항이 생기면 바로 반영이 되도록 설정해보자.
$ npm i react-refresh @pmmmwh/react-refresh-webpack-plugin -D
refresh 플러그인을 개발용으로 설치한후, 개발용 서버를 만들자.
$ npm i -D webpack-dev-server
package.json에서 scripts 명령어를 수정해준다.
"scripts": {
"dev": "webpack serve --env development"
}
다운받은 webpack 패키지를 사용할 수있도록 webpack.config.js에서 설정해주자.
// webpack.config.js
// 추가할 사항
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// module의 플러그 아님
plugins: [
new ReactRefreshWebpackPlugin()
],
// module안의 plugins
plugins: [
"@babel/plugin-proposal-class-properties"
// 추가
// 바벨이 최신 문법 바꿀때 핫리로딩 기능까지 추가시켜줌
'react-refresh/babel'
]
// output에서 개발용 서버 설정
output: {
path: path.join(__dirname, 'dist'),
filename: 'app.js',
publicPath: '/dist/',
},
devServer: {
// output의 publicpath 그대로 적어줌
publicPath: '/dist/',
hot: true,
},
우리가 webpack.config.js에 적어준대로 빌드의 결과물을 돌리고 dist 폴더에 결과물을 메모리로 저장을 해준다. index.html을 실행하면 이 메모리에 저장된 결과를 보여준다.
webpack-dev-server는 변경점을 감지할 수 있는 기능이 있는데 그 기능이 바로 핫리로딩이다. 소스코드에 변경 사항이 생기면 dist 폴더에 수정해서 저장한다.
npm run dev
를 실행하면 http://localhost:8080/을 제공한다.
이제 변경사항이 생길때마다 npx webpack
해주지 않아도 자동으로 변경 사항이 저장되고 반영된다.
그런데 여기서 의문을 가질 수 있다. 우리가 핫리로딩을 사용하기 위해 받았던 두가지,
npm i react-refresh @pmmmwh/react-refresh-webpack-plugin -D
react-refresh와 plugin 이 두 가지가 없어도 리로딩이 된다고 생각할 수도 있다. 이는 핫리로딩이 아니고 리로딩이다. 핫리로딩과는 다르다. 리로딩은 변경점이 생기면 새로고침을 해준다. 새로고침을 하게되면 기존 데이터가 다 날아가게 된다.
하지만 핫리로딩은 기존 데이터를 유지하면서 화면을 바꿔준다. 예를 들어, 우리가 5단계가 있는데 그 중 3단계까지 완료한 후, 4단계에서 소스코드를 수정했다고 치자. 핫리로딩을 한다면 3단계에서부터 시작할 수 있지만 리로딩을 한다면 1단계에서부터 다시해야한다.
잘 와닿지 않는다면 저 두가지를 삭제하고 해보도록 하자.