CRA 없는 React & TypeScript 설정

황윤서·2021년 10월 14일
1

React

목록 보기
1/2
post-thumbnail

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

React 앱을 CRA로 생성하는 것은 매우 간단합니다 대부분의 상황에서 적용할 수 있도록 범용적으로 작성되었기 때문에 간단한 프로젝트에서는 적합하지만 프로젝트의 규모가 커지고 webpack 설정을 건드려야 할 때는 그 방법이 복잡합니다. 그렇기 때문에 처음부터 webpack 설정을 직접 작성하는 것을 선택하기도 합니다. 여기서는 그 방법에 대해서 다뤄보도록 하겠습니다.

1. 프로젝트 초기화

$ yarn init

값을 입력하고 난 후 생성된 package.json의 결과물은 아래와 같습니다.

{
  "name": "react-typescript-boilerplate",
  "version": "0.1.0",
  "description": "react & typescript boilerplate without CRA",
  "repository": "https://github.com/hseoy/react-typescript-boilerplate.git",
  "author": "Your Name <your.email@example.com",
  "license": "MIT",
  "private": true
}

그 다음에는 가장 기본적인 react 관련 패키지들을 설치해줍니다.

$ yarn add react react-dom
$ yarn add -D @types/react @types/react-dom

2. webpack 초기 설정

2-1. webpack 관련 패키지 설치 및 webpack.config.js 작성

# webpack 관련 패키지 설치
$ yarn add -D webpack webpack-cli webpack-dev-server webpack-merge

webpack-merge 패키지는 두 웹팩 설정을 병합(merge)하여 적용할 수 있도록 하며 여기서는 공통 설정과 모드(development/production)에 따른 설정을 병합하여 모드에 따라 다른 설정을 사용하기 위한 목적으로 사용합니다.

패키지를 설치하고 나면 프로젝트의 root 경로 바로 아래에 webpack.config.js 파일을 생성하고 아래의 내용을 작성해줍니다.

'use strict';

const { merge } = require('webpack-merge');

// 공통으로 사용할 설정
const common = require('./webpack/webpack.common');
// development mode일 때 적용할 추가 설정
const developmentConfig = require('./webpack/webpack.dev');
// production mode일 때 적용할 추가 설정
const productionConfig = require('./webpack/webpack.prod');

module.exports = (_env, argv) => {
  // 만약 mode가 development이면 공통 설정과 developmentConfig를 병합하고 
  // 아니라면 productionConfig와 병합
  const isDevelopment = argv.mode === 'development';
  const config = isDevelopment ? developmentConfig : productionConfig;
  return merge(common, config);
};

webpack의 기본 설정 파일 경로는 ./webpack.config.js입니다. 해당 경로에 webpack 설정을 작성하면 해당 설정대로 webpack이 동작하게 됩니다. 위 설정 코드는 argv.mode에 따라 각기 다른 설정을 적용하기 위한 코드로 실질적인 설정 코드는 없이 분기처리만 해주고 있습니다. CLI에서 환경 변수나 argument들을 넘겨줄 수 있는 데 이러한 값을 설정 파일에서 가져오기 위해서는 함수 형태로 export 하게 되면 첫 번째 인자로 환경변수, 두 번째 인자로 CLI argument들을 받게 됩니다.(공식 문서 참고).

webpack --mode=development와 같이 mode argument를 설정해주면 분기되어 설정이 적용됩니다.

2-2. mode 별 webpack 설정 파일 작성

이제 webpack.config.js에서 require하고 있는 webpack/ 설정 파일들을 작성해야 합니다. 가장 기본적으로 공통 설정을 아래와 같이 작성해줍니다. entry point가 될 파일과 빌드 경로에 대한 설정 뿐 아니라 확장자가 없는 파일에 대한 확장자 해석 순서까지 지정해주었습니다.

'use strict';

const path = require('path');

module.exports = {
  // entry point 설정
  entry: './src/index.tsx',
  output: {
    // 빌드 경로 설정
    path: path.resolve(__dirname, '../build'),
  },
  resolve: {
    // 확장자가 포함되지 않은 파일에 대해서 아래의 확장자 순서로 해석합니다.
    // 참고 : https://webpack.kr/configuration/resolve/#resolveextensions
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  },
};

