JS + React 개발 환경 직접 만들기

jhj46456·2020년 9월 6일
12
post-thumbnail

React 및 JS 기초에 대해 설명하진 않습니다. 초보자를 위한 내용이 아닙니다.

이 글은 2020-10-01 이후로 업데이트 하지 않습니다.
Typescript에 집중하려고 합니다.

webpack v4 기준입니다.

📚 모든 내용은 공식 문서를 기반으로 작성되었습니다.

다음의 개발 환경을 구성합니다.

  • Package Manager : yarn
  • Library : React (JS)
  • JS : ES6+
  • CSS : Styled components

모든 설정을 따라할 필요는 없습니다. 프로젝트 성격에 맞게 활용하세요.


직접 만드는 이유

CRA가 있는데 굳이 힘들게 직접 만드는 이유가 무엇이냐?라고 묻는다면 필자는 장점을 말씀드리진 못합니다.

build time도 ESLint와 Type Checker를 설치하면 CRA에 비해 5~10초 정도 감소하는 정도이며
ESLint를 사용하지 않으면 build timeCRA에 비해 2배 넘게 감소합니다.

node_modules의 파일 개수도 직접 만들었더라도 패키지를 많이 설치하면 CRA를 초과할 수 있습니다.

소규모 프로젝트 정도는 대부분 CRA만으로 개발이 가능한데, 규모가 커지면 eject를 해야하는 상황이 생깁니다.

이럴 때 어차피 eject 해야할 거 그냥 직접 환경 만들자가 되는 것이라고 생각합니다.
webpack.config.js가 670 line인 상태에서 수정하는 것과 백지인 상태에서 수정하는 것은 완전 다르니까요.

필자가 20일 넘게 webpack을 연구한 결과, 172 line만으로 CRA와 동일한 chunk size와 유사한 CUI를 구현할 수 있었습니다.

필자는 직접 개발 환경을 만드는 것을 위와 같이 생각합니다.


yarn을 사용하는 이유

필자가 만든 https://mo-gak-ko.xyz/ 를 npmyarn 두 개로 각각 패키지를 설치하고, 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.jssrc/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

📌 @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 구문을 해석합니다.


Webpack

CRA의 build directory 구조를 최대한 따라해봅니다.

프론트의 js같은 경우에는 Module Bundler를 사용해야 하는 이유가 몇 가지 있습니다.

  1. Module Format을 사용하기 위함. (Node같이 CommonJS가 내장되어 있지 않음)
  2. 여러 번의 요청 대신 한번의 요청 만으로 js를 불러옴.
  3. ...

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/ 공식 문서를 기반으로 진행하면서 설정하겠습니다.

Mode

📌 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

번들 작업을 할 파일을 선택합니다.

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 환경 별로 다른 작업을 할당하기 위해 사용함.

Output

번들 결과물이 저장될 경로를 지정합니다.

자주 쓰이는 옵션으로는 pathfilename이 있습니다.

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까지 보여주는 예약어입니다.

Loaders

📌 사용 가능한 loaders 목록

webpack은 기본 설정에서 .js와 .json만 이해가 가능합니다.

이 항목에서는 '어떤 확장자는 이렇게 처리해라~'같은 설정을 해줍니다.

옵션은 다음과 같습니다.

  • test : 이 확장자는 webpack이 해석할 것입니다.

정규식(regex) 분석
/ ... / : 정규식의 시작과 끝을 나타냄
\.: .을 나타내는 escaped character
( ... ) : 문자열을 판별하는 group
a | b : a or b
$ : 해당 정규식으로 끝나는 문자열

  • use : 변환을 위해 사용하는 loader
  • exclude : 제외할 file/directory (절대경로)
  • include : 포함시킬 file/directory (절대경로)

그렇다면 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만 설정하였음.

Caching (optional)

매번 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로 해제

file-loader (optional)

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

url-loader (optional)

📌 url-loader docs

기본적으로 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를 설치합니다.

eslint-loader (optional)

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,
        },          
      ]
    }
  }
}

formatter options docs

loader를 추가했으니 eslint 설정이 필요합니다.

CRAeslint-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 FixCompile warning을 볼 수 있습니다.

Plugins

React는 아시다시피 index.html에 번들 작업을 마친 js 파일이 script 태그로 추가되어 작동하는 방식입니다.

그러기 위해서는 public/index.htmlbuild/ 로 옮겨주면서 번들 결과물이 script 태그로 들어가야 합니다.

Plugins 목록

이런 역할을 하는 플러그인이 하나 있습니다.

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을 만듦.

Environment variables (optional)

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_ENVPUBLIC_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_ENVwebpack 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,
          })
        )
      }),
    ],
  }
}

webpack-manifest-plugin (optional)

먼저 배경 지식을 짚고 넘어가겠습니다.

runtime은 기본적으로 모듈화된 앱을 연결하는데 필요합니다. 모듈을 연결하는데 필요한 로직을 포함합니다.

html이 브라우저에서 실행되면 필요한 bundle과 asset을 불러와서 연결해야 합니다.
하지만, webpack의 최적화로 src는 합쳐지고, 최소화되고, chunk로 나눠집니다.
그렇다면 webpack은 모듈들을 어떻게 관리할까요? 여기에서 manifest가 사용됩니다.

manifest가 무엇이냐면, 컴파일러가 입력, 확인, 맵핑 과정을 거치면서 모듈에 대한 메모를 기록합니다.
data collectionmanifest라고 부릅니다.

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 (optional)

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 }로 자동 적용됩니다.

DevServer

📌 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 : 디버깅을 위한 최상의 옵션

scripts (optional)

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입니다. (모든 기능 포함)

더 이상의 설명은 생략합니다.

npm script

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으로 접속해보세요.


stats (optional)

yarn startyarn build를 실행해보면 아시겠지만 필자만 이렇게 생각하는진 모르겠는데
build result를 지저분하게 보여줍니다.

이럴 때 사용하는 것이 stats 옵션입니다.

stats이란 webpack compiler가 동작하였을 때 출력하는 내용물을 결정하는 옵션입니다.

CRA필자가 설정한 옵션을 한번 보시죠

둘 다 동일하게 assets, time, publicPath을 동일하게 보여줍니다.

차이점이라면 CRAgzip size를 보여주고 Customparse size를 보여준다는 것이죠.
chunkCode 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 인스턴스를 얻어서 웹 서버로 사용한다면

두 가지 방법이 있습니다.

  1. 프록시 서버 설정을 SSL 여부에 따라 serve의 PORT를 80 혹은 443 PORT에 proxy_pass로 연결.

  2. build directory만 VM으로 옮기고 root, index 수정 (프록시 서버 메인 페이지를 build/index.html로 변경)

    npm i serve -g

    serve -s build

    혹은

    npx serve -s build


Code Splitting

바로가기


Tree Shaking

바로가기


SCSS to CSS

바로가기


Typescript + React

바로가기

2개의 댓글

comment-user-thumbnail
2020년 10월 14일

감사합니다! 그런데 webpack-dev-server 버전 4부터는 webpack serve를 통해 실행되는 것 같습니다. 이에 따라 devtools를 따로 설정하지 않아도 정상 작동하더군요.

1개의 답글