❗ React 및 JS 기초에 대해 설명하진 않습니다. 초보자를 위한 내용이 아닙니다.
이 글은 2020-10-01 이후로 업데이트 하지 않습니다.
Typescript에 집중하려고 합니다.
✅ webpack v4 기준입니다.
📚 모든 내용은 공식 문서를 기반으로 작성되었습니다.
다음의 개발 환경을 구성합니다.
⭐ 모든 설정을 따라할 필요는 없습니다. 프로젝트 성격에 맞게 활용하세요.
CRA가 있는데 굳이 힘들게 직접 만드는 이유가 무엇이냐?
라고 묻는다면 필자는 장점을 말씀드리진 못합니다.
build time
도 ESLint와 Type Checker를 설치하면 CRA
에 비해 5~10초 정도 감소하는 정도이며
ESLint를 사용하지 않으면 build time
은 CRA
에 비해 2배 넘게 감소합니다.
node_modules의 파일 개수
도 직접 만들었더라도 패키지를 많이 설치하면 CRA
를 초과할 수 있습니다.
소규모 프로젝트 정도는 대부분 CRA
만으로 개발이 가능한데, 규모가 커지면 eject
를 해야하는 상황이 생깁니다.
이럴 때 어차피 eject 해야할 거 그냥 직접 환경 만들자
가 되는 것이라고 생각합니다.
webpack.config.js
가 670 line인 상태에서 수정하는 것과 백지인 상태에서 수정하는 것은 완전 다르니까요.
필자가 20일 넘게 webpack
을 연구한 결과, 172 line만으로 CRA
와 동일한 chunk size
와 유사한 CUI
를 구현할 수 있었습니다.
필자는 직접 개발 환경을 만드는 것을 위와 같이 생각합니다.
필자가 만든 https://mo-gak-ko.xyz/ 를 npm
과 yarn
두 개로 각각 패키지를 설치하고, production build
를 진행해보았습니다.
npm
yarn
노란색
으로 표시된 부분은 node_modules
에 대한 chunk
인데,
package manager
를 변경했다는 것만으로 bundle 크기
가 줄어듭니다.
패키지 종속성 관리를 더 가볍게 해주는 것 같네요.
✅ CRA의 directory 구조를 최대한 따라해보겠습니다.
프로젝트 디렉터리를 하나 생성합니다.
mkdir [project_name]
하위 디렉터리를 생성합니다.
cd [project_name]
mkdir src build public
public/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" />
<title>React without CRA</title>
</head>
<body>
<div id="root"></div> 👈 App이 보여지는 element
</body>
</html>
리액트의 기본 패키지를 설치합니다.
yarn init
yarn add react react-dom
src/App.js
와 src/index.js
를 작성합니다.
📚 src/App.js
👇
import React from "react";
function App() {
return <>work</>
}
export default App;
---------------------------------
📚 src/index.js
👇
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
📌 @babel/preset-env docs
📌 @babel/preset-react docs
ES6+ 문법으로 개발을 하니 트랜스파일을 해주어야 합니다.
핵심 패키지 설치
yarn add @babel/core @babel/preset-env @babel/preset-react -D
Babel config file을 만드는 방법은 여러가지가 있는데, 필자는 그 중 package.json
을 활용하겠습니다.
📚 package.json
👇
{
...
"babel": {
"presets": [
"@babel/preset-react",
"@babel/preset-env"
]
}
}
여기서 잠깐!
Q. @babel/preset-env
는 어떤 역할이에요?
A. 최신 Javascript를 사용할 수 있게 babel이 만들어놓은 똑똑한 preset입니다. 이걸 사용하면 삶의 질이 높아지고 Javascript 번들 크기가 작아집니다.
Q. @babel/preset-react
는 어떤 역할이에요?
A. @babel/plugin-syntax-jsx, @babel/plugin-transform-react-jsx, @babel/plugin-transform-react-display-name 세 가지를 포함하는 preset입니다. 플러그인 이름에서 알 수 있듯이 JSX 구문을 해석합니다.
✅ CRA의 build directory 구조를 최대한 따라해봅니다.
프론트의 js같은 경우에는 Module Bundler를 사용해야 하는 이유가 몇 가지 있습니다.
Module Bundler는 webpack, gulp, ... 등 다양하지만, 페이스북이 webpack을 사용하므로 webpack으로 설정하겠습니다.
핵심 패키지 설치
yarn add webpack webpack-cli -D
webpack.config.js
를 생성합니다.
project directory
ㄴ webpack.config.js
https://webpack.js.org/concepts/ 공식 문서를 기반으로 진행하면서 설정하겠습니다.
📌 webpack environment-variables docs
📌 webpack environment-variables option docs
webpack 환경 변수에 따라 development
, production
, none
세 가지로 설정할 수 있습니다.
기본값은 production
입니다.
📚 webpack.config.js
👇
module.exports = (webpackEnv) => {
const isEnvDevelopment = webpackEnv === "development";
const isEnvProduction = webpackEnv === "production";
return {
mode: webpackEnv,
};
};
script 작업할 때
--env development/production
cli 옵션으로 넣어줄 예정입니다.
env 옵션으로 받은 문자열은 webpackEnv로 넘어갑니다.
번들 작업을 할 파일을 선택합니다.
entry: string | [string]
index.js를 entry에 넣어줍니다.
📚 webpack.config.js
👇
const appIndex = path.resolve(__dirname, "src", "index.js");
module.exports = (webpackEnv) => {
...
return {
mode: webpackEnv,
entry: appIndex,
};
};
mode
: development, production 환경 별로 다른 작업을 할당하기 위해 사용함.
번들 결과물이 저장될 경로를 지정합니다.
자주 쓰이는 옵션으로는 path와 filename이 있습니다.
CRA의 build directory는 다음과 같은 구조입니다.
build
ㄴ static
ㄴ js
ㄴ ...
ㄴ css
ㄴ ...
ㄴ media
ㄴ ...
위와 같은 구조가 될 수 있도록 변경해봤습니다.
📚 webpack.config.js
👇
const path = require("path");
const appBuild = path.resolve(__dirname, "build");
module.exports = (webpackEnv) => {
...
return {
...
output: {
path: appBuild,
filename: isEnvProduction
? "static/js/[name].[contenthash:8].js"
: isEnvDevelopment && "static/js/bundle.js",
},
};
};
여기서 잠깐!
Q. __dirname
은 뭐에요?
A. 작업 중인 파일의 위치를 파일명을 제외한 가장 가까운 directory까지 보여주는 예약어입니다.
webpack은 기본 설정에서 .js와 .json만 이해가 가능합니다.
이 항목에서는 '어떤 확장자는 이렇게 처리해라~'같은 설정을 해줍니다.
옵션은 다음과 같습니다.
정규식(regex) 분석
/ ... / : 정규식의 시작과 끝을 나타냄
\.
:.
을 나타내는 escaped character
( ... ) : 문자열을 판별하는 group
a | b : a or b
$ : 해당 정규식으로 끝나는 문자열
그렇다면 React Component( js | jsx )는 ES6+ 문법으로 작성하기 때문에 babel-loader
를 설치해야 합니다.
yarn add babel-loader -D
설치가 끝났다면 babel-loader docs를 보고 설정을 추가합니다.
📚 webpack.config.js
👇
const path = require("path");
const appSrc = path.resolve(__dirname, "src");
module.exports = (webpackEnv) => {
...
return {
...
module: {
rules: [
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
use: "babel-loader",
include: appSrc,
},
],
},
};
};
📌 styled-components로 스타일링할 예정이라 babel-loader만 설정하였음.
매번 build할 때 모든 파일을 확인하다보니 오랜 시간이 걸립니다.
이런 경우 caching
을 하여 최초 build
시에만 모든 파일을 읽고, 다음 build
부터는 cache
로부터 읽어들여 변경사항이 있는 파일만 build하면 빠른 프로덕션 빌드를 얻을 수 있습니다.
babel-loader
에는 해당 옵션이 내장되어 있습니다.
webpack babel-loader docs를 보고 꾸며봤습니다.
📚 webpack.config.js
👇
const path = require("path");
const appSrc = path.resolve(__dirname, "src");
module.exports = (webpackEnv) => {
...
return {
...
module: {
rules: [
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
loader: "babel-loader",
include: appSrc,
options: {
cacheDirectory: true,
cacheCompression: false,
}
},
],
},
};
};
cacheDirectory
: 기본값은false
,true
로 설정 시node_modules/.cache/babel-loader
에 캐시 저장
cacheCompression
: 기본값은true
, 모든 캐시가 Gzip으로 압축됩니다.false
로 해제
React로 개발할 때 이미지를 import해서 가져와야 할 때가 있습니다.
import ReactIcon from "./assets/React.png";
function Sample() {
return (
<img src={ReactIcon} />
<img src={require("./assets/React.png")} />
)
}
이것 처럼요.
file-loader
가 rules에 없는 상태에서 webpack을 실행하면 이런 에러가 나옵니다.
ERROR in ./src/assets/React.png 1:0
Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
(Source code omitted for this binary file)
@ ./src/App.js 4:0-43 11:9-18
@ ./src/index.js
요약하면 이 파일 형식을 처리하기 위해선 적절한 로더가 필요할 수 있으며, 현재 이 파일을 처리하도록 구성된 로더가 없습니다.
입니다.
필요한 패키지를 설치합니다.
yarn add file-loader -D
file-loader docs를 보고 설정을 해봅니다.
📚 webpack.config.js
👇
module.exports = (webpackEnv) => {
...
return {
...
module: {
rules: [
...
{
loader: "file-loader",
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
outputPath: "static/media",
name: "[name].[hash:8].[ext]",
esModule: false,
},
},
],
},
};
};
옵션은 다음과 같습니다.
name : 파일명 지정
outputPath : 파일이 저장될 시스템 경로 지정
publicPath : express.static 같은 느낌의 정적 파일 제공과 유사하다. 만약 static으로 지정되었다면 /static/~.[ext]
로 static 내의 파일에 접근할 수 있다.
기본값이
__webpack_public_path__ + outputPath
라서 특별한 경우가 아니면 outputPath만 설정해줘도 됩니다.
esModule : CommonJS를 사용하기 위한 옵션, 모듈 연결/트리 쉐이킹이 필요한 경우 기본값(true) 권장.
기본값(true) 일 때 console.log(require("./assets/React.png"))의 결과
Module { default: "static/media/React.090b95be.png", Symbol: "Module", _esModule: true }
false일 때 console.log(require("./assets/React.png"))의 결과
static/media/React.090b95be.png
기본적으로 file-loader
와 같은 기능으로 동작합니다. 하지만 차이점이라면 바이트 제한보다 작은 경우 DataURL을 반환합니다.
DataURL은 base64로 제공됩니다.
이것을 언제 사용할까요?
작은 크기의 이미지, 글꼴 등은 복사하지 않고 base64 문자열
형태로 번들 결과물
에 넣습니다.
패키지 설치 및 수정
yarn add url-loader -D
📚 webpack.config.js
👇
module.exports = (webpackEnv) => {
...
return {
...
module: {
rules: [
{ ... },
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: "url-loader",
options: {
limit: 10000,
outputPath: "static/media",
name: "[name].[hash:8].[ext]",
},
},
],
},
};
};
옵션은 다음과 같습니다.
limit
: Boolean | Number | String를 입력으로 받습니다. 기준은Byte
입니다. 10000은 10KB입니다. test하는 확장자가 10KB를 넘으면file-loader
로 작동하고, 10KB 아래면base64
문자열로번들 결과물
에 들어갑니다.
outputPath
: 파일이 저장될 시스템 경로 지정
name
: 파일명 지정❗
outputPath
,name
은 url-loader 문서에는 없는 설정이지만 작동합니다.
브라우저에서도
base64
로 들어가는 것을 볼 수 있습니다.
svg / favicon 등 저용량에 유용합니다.
❗ file-loader
가 필요하다는 ERROR 발생 시 file-loader
를 설치합니다.
CRA
를 최대한 따라한다고 했으니 CRA
에서 사용하는 eslint config
을 진행하겠습니다.
먼저 webpack eslint-loader docs를 보고 설치 및 설정이 필요합니다.
yarn add eslint eslint-loader -D
📚 webpack.config.js
👇
const appSrc = path.resolve(__dirname, "src");
module.exports = (webpackEnv) => {
return {
...
module: {
rules: [
{
test: /\.(js|jsx)$/,
enforce: "pre",
exclude: /node_modules/,
loader: "eslint-loader",
options: {
cache: true,
formatter: isEnvDevelopment
? "codeframe"
: isEnvProduction && "stylish",
},
include: appSrc,
},
]
}
}
}
loader
를 추가했으니 eslint
설정이 필요합니다.
CRA
는 eslint-config-react-app을 사용합니다. 문서를 보면 알 수 있듯이 airbnb처럼 preset같은 느낌이라 간단하게 설정이 가능합니다.
yarn add eslint-config-react-app @typescript-eslint/eslint-plugin@2.x @typescript-eslint/parser@2.x babel-eslint@10.x eslint@6.x eslint-plugin-flowtype@4.x eslint-plugin-import@2.x eslint-plugin-jsx-a11y@6.x eslint-plugin-react@7.x eslint-plugin-react-hooks@2.x -D
📌 20년 9월 23일 기준이므로 문서 참조 필요
이후 .eslintrc.json
혹은 package.json
에 세 줄만 추가하면 됩니다.
📚 .eslintrc.json
👇
{
"extends": "react-app"
}
📚 package.json
👇
"eslintConfig": {
"extends": "react-app"
}
이제부터 Quick Fix
및 Compile warning
을 볼 수 있습니다.
React는 아시다시피 index.html에 번들 작업을 마친 js 파일이 script 태그로 추가되어 작동하는 방식입니다.
그러기 위해서는 public/index.html을 build/ 로 옮겨주면서 번들 결과물이 script 태그로 들어가야 합니다.
이런 역할을 하는 플러그인이 하나 있습니다.
yarn add html-webpack-plugin -D
설치가 끝나면 html-webpack-plugin docs를 보고 설정을 해줍니다.
📚 webpack.config.js
👇
...
const HtmlWebpackPlugin = require("html-webpack-plugin");
const appHtml = path.resolve(__dirname, "public", "index.html");
module.exports = (webpackEnv) => {
...
return {
...
plugins: [new HtmlWebpackPlugin({ template: appHtml })],
};
};
📌 template 옵션을 사용하지 않으면 webpack이 자체적으로 html을 만듦.
CRA에서 .env
로 환경 변수를 설정할 때 지켜야 하는 규칙이 하나 있습니다.
환경 변수 이름은 REACT_APP
으로 시작해야 한다는 것이죠. 지키지 않으면 사용이 불가능합니다.
설정한 환경 변수는 보통 개발/배포 환경에 따라 다르게 설정해야 하는 경우 유용합니다.
const httpLink = new HttpLink({
uri:
process.env.NODE_ENV === "production"
? process.env.REACT_APP_PROD_API_URL
: process.env.REACT_APP_DEV_API_URL,
});
개발 환경에서는 로컬 API 서버에 연결하고, 배포 환경에서는 실제 API 서버에 연결하는 코드입니다.
아래 이미지는 CRA로 만들어진 프로젝트에서 process.env
를 콘솔에 찍어본 이미지입니다.
NODE_ENV
와PUBLIC_URL
그리고REACT_APP
환경 변수만을 가져오죠.
이제 구현해보겠습니다. 먼저, 패키지를 설치합니다.
yarn add dotenv
다음으로 .env
를 생성하여 테스트 값을 채워넣습니다.
📚 .env
👇
REACT_APP_NAME="Larry Jung"
REACT_AP="ddd"
REACT_="dawd"
webpack에는 DefinePlugin이라는 플러그인이 있습니다.
컴파일 시 전역 상수를 구성하는 플러그인입니다.
📚 webpack.config.js
👇
require("dotenv").config();
const webpack = require("webpack");
function getClientEnv(nodeEnv) {
return {
"process.env": JSON.stringify(
Object.keys(process.env)
.filter((key) => /^REACT_APP/i.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{ NODE_ENV: nodeEnv }
)
),
};
} 👆 JS 기본기가 가득한 코드니 따로 설명은 안하겠다.
module.exports = (webpackEnv) => {
...
const clientEnv = getClientEnv(webpackEnv); 👈 webpackEnv 값에 따라 NODE_ENV가 달라짐
return {
plugins: [
...,
new webpack.DefinePlugin(clientEnv),
],
};
};
주의사항
webpack.DefinePlugin({...})
은 반드시 JSON 문자열 포맷으로 작성해야 합니다.
이 후 React 컴포넌트에서 실행해봅니다.
📚 src/App.js
👇
console.log(process.env);
NODE_ENV
는 webpack mode에 따라 webpack이 임의적으로process.env.NODE_ENV
를 추가해줍니다.
그래서process.env
에는 나타나지 않지만process.env.NODE_ENV
로 접근하면 값이 나옵니다.
반드시 NODE_ENV를 넣어줘야 하는 것은 아닙니다.
✅ 환경 변수 세팅을 마치셨습니다.
만약, 필터링 없이 .env
에 적힌 모든 것을 React로 보내고 싶다면 아래와 같이 하면 됩니다.
📚 webpack.config.js
👇
const dotenv = require("dotenv");
module.exports = (webpackEnv) => {
...
return {
plugins: [
...
new webpack.DefinePlugin({
"process.env": JSON.stringify(
Object.assign({}, dotenv.config().parsed, {
NODE_ENV: webpackEnv,
})
)
}),
],
}
}
먼저 배경 지식을 짚고 넘어가겠습니다.
runtime
은 기본적으로 모듈화된 앱을 연결하는데 필요합니다. 모듈을 연결하는데 필요한 로직을 포함합니다.
html
이 브라우저에서 실행되면 필요한 bundle과 asset을 불러와서 연결해야 합니다.
하지만, webpack
의 최적화로 src
는 합쳐지고, 최소화되고, chunk로 나눠집니다.
그렇다면 webpack
은 모듈들을 어떻게 관리할까요? 여기에서 manifest
가 사용됩니다.
manifest
가 무엇이냐면, 컴파일러
가 입력, 확인, 맵핑 과정을 거치면서 모듈에 대한 메모
를 기록합니다.
이 data collection
을 manifest
라고 부릅니다.
data collection
을 번들로 합쳐 브라우저에서 실행되면 모듈을 해석하고 불러오는데 runtime
이 사용됩니다.
Module Format
이 무엇이든 번들된 결과물은 __webpack_require__
로 모듈을 식별합니다.
결론은 manifest data
를 이용하여 식별자 뒤에 있는 모듈을 찾아야 하는 위치를 알아냅니다.
📚 build/bundle.js
👇
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
JS 난독화 작업 전에 볼 수 있는 모습. 난독화된 후에는 찾아볼 수 없다.
✅ 지금까지는 webpack이 보이지 않는 곳에서 어떻게 동작하는지 알아봤습니다.
물론, manifest
가 없어도 홈페이지는 완벽하게 동작하는 것처럼 보입니다.
하지만 브라우저 캐싱을 사용한다면 문제가 발생합니다.
필자는 webpack.output
설정에 [contenthash:8]
을 사용해서 매번 build
마다 파일명
이 변경됩니다.
이 경우 캐싱이 무효화 됩니다.
manifest를 추출하는 방법은 webpack-manifest-plugin을 사용하는 것입니다.
패키지 설치
yarn add webpack-manifest-plugin -D
webpack.config.js
수정
📚 webpack.config.js
👇
const ManifestPlugin = require("webpack-manifest-plugin");
module.exports = (webpackEnv) => {
...
return {
plugins: [
...,
new ManifestPlugin({
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce(
(manifest, { name, path }) => ({ ...manifest, [name]: path }),
seed
);
const entryFiles = entrypoints.main.filter(
(filename) => !/\.map/.test(filename)
);
return { files: manifestFiles, entrypoints: entryFiles };
}), // 📌 추가적인 옵션은 문서 참조
],
};
};
public/index.html
수정
<head>
<link rel="manifest" href="./manifest.json" />
</head>
이후 build에서 manifest.json
이 생깁니다.
📚 build/manifest.json
{
"files": {
"main.js": "/static/js/main.9a18f1f4.chunk.js",
"main.js.map": "/static/js/main.9a18f1f4.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.9b2e54b7.js",
"runtime-main.js.map": "/static/js/runtime-main.9b2e54b7.js.map",
"static/css/2.0d11afa3.chunk.css": "/static/css/2.0d11afa3.chunk.css",
"static/js/2.52bd26e2.chunk.js": "/static/js/2.52bd26e2.chunk.js",
"static/css/2.0d11afa3.chunk.css.map": "/static/css/2.0d11afa3.chunk.css.map",
"static/js/2.52bd26e2.chunk.js.map": "/static/js/2.52bd26e2.chunk.js.map",
"index.html": "/index.html",
"static/js/2.52bd26e2.chunk.js.LICENSE.txt": "/static/js/2.52bd26e2.chunk.js.LICENSE.txt",
"static/media/banner-min.3c026bd5.jpg": "/static/media/banner-min.3c026bd5.jpg"
},
"entrypoints": [
"static/js/runtime-main.9b2e54b7.js",
"static/css/2.0d11afa3.chunk.css",
"static/js/2.52bd26e2.chunk.js",
"static/js/main.9a18f1f4.chunk.js"
]
}
✅ 브라우저 캐싱에 관심 있다면 webpack caching docs를 확인하세요.
cache-loader
와는 다른 용도로 쓰이는 옵션입니다. 그래서 분류도 Other Options
에 속해있습니다.
이것이 어떤 역할을 하냐면, Plugins
에 대한 Caching
을 해줍니다.
예시를 들면 JS compressor toolkit
혹은 css minimizer plugin
등이 Caching
됩니다.
Webpack Other Options - cache docs를 보고 꾸며봤습니다.
📚 webpack.config.js
👇
const dotenv = require("dotenv");
module.exports = (webpackEnv) => {
...
return {
cache: {
type: isEnvDevelopment ? "memory" : isEnvProduction && "filesystem",
}
}
}
생성된 webpack module과 chunk를 캐싱하여 빌드 시간을 단축시킵니다.
development
환경에서는{ type : "memory" }
로 자동 적용됩니다.
production
환경에서는{ type : false }
로 자동 적용됩니다.
📌 https://webpack.js.org/configuration/dev-server/
📌 https://webpack.js.org/guides/development/#using-webpack-dev-server
CRA로 yarn start할 때 자동으로 localhost:3000이 열리는 것을 볼 수가 있는데요.
이와 같은 기능입니다. RAM에 개발 서버를 올려서 구동하는 것이죠.
필요한 패키지 설치
yarn add webpack-dev-server -D
설정을 추가합니다.
📚 webpack.config.js
👇
const appPublic = path.resolve(__dirname, "public");
module.exports = (env) => {
...
return {
...
devServer: {
port: 3000,
contentBase: appPublic,
open: true,
historyApiFallback: true,
overlay: true,
stats: "errors-warnings",
},
};
};
port : 웹 서버가 실행될 PORT 지정
contentBase : 개발 환경에서 정적 파일을 제공하려는 경우 필요합니다.
open : 번들 작업이 끝나면 자동으로 브라우저를 열어주는지
historyApiFallback : 개발 환경에서localhost:3000/subpage
등 URL로 직접 접근하였을 경우,cannot get /subpage
대신에index.html
로 보내줍니다.
overlay : 컴파일러 오류 또는 경고가있을 때 브라우저에 전체 화면 오버레이를 표시
stats : 컴파일(트랜스파일) 시 보여주는 항목 설정
webpack-dev-server로 개발 서버를 실행하면 브라우저 콘솔에 이런 Warning이 나올 것입니다.
DevTools failed to load SourceMap: Could not load content for webpack:///node_modules/sockjs-
client/dist/sockjs.js.map: HTTP error: status code 404, net::ERR_UNKNOWN_URL_SCHEME
이 warning을 해결하는 방법은 devtool 옵션을 추가하는 것입니다.
📚 webpack.config.js
👇
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
module.exports = (env) => {
...
return {
...
devtool: isEnvProduction
? shouldUseSourceMap
? "source-map"
: false
: isEnvDevelopment && "cheap-module-source-map",
};
};
source-map
: 일반적으로production
에서 사용 가능
cheap-module-source-map
: 디버깅을 위한 최상의 옵션
CRA
에는 scripts/
디렉터리 안에 start, build, test 파일이 있습니다.
파일을 생성하기 전에 어떤 코드를 짜야할 지 생각을 해봅시다.
webpack을 production mode
로 실행할 때, webpack compiler 동작 전에 build/
디렉터리를 비워준 뒤,
index.html
을 제외한 모든 public/
파일을 build/
로 복사해야 합니다.
이 동작을 2~3 줄 만으로 끝내는 코드가 있습니다.
📚 fs-extra 패키지를 이용한 방법
👇
fs.emptyDirSync(paths.appBuild);
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
filter: file => file !== paths.appHtml,
});
하지만 필자는 모듈 의존성을 고려하고 코딩하기 때문에 fs-extra
를 사용하지 않을 것입니다.
기본 제공하는 fs
만으로 기능 구현이 가능한데, 굳이 패키지를 추가로 설치해야 하냐는 것이죠.
scripts/prebuild.js
생성
project directory
ㄴ scripts/
ㄴ prebuild.js
내용 작성
📚 scripts/prebuild.js
👇
const path = require("path");
const fs = require("fs");
const APP_DIR = process.cwd();
const BUILD_DIR = path.resolve(APP_DIR, "build");
const PUBLIC_DIR = path.resolve(APP_DIR, "public");
function getBuildPath(name) {
return path.resolve(BUILD_DIR, name);
}
function getPublicPath(name) {
return path.resolve(PUBLIC_DIR, name);
}
// 인자 dir 내 파일이 존재하면 비우고 폴더가 존재하지 않으면 만듬
function emptyDir(dir) {
if (fs.existsSync(dir)) {
fs.readdir(dir, (_, files) => {
files.forEach((item) => {
if (/_|\.[\w]{1,}/.test(item)) {
fs.unlinkSync(getBuildPath(item));
} else {
fs.rmdirSync(getBuildPath(item), { recursive: true });
}
});
});
} else {
fs.mkdirSync(dir);
}
}
// `passList: Array<string>`를 제외한 모든 public 파일을 build/로 복사
function copyPublic(passList) {
fs.readdir(PUBLIC_DIR, (_, files) => {
files.forEach((item) => {
if (!passList.includes(item)) {
fs.copyFileSync(getPublicPath(item), getBuildPath(item));
}
});
});
}
emptyDir(BUILD_DIR);
copyPublic(["index.html"]);
확실히 fs-extra
를 사용하는 것 보다는 복잡해보입니다. 하지만 따지고 보면 직접 구현한 코드가 더 짧습니다.
무슨 🐶소리냐구요?
node_modules/fs-extra/lib/copy-sync.js의 LOC는 130입니다. (copySync 기능만 포함)
scripts/prebuild.js
의 LOC는 38입니다. (모든 기능 포함)
더 이상의 설명은 생략합니다.
✅ webpack command-line docs에 모든 cli 옵션이 적혀있습니다.
이제 Module Bulder를 설정했으니 CRA처럼 yarn start
를 입력하면 개발 서버가 실행되는 기능을 구현해야 합니다.
📚 package.json
👇
"scripts": {
"prebuild": "node scripts/prebuild",
"build": "webpack -p --env production",
"start": "webpack-dev-server --env development"
},
📌 options
👇
--progress : 진행 상황 표시
--watch, -w : 파일 변화 감지
--env larry : webpack.config.js에 환경변수 전달 >> "larry"
--env.name=larry : webpack.config.js에 환경변수 전달 >> { name: "larry" }
📌 shortcuts
👇
-d : --debug --devtool cheap-module-eval-source-map --output-pathinfo
-p : --mode production
yarn start
를 입력한 뒤 localhost:3000
으로 접속해보세요.
yarn start
와 yarn build
를 실행해보면 아시겠지만 필자만 이렇게 생각하는진 모르겠는데
build result를 지저분하게 보여줍니다.
이럴 때 사용하는 것이 stats
옵션입니다.
stats
이란 webpack compiler
가 동작하였을 때 출력하는 내용물을 결정하는 옵션입니다.
CRA
와 필자가 설정한 옵션
을 한번 보시죠
둘 다 동일하게 assets
, time
, publicPath
을 동일하게 보여줍니다.
차이점이라면 CRA
는 gzip size
를 보여주고 Custom
은 parse size
를 보여준다는 것이죠.
chunk
는 Code Splitting
과 관련된 내용입니다.
webpack stats docs를 보고 필자 입맛에 맞게 바꿔봤습니다.
📚 webpack.config.js
👇
module.exports = (webpackEnv) => {
return {
...
stats: {
builtAt: false,
children: false,
entrypoints: false,
hash: false,
modules: false,
version: false,
publicPath: true,
excludeAssets: [/\.(map|txt|html|jpg|png)$/, /\.json$/],
warningsFilter: [/exceed/, /performance/],
},
};
};
❗ webpack을 경험한 기간이 짧다면 위의 설정은 비추천합니다.
모든 항목을 익힌 다음, 필요한 항목만 가져와야 합니다.
필자가 Router와 scss(styled-components)를 이용해서 간단하게 하나 만들어봤습니다.
Web
: https://react-without-cra.netlify.app/
Github
: https://github.com/Kunune/react-without-cra
이 상태로도 build 후 정적 사이트 배포가 가능하다는 것을 보여주고 싶습니다.
React, Webpack 이미지는 file-loader
를 통해서 가져온 것이고
검은 배경 이미지는 url-loader
를 통한 base64
문자열로 가져온 이미지입니다.
F12로 확인을 해봅시다.
정적 배포 플랫폼 말고 직접 VPS 호스팅으로 VM 인스턴스를 얻어서 웹 서버로 사용한다면
두 가지 방법이 있습니다.
프록시 서버 설정을 SSL 여부에 따라 serve의 PORT를 80 혹은 443 PORT에 proxy_pass로 연결.
build directory만 VM으로 옮기고 root, index 수정 (프록시 서버 메인 페이지를 build/index.html로 변경)
npm i serve -g
serve -s build
혹은
npx serve -s build
감사합니다! 그런데 webpack-dev-server 버전 4부터는 webpack serve를 통해 실행되는 것 같습니다. 이에 따라 devtools를 따로 설정하지 않아도 정상 작동하더군요.