CRA 없이 React 앱 설정하기 (+ TS, Eslint/Prettier) — (1) Babel과 Webpack

Bling
15 min readJul 3, 2022

--

Photo by Lautaro Andreani on Unsplash

시리즈

(0) 동기와 방향성
(1) Babel과 Webpack
(2) TypeScript와 React
(3) Eslint와 Prettier

1. npm init

이번 설정 과정에서 사용할 패키지 매니저는 npm이다. npm으로 패키지를 설치하고 관리하기 위해서는 npm init 명령어를 사용해서 package.json 파일을 생성해야 한다.

명령어를 실행하면 프로젝트의 기본 정보를 입력하라는 명령이 출력된다. 프로젝트의 이름과 설명, 버전 정보 등을 물어보는데 대부분 엔터를 통해 기본 값으로 설정해도 괜찮은 설정이다. 추가적으로 수정하고 싶다면 직접 생성된 package.json 파일에서 수정할 수 있다.

2. Babel

다음 명령을 실행해서 babel을 설치한다.

npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/preset-react @babel/preset-typescript

각각 패키지는 다음과 같은 역할을 한다.

  1. @babel/core: 실제로 babel의 작동을 위한 코드를 담고 있는 핵심 패키지
  2. @babel/cli: cli를 이용해 직접 babel을 가동해서 트랜스파일을 하기 위해서 필요한 패키지 (작성 중 발견한 사실: 사실 상 무조건 webpack을 통해서 babel을 활용할 것이기 때문에 필요 없을 지도 모르겠다. 추후 확인 후 삭제 예정)
  3. @babel/preset-env(공식 문서): 가장 기본이 되는 패키지, 최신 JS 문법으로 작성한 코드를 설정한 대상 환경에 맞게 변환해주는 preset 패키지
  4. @babel/preset-react(공식 문서): React의 사용을 위해서 필요한 패키지, 핵심적으로는 JSX를 JS로 변환하기 위해서 필요한 preset 패키지
  5. @babel/preset-typescript(공식 문서): JS의 슈퍼셋인 TypeScript를 일반 JS로 변환하기 위해서 필요한 preset 패키지

설치가 끝났다면 이제 babel이 제대로 작동할 수 있도록 설정을 해주어야 한다. Babel은 여러 파일에서 설정할 수 있는데, 공식 문서에서 추천하는 방법은 babel.config.json을 이용하는 것이다. 그 대체제로는 .babelrc.json을 제시하고 있다.

두 파일의 차이점은 간단하게 설명하자면 각각 전역으로 트랜스파일링을 적용할 것인지 프로젝트의 일부분에만 트랜스파일링을 적용할 것인지 여부이다. 이 앱에서는 전역적으로 트랜스파일링이 필요하다고 판단해서 전자를 택했다. 사실 이전에 다른 프로젝트를 진행하면서는 후자도 자주 사용했던 기억이 있기 때문에 이것이 실질적으로는 어떤 차이가 있는 지는 아직 확신하지 못한다.

babel.config.json파일은 다음과 같이 작성했다.

{
"presets": [
"@babel/preset-env",
["@babel/preset-react", { "runtime": "automatic" }],
"@babel/preset-typescript"
],
"targets": "> 0.5%, not dead"
}

설치한 preset 세 가지를 preset 설정에 배열로 추가해주었다.

이 중 눈여겨보아야 하는 것은 preset-react와 관련한 설정이다. 유일하게 단순한 문자열이 아니라 배열로 되어있는데, 각각 [패키지, 설정]을 의미한다. 설정에 "runtime""automatic"으로 설정하면 React의 JSX에서 React를 실제로 import 해오지 않더라도 자동으로 해석해준다. 이는 React 17에서 추가된 기능인데, CRA를 활용하면 자동으로 적용이 되지만 지금처럼 자체 설정을 하는 경우에는 직접 추가해주어야 한다 (공식 문서). 작동 방식은 기존에 JSX의 문법을 변환하면 React.createElement()가 되었던 것과는 다르게 JSX가 활용될 때 트랜스파일러인 babel이 직접 jsx라는 함수를 _jsx라는 익명으로 import해서_jsx()로 변환해주는 식이다.

