CRA없이 React + TypeScript 셋팅하기!

chanyeong cho2020년 6월 28일
3

CRA(Create React App)은 프로덕션 용도의 애플리케이션을 개발할 수 있는 환경을 가장 쉽게 셋팅하는 방법 중 하나이다. 초기에 복잡한 Babel과 Webpack, TypeScript등의 설정 필요 없이 명령어 하나로 개발에 필요한 최소한의 환경을 셋팅해준다.

사실 나는 리액트를 처음 시작했을 때부터 지금까지 간단한 토이프로젝트를 제외하고는 CRA를 사용한 적이 없다. 보통 이런식으로 프로젝트 셋팅을 하다가 어느샌가 Next.js로 넘어가게 됬다. (개인적인 생각으로 Next.js가 CRA보다 프로젝트 셋팅이 훨~~씬 편한 것 같다.)

그렇다면 왜 CRA없이 리액트 프로젝트를 셋팅할까?

CRA는 충분히 좋은 툴(?)이다. 초기에 리액트를 시작할 때 수많은 환경 셋팅 때문에 다소 진입 장벽이 높아보일 수 있지만 CRA를 이용하면 손쉽게 리액트를 시작할 수 있다.

하지만 CRA는 너무 편리하다. CRA에 의존하다 보면 Webpack, Babel에 추가적인 설정을 해야 할 때 어려움이 발생할 수 있다. 그러므로 CRA없이 리액트 프로젝트를 셋팅하는 방법을 알아보려 한다.

리액트 및 타입스크립트 설정

$npm init -y
$npm i react react-dom
$npm i -D typescript @types/react @types/react-dom

프로젝트를 초기화 한 뒤 리액트와, 타입스크립트 패키지를 설치한다. 리액트는 공식적으로 타입스크립트를 지원하지 않기 때문에 따로 타입 정의 패키지를 설치해야 한다.

타입스크립트 설정

$node_modules/.bin/tsc --init

이제 타입스크립트에 대한 설정파일을 만들어야 한다. 타입스크립트가 전역으로 설치되어 있다면 바로 tsc —init을 호출해도 되지만 해당 프로젝트에만 설치되어있을 경우에는 경로를 직접 명시해 tsc를 호출해야 한다.

tsconfig.json

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es5",
    "lib": ["dom", "ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020"],
    "allowJs": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "module": "commonjs",
    "isolatedModules": true,
    "jsx": "preserve",
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./",
    "outDir": "./dist",
    "moduleResolution": "node"
  },
  "exclude": ["node_modules"],
  "include": ["**/*.ts", "**/*.tsx"]
}

타입스크립트 설정파일은 보통 자기 스타일에 맛게 설정하면 된다. 나같은 경우는 서버와 리액트, Next.js에 경우 매번 설정파일이 달라서 따로 저장해 놓고 사용하는 편이다.

리액트 entry file 설정

index.tsx

import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';

const rootElement = document.getElementById('root');

ReactDOM.render(<App />, rootElement);

기본적인 리액트 index.tsx파일을 만들어 준다. 여기서 root라는 id값을 가진 엘리먼트를 가져오기 때문에 나중에 index.html에서 만들어 줘야 한다.

App.tsx

import React from 'react';

const App = () => <>Init</>;

export default App;

App.tsx파일 역시 기본적인 컴포넌트만 만들어 준다.

index.html

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

아까 index.tsx에서 말했듯이 index.html에는 반드시 root라는 id값을 가진 div를 생성해 줘야 한다.

Babel 설정

$npm i -D babel-loader @babel/core @babel/preset-env
$npm i -D @babel/preset-react @babel/preset-typescript

타입스크립트는 자체 기능으로 트랜스파일링 기능이 있기 때문에 굳이 babel을 사용하지 않아도 된다. 하지만 개발을 진행하다보면 추가적으로 babel이 필요한 경우가 있기 때문에 설치했다.

babel.config.js

module.exports = {
  presets: ['@babel/preset-react', '@babel/preset-env', '@babel/preset-typescript'],
};

다음과 같이 바벨 설정 파일을 생성한다.

