cra없이 리액트 환경 설정하기

CRA없이 한땀한땀 리액트 환경 설정을 해보자

원활한 설명을 위해 중복되는 설정 코드는 제거했습니다. 전체 코드는 여기서 확인가능하니, 본 글은 이해의 용도로만 활용해 주시기 바랍니다!

Create-React-App없이 웹팩과 바벨 설정으로 리액트 개발 환경을 구성해보겠습니다. Tech Stack은 다음과 같습니다.

  1. Wepback
  2. Babel
  3. ESLint & Prettier
  4. Storybook
  5. React
  6. Typescript
  7. Emotion

React없이 세팅하기

우선 npm init -y 를 해서 package.json을 생성합니다.

typescript

설치

npm install --save-dev typescript

설정

// tsconfig.json

{
  "compilerOptions": {
    "target": "esnext", // default : es3
    // 몇 버전의 ECMAScript로 컴파일할것인지 지정합니다.
    // 예를들어, 'es5'로 지정하면 화살표 함수를 함수선언식으로 변경합니다.
    // esnext로 지정하면 현재 프로젝트에서 사용하는 typescript버전이
    // 지원하는 가장 높은 ECMAScript 버전을 알아서 선택합니다

    "allowJs": true,
    // js파일도 ts파일에서 import할 수 있습니다

    "skipLibCheck": true,
    // 외부 라이브러리에 명시된 타입에 대한 검사를 진행하지 않습니다.
    // 다만, 우리가 외부 라이브러리에서 사용한 코드에 대해서는 검사를 해줍니다.
    // 주로 이 옵션을 true로 해주는 경우는 3가지가 있습니다
    // - 첫째 - 
    // 타입스크립트가 외부 라이브러리까지 전부 타입체크를 하면 너무 오래걸립니다.
    // 그래서 컴파일 속도를 높일때 사용합니다.
    // - 둘째 -
    // 외부 라이브러리의 타입에 문제가 있을때 이 옵션을 키면
    // 그냥 any로 해석해줍니다.
    // - 셋째 -
    // 외부 라이브러리에서 사용한 TS버전과 지금 내 프로젝트에서 사용하고 있는 TS버전이
    // 다른 경우에, 이 옵션을 키면 에러를 안뿜습니다.
    // 그리고 외부 라이브러리에서 타입 체크를 덜 엄격하게 하기로 하고 any를 썼는데
    // 내 프로젝트에서는 타입을 업격하게 하기로 설정 했다면, 이 설정의 차이에서 오는 오류들을
    // 무시할 수 있도록 해줍니다.

    "strict": true,
    // strict로 시작하는 옵션들을 true로 해줍니다.
    // strictFunctionTypes, strictNullChecks같은 옵션들이 true로 됩니다.

    "forceConsistentCasingInFileNames": true,
    // import할때 파일명을 대소문자도 고려해서 똑같이 적어줘야 합니다

    "moduleResolution": "node",
    // module 찾는 방식을 지정합니다.
    // 자세한 내용은 https://www.typescriptlang.org/docs/handbook/module-resolution.html#module-resolution-strategies
    // 요기를 보면 나와있습니다.

    "isolatedModules": true,
    // 하나의 파일이 독립된 모듈로 되기를 강제합니다
    // 여기서 독립된 모듈이란 import나 export가 파일안에 꼭 있어야 한다는 의미입니다
    // 이것은 babel같은 외부 컴파일러를 위한 옵션이라고는 하는데, 이것이 무엇을 의미하는지는 모르겠습니다.

    "noEmit": true,
    // 타입체크만 하고 js로 컴파일 하지 않습니다
    // 현재 babel을 사용해서 컴파일 할것이기 때문에 true로 설정해 줍시다

    "resolveJsonModule": true,
    // json파일도 모듈로 취급해서 import할 수 있게 합니다

    "baseUrl": "./src"
    // import something from './src/some/thing.ts' 이렇게 안하고
    // import something from 'some/thing.ts' 이렇게 해도 잘 작동하도록 합니다.
    // 즉, 번거롭게 앞에 path를 계속 적어줘야 하는 불편함을 줄여줍니다.

  },

  "include": ["src"]
  // ts가 인식하고 컴파일할 파일의 경로를 지정합니다.
  // 현재 src폴더만 ts를 사용할것이기 때문에, src를 넣어줍니다.
}

