CRA 없는 React & TypeScript 설정 - 최적화 및 webpack 추가 설정

황윤서·2021년 10월 17일
1

React

목록 보기
2/2
post-thumbnail

CRA 없는 React & TypeScript 설정에 이어서 진행되는 글입니다.

이 글은 webpack을 사용해서 React & TypeScript 개발 환경을 처음부터 설정하는 과정을 다루며 개인적인 용도로 추후에도 쉽게 설정하기 위한 목적으로 작성되었습니다.

이전 에서 작성한 설정까지만 해도 사용은 할 수 있지만 더 빠른 개발과 더 최적화된 배포를 위해서 몇 가지 설정들을 추가적으로 작업하려 합니다.

1. 소스맵 설정

webpack은 최대한 용량을 줄이기 위해서 공백을 없애고 변수의 이름을 최대한 짧게 변경하는 등의 작업을 수행합니다. 이 과정은 배포를 하는 데에 있어서는 매우 유용하지만 개발을 하는 데에 있어서는 불편함을 초래합니다. 그렇기 때문에 빌드된 파일과 원본 파일을 연결하여 개발 과정에서는 원본 파일의 코드를 보며 디버깅할 수 있도록 설정해줘야 하는 데 이를 위한 설정이 소스맵Source Map입니다.

배포할 때는 소스맵 설정을 해줄 필요가 없으므로 설정을 생략하고 webpack.dev.js만 변경하여 development 모드에서만 소스맵이 동작하도록 설정하도록 하겠습니다. 소스맵을 설정하기 위한 속성은 devtool입니다.

// ... 생략

module.exports = {
  mode: 'development',
  devtool: 'inline-source-map',
  output: {/* ... */},
  module: {/* ... */ },
  devServer: {/* ... */ },
};

development 모드에서의 devtool은 기본적으로 eval로 설정되어 있습니다. eval 소스맵 옵션은 eval 함수를 사용해서 각 모듈을 실행하고 변경된 부분만 재빌드해서 빠르지만 정확한 소스 코드 위치를 매핑하지 못하는 경우가 종종 생깁니다. 빠른 빌드도 중요하지만 개발할 때는 코드를 살펴보면서 원인을 파악하는 것도 만만치 않게 중요하다 생각하기 때문에 source-map 혹은 inline-source-map를 택하기로 결정했고 그 중 별도의 map 파일을 생성하지 않는 inline-source-map을 선택했습니다.

2. CSS 파일 최적화 설정

src/ 디렉토리 밑에 App.css 파일을 추가하고 이를 App.tsx에서 import 한 후 빌드해보면 한 가지 최적화할 수 있는 부분이 눈에 보이게 됩니다.

/* App.css */
h1 {
    /* 테스트용 컬러는 red 가... ㅎ */
    color: red;
}
// App.tsx
import React, { FC } from "react";
import "./App.css";

const App: FC = () => { /* ... */ };

export default App;

빌드를 하고 build/static/css 디렉토리로 가보면 하나의 CSS 파일이 생성되는 데 위에서 작성한 파일의 내용과 정확하게 똑같이 작성되어 있는 것을 볼 수 있습니다. 빌드된 스크립트 파일과는 다르게 주석도 그대로 남아있고 공백이나 줄바꿈 제거도 이뤄지지 않았습니다. 여기서는 css-minimizer-webpack-plugin 을 사용해서 CSS 파일의 크기를 최적화시키는 설정을 추가해보도록 하겠습니다.

# CSS 파일 최적화를 위한 plugin 설치
$ yarn add -D css-minimizer-webpack-plugin

패키지를 설치하고 나면 webpack.prod.js를 수정해줍니다. webpack에서는 terser-webpack-plugin를 비롯한 minimizer를 통해서 코드를 최적화하고 있는 데 여기에 방금 설치한 CSS 관련 minimizer를 추가하였고 minimizer 설정에서 보이는 '...'는 webpack에서 제공하는 기존 minimizer를 그대로 사용하면서 마지막에 CSS minimizer를 추가하겠다는 의미입니다.

CSSMinimizerPlugin에서 parallel 옵션은 멀티프로세서 병렬화와 관련된 옵션인데 해당 옵션을 사용하게 되면 빌드 속도를 높일 수 있으므로 os의 cpus() 함수로 CPU 코어 정보들을 넘겨주어 설정해주었습니다.

// ... 생략

// 추가된 부분
const os = require('os');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  mode: 'production',
  output: {/* ... */},
  module: {/* ... */},
  plugins: [/* ... */],
  // 추가된 부분
  optimization: {
    minimize: true,
    minimizer: [
      '...',
      new CssMinimizerPlugin({
        parallel: os.cpus().length - 1,
      }),
    ],
  },
};