공통 설정을 작성해주었으면 각 모드별 다른 설정을 적용하기 위한 webpack.dev.jswebpack.prod.js를 생성해줍니다. 여기서는 가장 기본적인 mode 값과 파일 이름을 지정해주었습니다.

  • webpack.dev.js
'use strict';

module.exports = {
  mode: 'development',  
  output: {
    filename: 'static/js/build.js',
  },
};
  • webpack.prod.js

production 모드에서는 새롭게 빌드했을 때 브라우저의 캐시가 동작하지 않도록 하기 위해 파일의 이름과 더불어 8자리의 contenthash를 포함시켰습니다.

'use strict';

module.exports = {
  mode: 'production',
  output: {
    filename: 'static/js/[name].[contenthash:8].js',
  },
};

2-3. webpack-dev-server 설정

코드를 변경된 사항을 반영해야 할 때 매번 전체 코드를 빌드하고 실행한다면 매우매우 불편할 것입니다. webpack-dev-server를 사용하게 되면 변경된 부분만 업데이트되기 때문에 매우 편리하고 적용 역시 간단해서 개발 과정에서 거의 필수적으로 사용합니다.

패키지 설치는 위에서 이미 수행했으므로 설정 파일만 수정할 건데, webpack-dev-server는 development 모드일 때만 필요할 것이므로 webpack.dev.js에 설정을 추가하도록 하겠습니다.

'use strict';

module.exports = {
  mode: 'development',
  output: {
    filename: 'static/js/build.js',
  },
  devServer: {
    static: path.join(__dirname, '../build'),
    port: 8080,
    historyApiFallback: true,
  },
};

devServer라는 속성을 통해 webpack-dev-server의 설정을 지정할 수 있습니다. 먼저 static은 정적 파일들의 경로를 설정하는 속성으로 빌드된 파일들이 ../build에 위치하므로 해당 경로로 지정을 해주었습니다. port는 개발 서버의 실행 포트 번호를 지정할 수 있으며 historyApiFallback은 잘못된 URL로 요청을 보냈을 때 index.html을 응답하도록 설정할 것인지에 대한 속성입니다.

React를 사용할 때 URL 변경에 따른 처리를 서버 쪽에서 하는 것이 아니라 클라이언트 측에서 URL을 확인하여 어떤 페이지를 보여줄 지 결정하기 때문에 모든 URL 요청에 대해서 index.html을 응답해줘야 합니다. 그래서 true로 설정해주었습니다. 그래서 historyApiFallback 속성에 대해 true로 설정해주었습니다.

3. webpack loader 설정

로더Loader는 웹팩에서 자바스크립트 파일 외의 리소스(HTML, CSS, Font, TypeScript 등)을 변환할 수 있도록 도와줍니다. 특정 확장자에 특정 loader를 연결해주면 해당 확장자의 파일을 해석할 때 해당 loader가 변환 작업을 처리하도록 하여 loader만 있다면 다른 여러 확장자의 파일들을 번들링할 수 있습니다.

3-1. TypeScript 관련 webpack loader 설치 및 적용

# TypeScript 및 관련 loader 설치
$ yarn add -D typescript ts-loader

TypeScript를 사용하기 위해 loader를 설치했으면 .ts 확장자를 지닌 TypeScript 파일을 해석할 때 해당 loader를 사용하라는 설정을 webpack 설정 파일에 추가해줘야 합니다. TypeScript 관련 설정은 development 모드나 production 모드나 동일할 것이므로 webpack.common.js에 추가해줍니다.

// ... 생략

module.exports = {
  entry: '...',
  output: { /* ... */ },
  // 추가해야 하는 부분의 시작
  modules: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        use: ['ts-loader']
        // node_modules 내의 코드는 제외하기 위한 목적
        exclude: /node_modules/,
      }
    ]
  },
  // 추가해야 하는 부분의 끝
  resolve: { /* ... */ },
};