babel을 사용해서 컴파일 할것이기 때문에 설정이 많지 않습니다.

webpack

설치

npm install webpack webpack-cli webpack-dev-server webpack-merge html-webpack-plugin clean-webpack-plugin --save-dev

webpack

webpack은 모듈 번들러 입니다. javascript파일들의 의존성을 고려해 번들링 합니다. js뿐만 아니라 image, svg등 다른 자원들도 번들링 할 수 있습니다.

webpack-cli

터미널 환경에서 webpack을 사용할 수 있도록 명령어들을 제공합니다. 예를들어서, 터미널에서 webpack --config ./webpack.prod.js 이렇게 치면 webpack이 작동합니다.

webpack-dev-server

webpack을 위한 노드 서버입니다. 파일의 변경사항을 감지해서 다시 빌드하고 브라우저를 자동으로 새로고침해줍니다.

html-webpack-plugin

index.html파일을 생성해주는 플러그인입니다. 빌드시에 index.html에 여러가지 값들을 넘길 수 있습니다.

clean-webpack-plugin

빌드할때 dist폴더의 내용물을 싹 지우고, 새로 빌드된 파일을 넣습니다. 종종 이전에 빌드할때 생성된 파일들이 남아있는 경우가 있어서 이 플러그인이 필요합니다.

webpack-merge

webpack설정 파일을 병합해주는 함수를 제공하는 라이브러리입니다.

설정

개발 환경용 설정 파일과 프러덕션용 설정 파일을 분리합니다.

webpack.common.js

const { join, resolve } = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: join(__dirname, '../src/index.ts'),
  devtool: 'eval-source-map',
  output: {
    filename: 'main.js',
    path: join(__dirname, '../dist'),
  },
  module: {
    rules: [
      {
        test: /\.(ts)$/, // .ts파일을 만나면
        exclude: /node_modules/, // node_modules를 제외하고
        use: ['babel-loader'], // babel-loader에게 일을 맡긴다!
      },
      {
        test: /\.(png|jpg|jpeg)$/i,
        type: 'asset/resource', // asset/resource는 webpack5 에서 url-loader, file-loader대신 지원하는 기능입니다
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: join(__dirname, '../public/index.html'),
      favicon: join(__dirname, '../public/favicon.png'), // favicon경로도 설정할 수 있습니다
    }),
    new CleanWebpackPlugin(),
  ],
  resolve: {
    extensions: ['.ts', '.js'], 
  }
};

webpack.dev.js

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

const common = require('./webpack.common');

module.exports = merge(common, { // webpack.common.js와 merge해줍니다
  mode: 'development',
  devServer: {
    open: true,
    port: 3000,
    compress: true,
    client: {
      overlay: {
        errors: true,
        warnings: true,
      },
    },
    historyApiFallback: true,
    // url을 찾을 수 없을때 그냥 index.html을 돌려줍니다.
    // react-router-dom쓸때 필요합니다.
  },
});

webpack.prod.js

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

module.exports = merge(common, {
  mode: 'production',
});

Babel

설치

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

@babel-core

js코드를 transform하는 핵심 기능이 모아져 있다. 하지만, 변환을 하려면 core만 단독으로 사용하는것이 아닌 plugin이 필요하다.

@babel-cli

babel을 터미널에서 사용할 수 있도록 명령어들을 제공한다

@babel/preset-env

자주 쓰는 바벨 플러그인들을 세트로 모아놓았다.

https://github.com/babel/babel/blob/master/packages/babel-preset-env/src/available-plugins.js