여기까지 설정하고 다시 빌드해보면 아래와 같이 주석과 공백/줄바꿈이 삭제되어 최적화된 CSS 코드를 볼 수 있습니다 :)

h1{color:red}

3. 이전 빌드 삭제 plugin 설정

한번 빌드를 한 상태에서 App.css 내의 색상을 red에서 blue로 변경하고 다시 빌드를 해보면 build/static/css 디렉토리에 CSS 파일이 두 개 생성된 것을 볼 수 있습니다. 하나는 색상이 red이고 하나는 색상이 blue인 버전의 파일이 생성되어 있습니다.

파일을 변경할 때마다 새로운 버전의 파일이 생성되고 반면에 이미 생성한 것과 동일할 경우 새롭게 만들지 않고 이전의 것을 사용하게 됩니다. 이것이 유용할 수도 있지만 초기에는 잦은 빈도로 파일을 변경하게 되니 오히려 공간만 많이 차지하게 되는 요소가 됩니다.

여기서는 clean-webpack-plugin과 같은 plugin을 사용하여 이전 빌드 파일과 같이 사용하지 않는 빌드 결과물들을 삭제하는 설정을 추가해주겠습니다.

# build 디렉토리를 깔끔하게 유지하는 데 도움을 주는 plugin 설치
$ yarn add -D clean-webpack-plugin

패키지 설치가 끝났으면 webpack.common.js에 해당 plugin을 추가해줍니다.

// ... 생략

// 추가된 부분
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  entry: '...',
  output: {/*...*/},
  module: {/*...*/},
  plugins: [
    // 추가된 부분
    new CleanWebpackPlugin(),
    /*...*/
  ],
  resolve: {/*...*/},
};

이렇게 plugin을 추가한 후 다시 빌드를 해보면 현재 수정한 파일만 남아있고 이전에 빌드했던 결과물들은 삭제된 것을 확인할 수 있습니다.

4. 환경 변수 설정

개발 중에 코드와 분리하여 환경 변수로 관리를 하고 싶을 수도 있습니다. 이와 같은 상황에서 사용할 수 있는 plugin이 이미 webpack에 내장되어 있는 DefinePlugin입니다. 아래와 같이 간단하게 환경 변수를 문자열과 매칭하여 사용할 수 있습니다. webpack이 해당 문자열을 발견하면 해당하는 환경 변수로 치환하는 방식으로 동작하게 됩니다.

// ... 생략

const webpack = require('webpack');

module.exports = {
  entry: '...',
  output: {/*...*/},
  module: {/*...*/},
  plugins: [
    /*...*/
    new webpack.DefinePlugin({
      'process.env.API_URL': JSON.stringify(process.env.API_URL),
    }),
  ],
  resolve: {/*...*/},
};

하지만 이런 방식의 경우 추가하고 싶은 환경 변수가 생길 때마다 webpack 설정을 변경해줘야 합니다. 더 쉬운 사용을 위해 webpack/env.js를 추가하고 webpack.common.js를 변경해주도록 하겠습니다.

  • env.js
// env.js
'use strict';

const APP_ENV = /^APP_/i;

function getClientEnvironment() {
  const raw = Object.keys(process.env)
    .filter((key) => APP_ENV.test(key))
    .reduce((env, key) => {
      env[key] = process.env[key];
      return env;
    }, {});
  const stringified = {
    'process.env': Object.keys(raw).reduce((env, key) => {
      env[key] = JSON.stringify(raw[key]);
      return env;
    }, {}),
  };

  return { raw, stringified };
}

module.exports = getClientEnvironment();
  • webpack.common.js
// ... 생략

const env = require('./env.js');
const webpack = require('webpack');

module.exports = {
  entry: '...',
  output: {/*...*/},
  module: {/*...*/},
  plugins: [
    /*...*/
    new webpack.DefinePlugin(env.stringified),
  ],
  resolve: {/*...*/},
};

위 부분은 process.env 환경 변수 객체에서 REACT_APP_으로 시작하는 환경 변수들만 가져와 'process.env' 문자열에 맵핑해주는 설정입니다. env.js는 CRA의 설정을 참고한 것인 데 REACT_APP_으로 시작하는 환경 변수들만 가져오도록 설정해준 이유는 보안적인 이유로 의도치 않은 환경 변수에 접근하는 것을 방지하기 위한 것입니다.

그 다음으로 환경 변수를 .env 파일에서 관리하도록 하기 위해 env-cmd를 사용하도록 하겠습니다. 해당 패키지는 .env 내에 정의된 값들을 환경변수로 주입시켜줍니다.

# 환경변수 관련 패키지 설치
$ yarn add -D env-cmd

패키지를 설치하고 나면 package.json의 스크립트 명령을 수정해주고 .env 파일에 환경변수를 정의해줍니다.

  • package.json