여기까지만 설정해도 tsconfig.json을 제외했을 때 webpack이 TypeScript 파일을 불러오기 위한 준비는 끝이지만 한 가지 더 설정을 해주면 좋은 것이 있습니다. 바로 Babel입니다. Babel은 브라우저 호환성을 위해 최신 문법으로 작성된 코드를 하위 버전의 문법으로 변환하는 컴파일러입니다. 이전 브라우저에서도 최신 문법으로 작성한 코드를 사용하기 위해서는 하위 버전의 문법으로 변환해서 호환성을 맞춰줘야 하는 데 이러한 작업을 도와주는 도구가 Babel입니다.

Webpack에서 빌드할 때 Babel Loader를 사용하면 코드를 불러오면서 최신 문법을 하위 버전의 문법으로 변환해주기 때문에 여기서는 이를 위한 설정을 진행하도록 하겠습니다. 먼저 Babel과 관련 Loader에 대한 패키지들을 설치해줍니다.

아래 Babel 설정에 대한 자세한 설명은 아래 글로 대체하도록 하겠습니다.

# Babel & Babel Preset 설치
$ yarn add -D @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript

# Babel Polyfill 관련 패키지 설치
$ yarn add corejs@3 @babel/runtime
$ yarn add -D @babel/plugin-transform-runtime @babel/runtime-corejs3

# Babel Loader 설치
$ yarn add -D babel-loader

그 다음으로는 Babel의 설정 파일을 추가해줘야 합니다. 현재 사용할 Babel Preset들을 명시해줍니다.

// .babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      { 
        "targets": "> 1%, not dead",
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ],
    "@babel/react",
    "@babel/typescript"
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs":3
      }
    ]
  ]
}

마지막으로는 webpack.common.js에서 ts-loader 앞에 babel-loader를 추가해줍니다. Loader는 오른쪽에서 왼쪽 순으로 실행되는 데 지금 설정은 TypeScript를 JavaScript로 변환한 후에 Babel을 사용해 하위 버전의 문법으로 다시 변환하게 됩니다.

// ... 생략
{
  test: /\.(js|jsx|ts|tsx)$/,
  use: ['babel-loader', 'ts-loader'],  
  exclude: /node_modules/,
}
// ... 생략

3-2. style 관련 webpack loader 설치 및 적용

# style 관련 loader 설치
$ yarn add -D css-loader style-loader mini-css-extract-plugin

css-loader 패키지는 CSS 파일을 불러오기 위한 loader이고 style-loader 패키지는 이렇게 불러온 CSS 스타일을 <style></style> 태그 하위로 삽입하는 loader입니다. 그리고 mini-css-extract-pluginstyle-loader와 다르게 불러온 CSS 스타일을 별도의 파일로 추출하여 삽입하는 플러그인이며 loader가 포함되어 있어 해당 loader를 사용하면 됩니다. style-loadermini-css-extract-plugin은 서로 동작이 다르기 때문에 함께 사용하면 안됩니다.

development 모드에서는 빠른 개발 속도가 더 중요하기 때문에 개발 시 자주 바뀌는 스타일을 반영하는 데 있어 빠르게 동작하는 style-loader가 적합하고 반면 production 모드에서는 CSS 스타일 파일을 별도의 파일로 분리하여 초기 로딩 시 필요한 스타일만 불러오도록 하는 mini-css-extract-plugin이 적합합니다. 그렇기에 여기서는 development 모드일 때는 style-loader를 사용하고 production 모드일 때는 mini-css-extract-plugin을 사용하도록 설정하도록 하겠습니다.

먼저 webpack.dev.jsstyle-loader를 적용해줍니다.

'use strict';

module.exports = {
  mode: 'development',
  output: {
    filename: 'static/js/build.js',
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader',],
      },
    ],
  },
};

그 다음은 mini-css-extract-pluginwebpack.prod.js에 적용해줍니다. mini-css-extract-plugin은 플러그인이므로 plugin에 명시를 해주고 MiniCssExtractPlugin.loadercss-loader 앞에 추가해줍니다.