@babel/preset-typescript

내부적으로 @babel/plugin-transform-typescript플러그인을 사용한다. 이 플러그인은 단순히 type을 때내어준다.

// input
const x: number = 0;

// output
const x = 0;

babel-loader

Webpack이 파일을 빌드할때 js파일을 만나면 이 babel-loader가 babel을 작동시켜 js파일을 변환한다

설정

.babelrc.json

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ]
}

ESLINT & prettier

설치

npm install eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-import eslint-import-resolver-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser @trivago/prettier-plugin-sort-imports --save-dev

eslint

js파일 실행시에 발생할 수 있는 문제를 사전에 검사해주고, 코딩 컨벤션을 강제합니다. prettier처럼 코드 스타일에 관련된 룰도 있긴 합니다.

prettier

eslint가 에러를 방지하기 위함이라면, prettier는 코드의 스타일을 강제합니다. 예를들어서 indentation으로 space를 사용할것인지 tab을 사용할 것인지 설정할 수 있습니다.

eslint-config-prettier

ESLint의 formatting 관련 설정 중 Prettier와 충돌하는 부분을 비활성화 한다

eslint-plugin-prettier

Prettier를 ESLint 플러그인으로 추가한다. 즉, Prettier에서 인식하는 코드상의 포맷 오류를 ESLint 오류로 출력해준다.

주의! 공식 문서의 설명에 따르면, plugin:prettier/recommended 했을때, eslint-config-prettier가 알아서 적용되니, 따로 설정할 필요가 없다.

eslint-plugin-import

ES6에 추가된 import/export 구문을 사용할때 혹여나 파일 경로를 잘못 지정 했는지, 파일 이름은 똑바로 썼는지 검사해 주는 플러그인이다

eslint-import-resolver-typescript

eslint-plugin-import에 ts지원을 추가해준다. 다시말해서, ts나 tsx도 import할때 lint를 적용할 수 있게 된다.

추가적으로, tsconfig.json에서 정의한 paths속성을 사용한다. 나중에 설명하겠지만, 덕분에 tsconfig.jsonpaths에 설정한 절대 경로를 eslint가 인식한다.

@typescript-eslint/eslint-plugin

eslint는 본래 js용인데, 이 플러그인을 사용하면 ts도 인식해준다. 또한 ts에만 적용되는 특별한 rule들도 지정할 수 있다

@trivago/prettier-plugin-sort-imports

import 순서를 깔끔하게 정리해준다.

import { css } from '@emotion/react';

import MainPage from '@pages/MainPage';

import Footer from '@components/Footer';
import Header from '@components/Header';
import Wrapper from '@components/Wrapper';

설정

.eslintrc.json

{
  "env": {
    "browser": true,
    "es2022": true
  },
  "settings": {
    "import/resolver": {
      "typescript": {}
    }
  },
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "extends": [
    "plugin:import/recommended",
    "plugin:import/typescript",
    "plugin:prettier/recommended",
    "plugin:@typescript-eslint/recommended",
  ],
  "rules": {
    "import/extensions": [
      "error",
      "ignorePackages",
      {
        "ts": "never",
        "js": "never"
      }
    ],
  }
}

plugins속성에 일일이 eslint plugin을 추가하지 않고 extends"plugin:~~"이렇게 적어도 됩니다.

.prettierrc.json

{
  "printWidth": 120,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "bracketSpacing": true,
  "jsxBracketSameLine": false,
  "arrowParens": "avoid",
  "endOfLine": "auto",

  "importOrder": [
    "^@emotion",
    "^@pages/(.*)$",
    "^@components/(.*)$",
    "^[./]"
  ],
  "importOrderSeparation": true,
  "importOrderSortSpecifiers": true
}

이렇게 typescript, webpack, babel, eslint & prettier를 설치했습니다.

잠깐!

여기서 VSC를 한번 껏다가 켜줍시다!! 종종 eslint가 prettierrc.json을 안읽는 경우가 있기 때문입니다.

