반응형

리액트 프로젝트를 시작할 때 create-react-app만 사용하다 보니까 번들러에 대한 지식이 부족한 걸 느꼈다. 그래서 CRA를 사용하지 않고 CRA 예제 코드 실행하기에 도전해봤다. 웹팩에 대해서 찾아보다 보니까 확실히 복잡하긴 하다.... 나중엔 다른 번들러(Parcel? Rollup?)도 한번 공부해봐야 할 듯.

package.json 생성

일단 적당한 폴더를 만들고 아래 명령어로 package.json 파일을 생성한다.

yarn init -y

yarn을 기준으로 작성하겠음. npm도 명령어만 약간 다를 뿐 과정은 똑같다.

리액트 설치

yarn add react react-dom react-refresh

바벨 설치

yarn add @babel/core @babel/preset-env @babel/preset-react

바벨은 최신 자바스크립트 문법을 구형 브라우저에서도 동작하게, 혹은 리액트의 jsx 문법을 자바스크립트 문법으로 변환해주는 자바스크립트 트랜스파일러이다.

@babel/preset-env - 최신 자바스크립트 문법을 구형 브라우저에서도 작동하도록 변환하거나 폴리필 추가
@babel/preset-react - 리액트의 JSX 문법을 변환

babel.config.json 생성

babel.config.json은 바벨 설정 파일이다. 앞에서 바벨과 함께 설치한 프리셋을 설정해준다.

{
  "presets": [
    "@babel/preset-env",
    ["@babel/preset-react", { "runtime": "automatic" }]
  ]
}

React 17 이후부턴 "runtime": "automatic" 옵션을 추가해야 한다.

웹팩 설치

yarn add webpack webpack-cli webpack-dev-server

웹팩은 자바스크립트 번들러이다. 직접 작성한 코드나 여러 라이브러리의 자바스크립트 코드를 하나로 묶고 최적화해준다.

webpack-cli - 웹팩을 커맨드라인에서 실행할 수 있게 해 줌
webpack-dev-server - 파일이 변화할 때마다 실시간으로 빌드하는 개발 서버 구동

웹팩 로더 설치

yarn add babel-loader css-loader style-loader

로더는 웹팩이 파일을 빌드할 때 파일을 해석하기 위한 패키지이다.

babel-loader - jsx 파일과 최신 자바스크립트 문법을 변환(바벨과 연동)
css-loader - css 파일을 해석
style-loader - css를 dom에 삽입

웹팩 플러그인 설치

yarn add html-webpack-plugin mini-css-extract-plugin interpolate-html-plugin @pmmmwh/react-refresh-webpack-plugin

플러그인은 웹팩이 해석한 결과물을 처리하는 패키지이다.

html-webpack-plugin - html 파일에 번들링 된 js 파일을 삽입
mini-css-extract-plugin - js 파일과 css 파일을 분리
interpolate-html-plugin - html 파일에서 %ENV% 같은 템플릿 구문 사용 가능. 꼭 필요한 건 아니지만 CRA 기본 예제 파일에서 %PUBLIC_URL%을 사용하기 때문에 이를 변환하기 위해 설치
@pmmmwh/react-refresh-webpack-plugin - 좀 더 우수한 핫 리로드 패키지인 react-refresh 사용

webpack.config.js 생성

webpack.config.js는 웹팩 설정 파일이다. 앞에서 설치한 로더와 플러그인들을 넣어준다.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const InterpolateHtmlPlugin = require('interpolate-html-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const devMode = process.env.NODE_ENV !== 'production';

module.exports = {
  entry: './src/index.js',
  resolve: {
    extensions: ['.js', '.jsx'],
  },
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'static/js/[name].[contenthash:8].js',
    chunkFilename: 'static/js/[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'static/media/[name].[hash:8].[ext]',
    clean: true,
  },
  devtool: devMode ? 'eval-source-map' : false,
  devServer: {
    port: 3000,
    hot: true,
    open: true,
    client: {
      overlay: true,
      progress: true,
    },
  },
  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: [['@babel/preset-env', { targets: 'defaults' }]],
                plugins: devMode ? ['react-refresh/babel'] : [],
              },
            },
          },
          {
            test: /\.css$/i,
            use: [
              devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
              'css-loader',
            ],
          },
          {
            test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
            type: 'asset',
            parser: {
              dataUrlCondition: {
                maxSize: 10000,
              },
            },
          },
          {
            type: 'asset/resource',
            exclude: [/\.(js|jsx)$/, /\.html$/, /\.json$/, /^$/],
          },
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin(
      Object.assign(
        {},
        {
          template: 'public/index.html',
        },
        !devMode
          ? {
              minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeRedundantAttributes: true,
                useShortDoctype: true,
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true,
                minifyJS: true,
                minifyCSS: true,
                minifyURLs: true,
              },
            }
          : undefined
      )
    ),
    new InterpolateHtmlPlugin({ PUBLIC_URL: '' }),
  ].concat(
    devMode ? [new ReactRefreshWebpackPlugin()] : [new MiniCssExtractPlugin()]
  ),
};

개발 빌드에선 style-loader를 사용하고 프로덕션 빌드에선 mini-css-extract-plugin을 사용한다. 왜 이렇게 했냐면 css-loader 문서에서 이걸 추천한대서...ㅎㅎ

CRA나 다른 글에서는 file-loader와 url-loader를 사용하는데 webpack 5부턴 자체적으로 지원해서 사용하지 않았다. 설정은 CRA 꺼 따라 했음. 약 10KB보다 작은 이미지 파일은 base64 주소로 변환돼서 결과물에 직접 삽입된다.

마지막 룰을 보면 js, html, json, 이미지 파일을 제외한 파일들은 전부 리소스로 처리해서 static/media 안에 집어넣는데 제외한 파일 정규식에 /^$/도 있는 이유가 html-webpack-plugin에서 이상한 파일을 생성하는 문제가 있어서(파일이 아니라 인라인 자바스크립트라고 함. 근대 이걸 리소스로 처리해서 문제가 생김) 이를 제외하기 위해 넣었다. (참고)

마찬가지로 html-webpack-plugin 설정도 CRA를 참고했다. 프로덕션 빌드에선 최적화된 html을 내보낸다.

리액트 컴포넌트 작성

그냥 CRA에서 기본으로 만들어주는 예제 파일을 그대로 복사한다. (src, public 폴더)

단, 예제 소스에서는 web-vitals을 사용하므로 이걸 추가로 설치해야 한다.

yarn add web-vitals

package.json에 scripts 추가

  "scripts": {
    "start": "webpack serve --progress --mode development",
    "build": "webpack --progress --mode production"
  }

yarn start - 개발 서버를 실행해 프로젝트를 바로 확인. 주소는 http://localhost:3000/
yarn build - 빌드. 결과물은 build 폴더에 생성

이걸로 리액트를 개발하기 위한 최소한의 환경 구성이 완료되었다. 공부하면서 느낀 점은 그냥 얌전히 create-react-app 쓰자...ㅎㅎ

정말 최소한의 세팅이라 eslint, prettier 등등 추가할 게 많은데 이것도 한번 정리해볼까 고민 중...

반응형

+ Recent posts