그 다음 target을 설정해주었다. target은 트랜스파일을 통해서 출력하는 코드가 어떤 환경을 대상으로 하는 지에 대한 설정이다. 이 곳에는 문자열, 문자열로 이루어진 배열, 문자열을 값으로 하는 객체가 올 수 있다.

문자열로 된 설정은 제 3자 패키지인 browserify가 해석할 수 있는 브라우저 환경 문자열이다. 현재 설정으로는 “0.5%를 초과하는 브라우저 중 최근 24개월 동안 공식 지원 혹은 업데이트가 있었던 브라우저”를 대상으로 한다. 더 자세한 내용은 browserify가 제공하는 전체 목록을 참고하면 된다.

객체로 된 설정은 각 환경의 이름과 버전을 직접 지정하고자 할 때 활용한다.

target을 설정하지 않으면 babel은 자동으로 지원할 수 있는 가장 오래된 브라우저 환경을 대상으로 한 트랜스파일링을 진행한다. 예를 들어 preset-env를 적용한 상태라면 ES5 코드로 변환하게 된다. 자연스럽게 이런 설정 하에서는 출력되는 코드의 양이 증가하고 여러 가지 이유로 성능에 좋지 않기 때문에 꼭 default로라도 설정해줄 필요가 있다. (default는 browserify에서 해석해서 기본 설정으로 적용한다.)

3. Webpack

1. 설치

다음 명령어를 실행해서 webpack을 설치했다.

npm install --save-dev webpack webpack-cli webpack-dev-server

각각 패키지는 다음과 같은 역할을 한다.

  1. webpack: 핵심 작동 코드가 담겨있는 패키지
  2. webpack-cli: webpack을 cli를 통해서 실행하기 위한 패키지
  3. webpack-dev-server: 개발할 때 활용하는 개발 서버를 활용하기 위한 설정을 가능하게 하는 패키지

또 webpack에서 활용할 loader를 설치해야 한다. webpack은 loader를 다음과 같이 정의하고 있다.

Loaders are transformations that are applied to the source code of a module. They allow you to pre-process files as you import or “load” them.

Loader는 모듈의 소스 코드에 적용되는 변환입니다. 이는 당신이 import, 즉 load 할 때 파일을 전처리할 수 있게 합니다.

조금 더 풀어서 설명해보자면 번들링하는 모듈이 어떤 파일을 import할 때, 그 파일을 어떻게 해석할 지를 결정하는 것이 loader의 역할이라고 할 수 있다. 상식적으로 생각한다면 JS 파일에서는 모듈로 불러오더라도 다른 JS 파일만 해석할 수 있어야 할 것 같다. 하지만 loader를 활용하면 이미지, css 등 기타 많은 파일을 JS 파일로 불러와서 활용할 수 있다.

지금 설정에서 필요한 loader를 설치하기 위해서는 다음 명령어를 실행한다.

npm install --save-dev babel-loader css-loader sass sass-loader style-loader

babel-loader는 앞서 설치하고 설정한 babel을 활용해서 JS, TS와 React 파일인 JSX, TSX 파일을 사용하기 위해서 필요하다

css, sass, style-loader는 css를 사용하기 위해서 필요한 코드들이다. 이렇게 많이 필요한 이유는 후술할 설정에서 더 자세하게 설명할 예정이다.

sass를 설치한 이유는 sass-loader에서 sass를 필요로 하기 때문이다.

마지막으로 설치할 것은 플러그인 중 하나로 번들링 후 출력된 파일을 자동으로 불러오는 HTML 파일을 생성하는 데에 사용하는 플러그인이다.

npm install --save-dev html-webpack-plugin

2. 설정

이제 본격적으로 설정을 할 수 있다. 설정 파일은 CommonJS의 방식으로 모듈이 관리되며 따라서 기본적으로 다음과 같은 형식을 띄고 있다.

