원활한 설명을 위해 중복되는 설정 코드는 제거했습니다. 전체 코드는 여기서 확인가능하니, 본 글은 이해의 용도로만 활용해 주시기 바랍니다!
Create-React-App없이 웹팩과 바벨 설정으로 리액트 개발 환경을 구성해보겠습니다. Tech Stack은 다음과 같습니다.
- Wepback
- Babel
- ESLint & Prettier
- Storybook
- React
- Typescript
- 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.json
의 paths
에 설정한 절대 경로를 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 };
};
저도 자세히는 모르겠지만, 그냥 __importStart
는 import * as React from 'react'
이 코드에서 react
모듈이 export하는 요소들을 React
객체에 담아서 return하는 것 같고
__importDefault
는 import 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;
},
};
이상입니다. 긴 글 읽어주셔서 감사합니다.