React와 함께 설정하기

React

설치

npm install react react-dom

typescript

설치

npm install @types/react @types/react-dom  --save-dev

설정

{
  "compilerOptions": {
    ...
    "esModuleInterop": true, // 필요 없는데 설명을 위해 넣었습니다
    "allowSyntheticDefaultImports": true,
    ...
    "jsx": "preserve", // 어차피 babel을 쓸것이기 때문에 그냥 변환하지 않고 <div>로 냅둔다(preserve)
    "jsxImportSource": "@emotion/react", // emotion의 css props를 사용하기 위해 필요합니다
    ...
  },
  ...
}

allow_synthetic_default_imports

(이부분은 저도 잘 몰라서 설명이 애매합니다. 그냥 참고용으로 읽어주시면 감사하겠습니다)

어떤 모듈을 import하는데, 그 모듈이 export default를 하지 않았는데, export하는 요소들을 하나의 객체로 가져오고 싶다면 보통 import * as React from 'react' 이런식으로 씁니다. 저 React객체 안에는 react모듈이 export하는 모든 요소들이 담겨져 있는것이죠. 하지만 Babel은 편의를 위해서 import React from 'react' 이렇게 작성해도 React라는 객체에 ‘react’가 export한 요소들을 다 넣어줍니다.

하지만 typescript는 이 사실을 모르기 때문에, 타입 에러를 뿜습니다.

“아니, export default가 없는데, 마치 export default한것처럼 사용하네!? 그러면 안돼!”

그래서 typescript에게 알려줘야합니다.

“걱정하지마, 어차피 Babel이 알아서 해줄꺼야”.

바로 allowSyntheticDefaultImports 옵션이 이겁니다.

esmodule_interop

하지만 babel을 사용하지 않는 경우는 이 옵션을 써야합니다.

왜냐하면 기본적으로 react는 commonJs방식으로 export를 하고 있고, 이 프로젝트에서는 esmodule방식으로 import를 하고 있기 때문입니다.

그래서 esModuleInterop를 키고 Babel없이 import * as React from 'react' 혹은 import React from 'react' 를 사용한다면, typescript는 컴파일 할때 내부적으로 아래와 같은 함수를 생성합니다.

var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};

저도 자세히는 모르겠지만, 그냥 __importStartimport * as React from 'react' 이 코드에서 react모듈이 export하는 요소들을 React객체에 담아서 return하는 것 같고

__importDefaultimport React from 'react' 이 코드에서 react모듈이 export하는 모든 요소를 React객체에 담아서 return하는듯 싶습니다.

아무튼 바벨이 하는것처럼 해준다는것이죠.

그리고 이 옵션에 대해서 알아야 할것이 2가지 더 있습니다.

첫번째는, 이 옵션을 키면 자연스럽게 allowSyntheticDefaultImports옵션도 켜집니다.

그러면 이런 생각이 듭니다. Babel을 쓰면 필요 없는 옵션이 아닌가…? 맞습니다. 첫 문단에서 말했듯, Babel로 TS를 컴파일 하면 이 옵션은 필요 없습니다. 하지만, 너무 자주 보이는 옵션이여서 간단히 설명을 해본겁니다.

두번째는, 그럼에도 불구하고 TS로 컴파일 한다면 allowSyntheticDefaultImports 도 같이 직접 켜줘야 합니다.

이유는 모르겠지만 webpack에 이렇게 하라고 나와 있습니다.

jsx

typesript가 js를 다루는 방식을 지정합니다.

어차피 Babel로 컴파일 할것이기 때문에 preserve로 설정해 줍니다. 만약 typescript로 컴파일 한다면 react-jsx로 설정해줘야 합니다.

jsximportsource