module.exports = {
// ...
}

기본 설정

가장 먼저 entry과 output을 설정해서 파일을 불러오고 출력할 수 있도록 한다.

const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.[hash].js',
clean: true,
},
}

entry는 앱의 소스코드 중 가장 최상단 모듈의 경로로 설정한다. 대부분의 경우 React에서는 src 디렉토리 내부의 index.js가 이 역할을 한다.

output은 변환하고 번들링된 파일을 어느 디렉토리에 어떤 파일명으로 저장할 지에 대한 설정이다. 경로를 설정하기 위해서 path 모듈을 불러와야 한다. 파일명에는 각 build마다 고유한 hash 값을 부여하기 위한 설정을 추가했다. 단순히 파일명 문자열 안에 대괄호로 감싸진 hash 키워드만 추가하면 된다. 추측하건데 해당 파일명 설정으로 caching 시에 같은 파일인 지에 대한 여부를 판단할 것 같다.

clean 설정은 새로 build 시에 기존의 build된 파일을 모두 삭제하는 옵션을 boolean으로 설정할 수 있다. 만약 false로 설정한다고 해도 동일한 파일명을 가진 파일들은 덮어쓰여지겠지만, 기존의 build에 포함되었던 static 파일 중 삭제된 것이 있더라도 build 결과물에 이것이 삭제되지 않고 남아있다. 따라서 이전 모든 build가 독립적이기 위해서는 이 옵션을 true로 설정해야한다. 이 설정은 과거 CleanWebpackPlugin으로만 가능하던 설정을 webpack에서 제공하는 것이다.

Loader 설정

그 다음에는 각각의 loader를 설정해주어야 한다. 이 설정은 아래와 같은 구조로 추가할 수 있다.

module: {
rules: [
{
// loader 설정
},
// ... 여러 loader
],
},

우선 가장 많이 사용하는 JS와 React 관련 파일에 대한 설정이다.

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

기본 구성은 test, exclude, use로 이루어져있다.

  1. test: 어떤 파일을 대상으로 하는 지를 검증할 수 있는 정규 표현식 (주로 확장자)
  2. exclude: 위 정규표현식으로 대상이 되더라도 무시할 파일 및 디렉토리에 대한 정규표현식
  3. use: 어떤 loader를 활용할 지에 대한 규칙

여기에서 눈여겨 볼 점 한 가지는 ts, tsx 파일을 해석하기 위해서 별도의 loader를 설정하지 않는다는 점이다. 사실 ts를 위해서 ts-loader라는 별도의 loader를 추가할 수 있다. 하지만 이미 babel-loader의 preset으로 ts를 트랜스파일할 수 있게 설정해주었고, 따라서 이것만으로 이미 TS를 변환할 수 있다. 차이점이 존재하긴 하지만 아직 그 필요성을 느끼지 못해 별도로 설정하지 않았다.

다음은 css 관련 설정들이다.

{
test: /\.s?css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true,
},
},
{ loader: 'sass-loader' },
],
},

(sass까지 활용한다고 가정했을 때) css를 위해서 필요한 세 가지 loader가 각각 다른 역할을 한다. 그 역할에 대해서 설명하기 위해서는 위와 같이 여러 loader가 있을 때 어떤 식으로 작동하는 지 알아야 한다. 흥미롭게도 여러 loader가 있을 때는 아래에서 위의 순서대로 모든 loader가 한 번씩 작동한다.

그 순서에 맞춰 우선 sass-loader는 sass 파일을 css로 변환하는 역할을 한다. 다음으로 css-loader는 css 파일 내부에서 @import 혹은 url을 활욜할 때 이것이 JS가 모듈을 처리하는 방식 (import 혹은 require())과 동일하게 작동하도록 해석한다. 마지막으로 style-loader는 이렇게 변환된 css를 dom에 주입한다.

이 중 css-loader의 module 옵션을 true로 설정했을 때만 모든 파일에 대해서 css 모듈의 활용을 가능하다.