"scripts": {
  "dev": "env-cmd webpack serve --mode development",
  "build": "env-cmd webpack --mode production --progress"
}
  • .env
APP_ENV=development

위와 같이 설정해주고 코드 내에 환경 변수를 사용하게 되면 해당 환경 변수의 값으로 치환되는 것을 확인할 수 있습니다.

// App.tsx
import React, { FC } from "react";
import "./App.css";

const App: FC = () => {
  return <h1>Hello World {process.env.APP_ENV}</h1>;
};

export default App;

5. 이미지 최적화 설정

src/assets/images에 위 이미지를 저장하고 src/@types/index.d.ts를 추가한 다음 App.tsx를 수정해줍니다.

  • index.d.ts
// src/@types/index.d.ts
declare module "*.jpg";
  • App.tsx
import React, { FC } from "react";
import "./App.css";
import testImage from "./assets/images/test.jpg";

const App: FC = () => {
  return (
    <>
      <h1>Hello World {process.env.NODE_ENV}</h1>
      <img src={testImage} width="500px" />
    </>
  );
};

export default App;

여기까지 설정하고 빌드를 하고 나면 위 이미지의 크기는 473.9kB입니다. 이러한 이미지에 대한 최적화 설정을 webpack level에서 수행해보도록 하겠습니다.

# 이미지 최적화를 위한 loader 설치
$ yarn add -D image-webpack-loader

image-webpack-loaderimagemin을 사용해서 PNG, JPEG, GIF, SVG, WEBP 확장자를 가진 이미지에 대해 최적화해주는 loader입니다. 이 loader를 enforce: 'pre' 옵션을 적용하여 다른 loader 전에 먼저 적용해주도록 하겠습니다. 최적화를 하면 할수록 빌드 속도는 느려지므로 production 모드에서만 설정해주겠습니다.

// webpack.prod.js

// ... 생략

module.exports = {
  mode: 'production',
  output: {/*...*/},
  module: {
    rules: [
      /*...*/
      // 추가한 부분
      {
        test: /\.(gif|jpe?g|png|svg|webp)$/i,
        loader: 'image-webpack-loader',
        enforce: 'pre',
      },
    ],
  },
  plugins: [/*...*/],
  optimization: {/*...*/},
};

image-webpack-loader를 설정해주고 다시 빌드를 하면 위 이미지의 크기는 307.9kB로 이전 473.9kB에 비해 65% 수준으로 크기가 감소하였습니다.

여기서 추가적으로 SVG 파일에 대한 최적화를 위한 설정을 해주도록 하겠습니다. SVG 파일의 경우 현재 url-loader를 적용하고 있는 데 SVG 파일은 svg-url-loader를 적용하도록 수정하겠습니다.

url-loader는 Base64 인코딩 방식을 사용하고 있는 데 svg-url-loader는 UTF-8 인코딩 방식을 사용하고 있어 아래와 같은 장점이 있다고 공식 문서에서 소개하고 있습니다.

  1. 결과물이 더 짧다. 2K 크기의 아이콘의 경우 최대 2배까지 짧아진다.
    Resulting string is shorter (can be ~2 times shorter for 2K-sized icons);

  2. gzip을 사용해 압축했을 때 더 잘 압축된다.
    Resulting string will be compressed better when using gzip compression;

  3. 브라우저에서 UTF8 인코딩 문자열을 Base64 문자열보다 더 빠르게 파싱한다.
    Browser parses utf-8 encoded string faster than its base64 equivalent.

# svg url loader 설치
$ yarn add -D svg-url-loader

먼저 패키지를 설치해주고 webpack.common.js를 수정해줍니다. url-loader에서 SVG를 삭제해주고 svg-url-loader를 추가해줍니다.

// ... 생략

module.exports = {
  entry: '...',
  output: {/*...*/},
  module: {
    rules: [
      /*...*/
      {
        // 수정한 부분
        test: /\.(bmp|gif|jpe?g|png|webp)$/i,
        loader: 'url-loader',
        options: {
          limit: imageInlineSizeLimit,
          name: 'static/media/[name].[contenthash:8].[ext]',
        },
      },
      // 추가한 부분
      {
        test: /\.svg$/i,
        loader: 'svg-url-loader',
        options: {
          limit: imageInlineSizeLimit,
          name: 'static/media/[name].[contenthash:8].svg',
        },
      },
    ]
  },
  plugins: [/*...*/],
  resolve: {/*...*/},
};

6. 대소문자 구분 plugin 설정

OS별로 대소문자 구분 정책이 다른 경우가 있기 때문에 MAC에서 개발하다가 AWS Linux에 배포하게 되면 문제가 생기는 경우가 종종 생깁니다. MAC은 대소문자를 구분하지 않기 때문에 대소문자에 문제가 있어도 개발할 때는 문제를 발견하지 못하다가 배포할 때 Linux 환경에서 문제가 생기는 것입니다.