jsx 코드를 어떻게 다룰것인지 지정할 수 있습니다. 예를들어서, emotion같은 경우에는 기존의 jsx에는 없는 css prop이 있습니다. 그리고 css prop을 받아서 처리한뒤에 className에 넣어줍니다. 그래서 이런 처리를 해주려면 기존의 방식으로 jsx를 처리하면 안되고, emotion에서 뭔가 커스텀하게 처리 해야합니다. 이럴때 그 커스텀 해석기(?)를 jsxImportSource에 지정하는것입니다.

그런데! 이것도 역시나 Babel을 사용하기 때문에 TS가 직접 컴파일 할일은 없기에 지워,,,,,,준다고 생각 할 수 있지만!!

그래도 필요합니다. VSC의 TS서버가 이거를 참고해서 css prop을 인식하기 때문입니다.

webpack

아까는 ts만 번들링 요소에 포함하도록 했는데, 이제는 tsx도 포함 시켜줍니다.

설정

webpack.common.js

module.exports = {
  mode: ...
  entry: join(__dirname, '../src/index.tsx'),
  devtool: ...
  output: {
    ...
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },
      {
        ...
      },
    ],
  },
  plugins: [
    ...
  ],
};

Babel

설치

npm install @babel/preset-react --save-dev

설정

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

ESlint & prettier

설치

npm install eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks --save-dev

설정

{
  "env": {
    "browser": true, // browser에서 사용하는 전역 기능(변수, 함수, 객체)를 인식한다
    "es2022": true // ECMAScript 2022와 그 하위에서 지원하는 기능들을 인식한다
  },
  "settings": {
    // plugin 세팅
    "react": {
      // eslint-plugin-react의 설정이다
      "version": "detect" // react version을 현재 프로젝트에서 사용하는 version을 알아서 찾아서 지정한다
    },
    "import/resolver": {
      // eslint-plugin-import의 설정이다
      "typescript": {
        // eslint-import-resolver-typescript 설정이다
        "project": "frontend/tsconfig.json" // tsconfig의 위치를 지정한다
      }
    }
  },
  "parser": "@typescript-eslint/parser", // parser는 ts parser를 활용한다
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true // jsx를 lint가 인식하도록 한다
    },
    "ecmaVersion": "latest",
    // 사실 위에서 env.es2022 = true로 했기 때문에, 알아서 최신 ecmaVersion을 사용한다
    // 지금은 필요 없는 옵션이다. 하지만 공부용으로 냅두자!
    "sourceType": "module"
    // ESLint의 Parser가 분석하려는 JS file이 script가 아닌 module이라는것을 명시한다
  },
  "extends": [
    "plugin:jsx-a11y/recommended", // 웹 접근성을 준수하여 jsx를 작성했는지 검사한다
    "plugin:react/recommended", // react관련 rule을 적용한다
    "plugin:react/jsx-runtime", // jsx관련 rule을 off시킨다
    "plugin:react-hooks/recommended", // react-hook관련 rule을 적용한다
    "plugin:import/recommended", // import관련 rule을 적용한다
    "plugin:import/typescript", // typescript import관련 rule을 적용한다
    "plugin:prettier/recommended", // .prettierrc.json에 기입한 prettier rule을 eslint rule로 적용한다
    "plugin:@typescript-eslint/recommended" // typescript관련 rule을 적용한다
  ],
  "rules": {
    "import/extensions": [
      "error", // 이 규칙을 지키지 않으면 error로 간주하겠다
      "ignorePackages", // pacakge빼고는 모두 import할때 확장자명을 작성 해라!
      {
        "ts": "never", // ts는 확장자명 사용하지 않는다
        "tsx": "never", // tsx도 절대!
        "js": "never", // js도!
        "jsx": "never" // jsx 마저도!
      }
    ],
    "react/prop-types": "off", // typescript를 사용 하니까 prop-types안써도 될듯 싶다
  }
}

emotion

설치

npm install @emotion/babel-plugin @emotion/react @emotion/styled  --save-dev

설정

.babelrc.json