마지막으로 이미지에 대한 설정이다.

{
test: /\.(png|jpe?g|gif|ico)$/,
type: 'asset/resource',
generator: {
filename: 'images/[hash][ext][query]',
},
},

흥미롭게도 여기에는 loader가 설정되어있지 않다. 별도의 loader도 설치하지 않았다. 이는 webpack 5에서 추가된 Asset Module을 활용하면 별도의 패키지 없이도 이미지 등의 asset을 활용할 수 있다. (webpack 4까지 활용했던 url-loader, file-loader는 deprecated 되었다.)

그 중 모듈 타입을 asset/resource으로 설정하면 해당 asset들을 별도의 파일로 emit하고 url을 export 한다.

generator 설정은 webpack을 통해서 emit된 asset의 명칭을 지정하는 데에 사용된다. 그 방식은 output.assetModuleFilename 매서드의 방식과 동일하다고 설명이 되어있다. 공식 문서에서 기본적으로 제공하는 파일명 생성 규칙과 동일하게 설정했는데, 마지막 query가 정확하게 어떤 것을 의미하는 지는 추가적인 공부가 필요하다.

개발 서버 설정

설치한 webpack-dev-server 패키지를 활용하면 개발을 할 때 임시로 사용할 서버를 설정할 수 있다.

devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
historyApiFallback: true,
compress: true,
port: 3000,
},

static은 static 파일의 위치를 나타내며 기본으로 public 디렉토리로 설정되어 있다.

historyAPIFallback은 SPA에서 발생하는 문제 중 하나인 router로 경로 이동 후 새로고침을 하면 404 오류가 발생하는 현상을 임시로 해결해주는 설정이다. true로 설정 되어있으면 404 오류를 반환하는 대신에 index.html을 반환해서 정상적으로 해당 경로를 화면에 출력할 수 있도록 도와준다.

compress는 출력되는 결과물이 gzip으로 압축된 파일로 반환하도록 하는 설정이다. 이 압축 파일은 브라우저에서 압축 해제된다.

port는 개발 서버를 실행할 포트에 대한 설정이다. React는 통상적으로 3000 포트를 사용한다.

기타 설정

HtmlWebpackPlugin에 대한 설정이다.

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

//...

plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './public/index.html'),
}),
],

아무런 설정 없이 해당 플러그인을 추가해도 정상적으로 작동하지만, template 옵션을 인자로 추가하면 사전에 설정한 template 파일을 기본으로 해서 플러그인이 작동한다.

JS, TS, JSX, TSX 파일에 한해서 import 시에 확장자를 생략하려면 아래와 같은 설정을 추가할 수 있다.

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

마지막으로 mode를 설정해주어야 한다. mode는 development와 production으로 설정할 수 있는데 이를 기반으로 webpack이 내장된 각 환경에 적합한 최적화를 적용할 수 있다. 통상적으로 활용할 수 있는 최적의 시나리오는 평소에는 development로 개발을 진행하고 배포를 위한 build를 하고자 할 때만 production를 적용하는 것이다. 이를 위해서 아래와 같이 설정했다.

mode: process.env.production === 'true' ? 'production' : 'development',

간단하게 환경 변수에 production이 ‘true’로 설정된 경우에는 production, 그렇지 않은 경우에는 development로 설정된다. (해당 설정 파일이 JS 파일이기 때문에 가능한 설정 방식이다.)

이를 활용하기 위한 cli 설정을 package.json의 script에 추가한다.

"scripts": {
"build": "npx webpack build --env production",
"start": "npx webpack serve",
},

--env 플래그를 활용하면 해당 cli 명령어가 실행할 때 환경변수를 설정할 수 있다.

위 설정을 위한 공식 문서에서는 다른 방식도 제시하고 있는데, 둘의 차이점과 어떤 것이 더 나은 지는 추후에 공부가 필요하다.

다음 편에서는 본격적으로 TypeScript와 React를 설정한다.

--

--