'use strict';

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  mode: 'production',
  output: {
    filename: 'static/js/[name].[contenthash:8].js',
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:8].css',
    }),
  ],
};

3-3. 이미지 관련 webpack loader 설치 및 적용

# 이미지 관련 loader 설치
$ yarn add -D file-loader url-loader

url-loader는 이미지 파일들을 불러오는 데 특정 사이즈 미만일 경우 base64 포맷으로 인코딩해 별도의 파일이 아니라 문자열로 삽입합니다. 파일 사이즈가 작을 경우에는 별도의 파일로 분리하여 불러오는 것보다 문자열로 삽입하는 것이 더 효율적일 수 있기 때문에 이러한 방식을 사용합니다. 반면 해당 사이즈보다 클 때에는 다른 로더를 사용하게 되는 데 기본적으로는 file-loader를 사용하도록 설정되어 있어 url-loader와 함께 설치해주었습니다.

다른 loader들과 마찬가지로 이미지 파일들과 url-loader를 연결해주는 데 여기서는 환경변수로 이미지의 사이즈를 받아 해당 사이즈 미만일 경우 문자열로 삽입하도록 하였습니다.

// webpack.common.js
'use strict';

const path = require('path');

// IMAGE_INLINE_SIZE_LIMIT라는 환경변수 사용
const imageInlineSizeLimit = process.env.IMAGE_INLINE_SIZE_LIMIT
  ? parseInt(process.env.IMAGE_INLINE_SIZE_LIMIT)
  : 1024 * 10;

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

4. HTML 관련 webpack 플러그인으로 index.html 설정하기

HTMLWebpackPlugin은 해당 html 파일 내의 body 태그 밑에 script 태그를 사용해서 webpack 번들링 파일들을 포함시킵니다. 여기서는 index.html에 빌드된 파일들을 추가하기 위해 사용하도록 하겠습니다.

# HTML 관련 plugin 설치
$ yarn add -D html-webpack-plugin

패키지를 설치하고 나면 webpack.common.js을 아래와 같이 작성해줍니다.

'use strict';

// ... 생략

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // ... 생략
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      filename: 'index.html',
    }),
  ],
  // ... 생략
};

이렇게 설정하게 되면 빌드 후에 나온 스크립트 파일들이 script 태그를 통해서 index.html에 추가되게 됩니다.

5. 타입스크립트 tsconfig.json 추가

타입스크립트를 위한 tsconfig.json을 생성합니다.

{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "moduleResolution": "node",
    "noResolve": false,
    "noImplicitAny": false,
    "removeComments": false,
    "sourceMap": true,
    "allowJs": true,
    "jsx": "react-jsx",
    "allowSyntheticDefaultImports": true,
    "keyofStringsOnly": true,
    "baseUrl": ".",
  },
  "typeRoots": ["node_modules/@types", "src/@types"],
  "exclude": [
    "node_modules",
    "build",
    "scripts",
    "webpack",
    "./node_modules/**/*"
  ],
  "include": ["./src/**/*", "@type", "__tests__"]
}

6. 기본 코드 작성

이제 간단한 샘플 앱을 작성해보도록 하겠습니다. public/index.html, src/index.tsx, src/App.tsx를 생성해주고 아래의 내용들을 작성해줍니다.

  • public/index.html
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>React App</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
  • src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
  • src/App.tsx
import React, { FC } from "react";

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

export default App;

7. 스크립트 추가 및 테스트

package.json에 아래와 같이 빌드를 위한 스크립트 명령을 추가해주고 실행해보면 잘 실행되는 것을 확인할 수 있습니다.

// ... 생략
"scripts": {
  "dev": "webpack serve --mode development",
  "build": "webpack --mode production --progress"
},
// ... 생략

실행 스크린샷

profile
꾸준함을 만들고 싶어요

1개의 댓글

comment-user-thumbnail
2021년 11월 9일

웹펙5버전에서는 asset-module을 이용해서 이전에 사용했던 url-loader를 대체한다고 하는데 url-loader, file-loader를 이용하면 최적할 수 있기 때문에 이런 설정을 하는건가요??
https://webpack.kr/guides/asset-modules/

답글 달기