이러한 문제를 해결하기 위해 파일 경로의 대소문자가 일치하는 지 체크해주는 plugin이 있는 데 여기서는 이에 대한 설정을 해주도록 하겠습니다.

# 대소문자 관련 plugin 설치
$ yarn add -D case-sensitive-paths-webpack-plugin

설치가 완료되면 webpack.common.js에 해당 plugin을 추가해줍니다.

// ... 생략

const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');

module.exports = {
  entry: '...',
  output: {/*...*/},
  module: {/*...*/},
  plugins: [
    /*...*/
    new CaseSensitivePathsPlugin(),
  ],
  resolve: {/*...*/},
};

이 plugin을 설정하게 되면 파일 경로의 대소문자가 다를 때 에러를 발생시켜 배포할 때 갑자기 대소문자로 인한 문제가 발생하는 위험을 줄일 수 있습니다.

7. TypeScript 트랜스파일링과 타입 체크 프로세스 분리

TypeScript를 사용할 때는 타입이 있기 때문에 자바스크립트로 트랜스파일링하는 과정 뿐 아니라 타입 체크하는 프로세스도 수행됩니다. ts-loader만을 사용할 때는 트랜스파일링과 타입 체크하는 로직이 단일 스레드에서 순차적으로 수행되는 데 이 프로세스를 분리하게 되면 빌드 속도를 더 빠르게 할 수 있습니다.

다만 프로젝트 규모가 커질수록 해당 plugin을 사용하는 것이 오히려 성능을 악화시킬 수도 있다고 하지만 해당 문제가 생길만큼 큰 프로젝트를 진행할 가능성은 낮기 때문에 여기서는 설정을 해주도록 하겠습니다.

# 타입 체크를 위한 plugin 설치
$ yarn add -D fork-ts-checker-webpack-plugin

plugin을 설치했으면 webpack.common.js에 플러그인을 추가해주고 ts-loader는 트랜스파일링만 하도록 옵션을 설정해줍니다. ts-loader에서는 트랜스파일링만 하고 타입 체크는 fork-ts-checker-webpack-plugin에서 수행하도록 하여 프로세스를 분리합니다.

// ... 생략

// 추가한 부분
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
  entry: './src/index.tsx',
  output: {/*...*/},
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        use: [
          'babel-loader', 
          {
            loader: 'ts-loader',
            options: {
              // 추가한 부분
              transpileOnly: true,
            },
          },
        ],
        exclude: /node_modules/,
      },
      /*.../
    ]
  },
  plugins: [
    /*...*/
    // 추가한 부분
    new ForkTsCheckerWebpackPlugin(),
  ],
  resolve: {/*...*/},
};

이제 타입들을 많이 사용하는 어느 정도 규모까지는 트랜스파일링과 타입 체크가 별도의 프로세스에서 수행되어 빌드 속도가 향상될 것입니다.

8. public 디렉토리 복사하기

CRA의 경우 index.html을 비롯해서 robots.txt, favicon.ico 등의 파일들을 public 디렉토리에 위치시키고 빌드 시 public 디렉토리 내의 전체 파일을 복사합니다. public 디렉토리의 역할이 그러함으로 여기서도 동일하게 설정해주도록 하겠습니다.

# 복사 관련 plugin 설치
$ yarn add -D copy-webpack-plugin

copy-webpack-plugin을 사용하면 빌드 디렉토리로 파일이나 디렉토리 전체를 복사할 수 있습니다. 아래와 같이 webpack.common.js를 수정해줍니다.

// ... 생략
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
  entry: '...',
  output: {/*...*/},
  module: {/*...*/},
  plugins: [
    /*...*/
    new CopyPlugin({
      patterns: [
        { 
          from: 'public',
          to: '.',
          globOptions: {
            ignore: [path.resolve(__dirname, '../public/index.html')],
          },
        },
      ],
    }),
  ],
  resolve: {/*...*/},
};

patterns라는 옵션으로 복사할 파일들의 패턴, 경로를 지정해줄 수 있습니다. 여기서 눈여겨봐야 할 부분은 globOptions 입니다. 단순히 public 디렉토리 내 전체 파일을 복사하도록 설정할 경우 index.html에 대해서 HtmlWebpackPlugin에 의해 이미 처리된 파일을 덮어쓰기하기 때문에 오류가 발생하게 됩니다. 이에 대해서 globOptions를 사용해서 제외할 파일들을 지정해줘야 합니다.

이렇게 설정해주고 public 디렉토리에 robots.txt 등을 생성하면 빌드 디렉토리로 그대로 복사되는 것을 확인할 수 있습니다.

profile
꾸준함을 만들고 싶어요

0개의 댓글