{
  "presets": [
    ...
    [
      "@babel/preset-react",
      {
        "runtime": "automatic",  // jsx를 변환하는 플러그인들을 자동으로 포함시켜준다는 의이미다.
        "importSource": "@emotion/react"
        // jsx를 처리할때 emotion에서 제공하는 함수를 사용하라는 의미이다.
        // 이렇게 해야 jsx에 달려있는 css prop를 해석하고 className으로 변환할 수 있기 때문이다.
      }
    ],
    ...
  ],
  "plugins": [
    [
      "@emotion",
      {
        "sourceMap": true, // sourceMap을 생성한다
        "autoLabel": "dev-only", // label은 개발모드에서만 적용한다
        "labelFormat": "[local]",
        // const CardList = styled.ul 이런식으로 되어 있다면,
        // local은 CardList가 된다.
        "cssPropOptimization": true
        // @emotion/react의 jsx함수를 모든 jsx파일에서 사용한다는 전제가 있다면
        // true로 해준다. 그러면 뭔가 최적화를 해주는것 같다.
      }
    ]
  ]
}

절대경로 설정하기

절대경로는 정말 편하다! 코드도 깔끔해진다. 설정도 별로 안까다롭다.

tsconfig.json

{
  "compilerOptions": {
    ...
    "baseUrl": "./src",
    "paths": {
      "@components/*": ["components/*"],
      "@styles/*": ["styles/*"],
    },
    ...
  },
  ...
}

webpack.common.js

module.exports = {
  mode: ...,
  entry: ...,
  devtool: ...,
  output: {
    ...
  },
  module: {
    rules: [
      ...
    ],
  },
  plugins: [
    ...
  ],
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js'],
    alias: {
      '@components': resolve(__dirname, '../src/components'),
      '@styles': resolve(__dirname, '../src/styles'),
    },
  },
};

Storybook

설치

npx sb init --type react

이렇게 설치하다가 보면, storybook관련 eslint-plugin을 설치하라고 나옵니다. y를 눌러 설치해주고,

eslintrc.json파일에 아래와 같이 플러그인을 추가해줍니다.

"extends": [
  ...,
  "plugin:storybook/recommended",
  ...
]

설정

사실 설정도 어지간한건 sb init으로 다 해주지만, storybook용 webpack/babel/lint 설정을 또 따로 해줘야 합니다.

const { resolve } = require('path');

module.exports = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
  framework: '@storybook/react',
  core: {
    builder: 'webpack5', // webpack5를 사용한다
  },
  webpackFinal: async config => {
    // storybook이 작성한 webpack config객체를 받는다

    // 절대 경로를 설정한다
    config.resolve.alias = {
      ...config.resolve.alias,
      '@root': resolve(__dirname, '../'),
      '@src': resolve(__dirname, '../src/'),
      '@components': resolve(__dirname, '../src/components'),
      '@styles': resolve(__dirname, '../src/styles'),
      '@types': resolve(__dirname, '../src/types'),
      '@pages': resolve(__dirname, '../src/pages'),
      '@assets': resolve(__dirname, '../src/assets'),
      '@constants': resolve(__dirname, '../src/constants.ts'),
      '@api': resolve(__dirname, '../src/api'),
      '@context': resolve(__dirname, '../src/context'),
    };

    // babel설정을 추가한다
    config.module.rules[0].use[0].options.presets = [
      require.resolve('@babel/preset-env'),
      [
        require.resolve('@babel/preset-react'),
        {
          runtime: 'automatic',
          importSource: '@emotion/react',
        },
      ],
    ];

    // emotion babel plugin을 설정한다
    config.module.rules[0].use[0].options.plugins = [
      [
        require.resolve('@emotion/babel-plugin'),
        {
          sourceMap: true,
          autoLabel: 'dev-only',
          labelFormat: '[local]',
          cssPropOptimization: true,
        },
      ],
    ];

    return config;
  },
};

이상입니다. 긴 글 읽어주셔서 감사합니다.

Leave a Reply

Your email address will not be published.