Webpack 설정

$npm i -D webpack webpack-cli webpack-dev-server
$npm i -D html-webpack-plugin ts-loader

이제 webpack을 설치해줘야 한다. 기본적으로 webpack webpack-cli가 필요하지만 생성된 html파일에 필요한 플러그인인 html-webpack-plugin과 개발 도중 변경사항을 확인할 수 있는 webpack-dev-server, 그리고 타입스크립트 코드를 자바스크립트 코드로 변환해주는 ts-loader를 설치한다.

(ts-loader와 awesome-typescript-loader가 있지만 최근 ts-loader의 이용자 수가 더 많기 때문에 ts-loader를 선택했다.)

webpack.config.js

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

const prod = process.env.NODE_ENV === 'production';

module.exports = {
  mode: prod ? 'production' : 'development',
  devtool: prod ? 'hidden-source-map' : 'eval',

  entry: './src/index.tsx',

  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: ['babel-loader', 'ts-loader'],
      },
    ],
  },

	output: {
    path: path.join(__dirname, '/dist'),
    filename: 'bundle.js',
  },

  plugins: [
		new webpack.ProvidePlugin({
      React: 'react',
    }),
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
};
  • mode: 프로덕션 모드인지 개발 모드인지 확인하는 옵션이다.
  • devtool: 프로덕션 모드인 경우엔 hidden-source-map을 권장한다. (외부에서 리액트 구조를 확인할 수 없다.)
  • resolve: 확장자나 경로를 알아서 처리할 수 있도록 설정하는 옵션이다.
  • module: 이 옵션에 설치한 ts-loaderbabel-loader를 설정하면 된다. loader들은 오른쪽에서 왼쪽 방향으로 적용되기 때문에 ts-loaderbabel-loader보다 오른쪽에 위치시켜야 한다.
  • output: 번들화 된 파일을 export할 경로와 파일명을 설정한다.
  • plugins: 설치한 플러그인을 적용하는 옵션이다.

WDS (webpack-dev-server)

webpack.config.js

const webpack = require('webpack');
const path = require('path');

...
module.exports = {...
	devServer: {
    historyApiFallback: true,
    inline: true,
    port: 3000,
    hot: true,
    publicPath: '/',
  },

	plugins: [
		...,
    new webpack.HotModuleReplacementPlugin(),
		...,
  ],
...
}

이제 webpack-dev-server를 설정해줘야 한다. webpack-dev-server는 빠른 실시간 리로드 기능을 갖춘 개발 서버로 개발하는 과정에서 코드가 정확히 수정이 일어났는지 바로바로 확인할 수 있어서 유용하다. (사실상 거의 필수로 필요하다)

  • historyApiFallback : 히스토리 API를 사용하는 SPA 개발 시 설정하며 404에러가 발생하면 index.html로 리다이렉트 한다.
  • inline : inline모드를 활성화 해준다.
  • port : 접속 포트를 설정한다.
  • hot : webpack의 HMR기능을 활성화 한다. (리로드 기능)
  • publicPath : 브라우저를 통해 접근하는 기본 주소값을 설정한다.

다음 옵션들을 설정한뒤 pluginsHotModuleReplacementPlugin을 추가한다.

이제 기본적인 webpack 설정이 끝났다! 이 다음은 명령어 설정을 해줘야 한다.

package.json

...,
"scripts": {
    "dev": "webpack-dev-server --mode development --open --hot",
    "build": "webpack --mode production",
    "prestart": "npm build",
    "start": "webpack --mode development"
  },
...

이제 package.json에 실행 명령을 추가해준다. dev 명령어는 webpack-dev-server를 실행하는 명령어고 start는 리액트 프로젝트를 빌드해 dist폴더 안에 번들링 된 파일을 추출시켜 준다.

지금까지 기본적인 리액트 프로젝트 셋팅이 끝났다. 사실 이정도에서 끝낼게 아니라 SSR, 코드 스플리팅, ESLint, Prittier 등 더 많은 기본 셋팅이 필요하지만 이번 포스트에서는 React + TypeScript + webpack + babel만을 알아보았다.