기본적인 React 프로젝트 세팅

목차

연습용, 혹은 소규모 프로젝트를 시작하려 할 때마다 비슷한 세팅들을 반복하게 된다. 대부분 가장 유명한 기술 스택들을 택하고 필요에 따라 몇 가지 라이브러리만 추가되기 때문이다.

그래서 React, Typescript, 코드 포매터, react-router-dom 정도를 기본적으로 사용하는 프로젝트를 시작할 때 필요한 것들을 정리했다. 그대로 따라하기만 하면 되도록 말이다.

1. 프로젝트 생성

React 공식 문서에서는 프레임워크를 사용해서 시작할 것을 권장하고 있지만 현재로서는 React만 사용하는 것이 가장 일반적이라고 생각한다.

create-react-app이 좋은 보일러플레이트를 제공하고 있었으나 유지보수가 안된다. 요즘은 React 프로젝트 시작에 Vite가 정배다.

그러니 React + Typescript 템플릿을 제공하는 Vite로 프로젝트를 생성한다. 경험상 패키지 매니저들 중 pnpm이 가장 속도도 빠르고 안정적이었기 때문에 pnpm을 사용한다(yarn berry도 좋지만 아직은 pnpm이 더 안정적인 것 같다).

pnpm create vite 프로젝트명 --template react-ts

2. 코드 포매터

ESLint + Prettier, 그리고 떠오르는 라이브러리 Biome가 포매팅을 위해 쓰일 수 있다.

2.1. ESLint + Prettier

포매팅을 위한 툴들을 설치한다. Prettier를 ESLint 규칙에 맞게 동작시켜 주는 라이브러리다. @typescript-eslint/parser@typescript-eslint/eslint-plugin는 기본적으로 설치되어 있었다.

pnpm add -D prettier eslint-config-prettier eslint-plugin-prettier

그럼 이제 eslint 플러그인들을 설치하자. 이전에 프로젝트를 할 때 사용했던 플러그인들을 집대성하고 eslint-config-airbnb를 더한 것이다. eslint-config-airbnbairbnb의 JS 스타일 가이드를 자동으로 적용해 준다.

pnpm add -D eslint-plugin-import eslint-plugin-react eslint-plugin-unused-imports eslint-config-airbnb eslint-plugin-jsx-a11y

그리고 다음과 같이 포매팅 규칙을 적은 .eslintrc.cjs를 작성한다. 이 룰은 이창희님의 블로그에 있는 lint 파일과 내가 개인적으로 개발하면서 썼던 airbnb 룰 몇 가지를 적절히 섞은 것이다.

// .eslintrc.cjs
module.exports = {
	root: true,
	env: { browser: true, es2020: true },
	extends: [
		"airbnb",
		"airbnb/hooks",
		"eslint:recommended",
		"plugin:@typescript-eslint/recommended",
		"plugin:@typescript-eslint/recommended-requiring-type-checking",
		"plugin:react/jsx-runtime",
		"plugin:react-hooks/recommended",
		"plugin:import/recommended",
	],
	ignorePatterns: ["dist", ".eslintrc.cjs", "vite.config.ts"],
	parser: "@typescript-eslint/parser",
	parserOptions: {
		ecmaVersion: "latest",
		sourceType: "module",
		project: true,
		tsconfigRootDir: __dirname,
	},
	plugins: ["react-refresh", "unused-imports"],
	rules: {
		"arrow-parens": ["warn", "as-needed"], // 화살표 함수의 파라미터가 하나일때 괄호 생략
		"comma-dangle": ["error", "always-multiline"],
		"consistent-return": "warn",
		"eol-last": ["error", "always"],
		indent: ["error", 2],
		"jsx-a11y/click-events-have-key-events": "off", // onClick 사용하기 위해서 onKeyUp,onKeyDown,onKeyPress 하나 이상 사용
		"jsx-quotes": ["error", "prefer-single"],
		"keyword-spacing": "error",
		"no-alert": ["off"], // alert를 쓰면 에러가 나던 규칙 해제
		"no-console": [
			"warn",
			{
				allow: ["warn", "error"],
			},
		],
		"no-duplicate-imports": "error",
		"no-extra-semi": "error",
		"no-param-reassign": "error",
		"no-shadow": "off",
		"no-trailing-spaces": "error",
		"object-curly-spacing": ["error", "always"],
		"padding-line-between-statements": [
			"error",
			{ blankLine: "always", prev: "*", next: "return" },
			{ blankLine: "always", prev: ["const", "let", "var"], next: "*" },
			{
				blankLine: "any",
				prev: ["const", "let", "var"],
				next: ["const", "let", "var"],
			},
		],
		"prefer-const": "error",
		"prefer-template": "error",
		quotes: [
			"error",
			"single",
			{
				avoidEscape: true,
			},
		],
		semi: "off",
		"space-before-blocks": "error",
		"unused-imports/no-unused-imports": "error",
		"import/extensions": "off",
		"import/no-named-as-default": "off",
		"import/no-unresolved": "off",
		"import/order": [
			"warn",
			{
				alphabetize: {
					order: "asc",
					caseInsensitive: true,
				},
				groups: [
					"builtin",
					"external",
					["parent", "internal"],
					"sibling",
					["unknown", "index", "object"],
				],
				pathGroups: [
					{
						pattern: "~/**",
						group: "internal",
					},
				],
				"newlines-between": "always",
			},
		],
		"import/prefer-default-export": "off",
		"react-hooks/exhaustive-deps": ["warn"], // hooks의 의존성배열이 충분하지 않을때 강제로 의존성을 추가하는 규칙을 완화
		"react/jsx-boolean-value": "off",
		"react/jsx-curly-brace-presence": [
			"error",
			{ props: "never", children: "never" },
		],
		"react/jsx-filename-extension": [
			"warn",
			{
				extensions: [".js", ".ts", ".jsx", ".tsx"], // 확장자로 js와 jsx ts tsx 허용
			},
		],
		"react/jsx-no-bind": "off",
		"react/jsx-no-useless-fragment": "warn",
		"react/jsx-props-no-spreading": "off",
		"react/no-array-index-key": "off",
		"react/no-unescaped-entities": "warn",
		"react/prop-types": "off",
		"react/require-default-props": "off",
		"react/self-closing-comp": "warn", // 셀프 클로징 태그 가능하면 적용
		"react-refresh/only-export-components": [
			"warn",
			{ allowConstantExport: true },
		],
		"@typescript-eslint/explicit-function-return-type": "off",
		"@typescript-eslint/no-explicit-any": "warn",
		"@typescript-eslint/no-misused-promises": [
			"error",
			{
				checksVoidReturn: false,
			},
		],
		"@typescript-eslint/no-non-null-assertion": "off",
		"@typescript-eslint/no-shadow": ["error"],
		"@typescript-eslint/no-unsafe-assignment": "warn",
		"@typescript-eslint/no-unused-vars": "error",
		"@typescript-eslint/semi": ["error"],
		"@typescript-eslint/type-annotation-spacing": [
			"error",
			{
				before: false,
				after: true,
				overrides: {
					colon: {
						before: false,
						after: true,
					},
					arrow: {
						before: true,
						after: true,
					},
				},
			},
		],
	},
};

그런데 이 상태에서는 제대로 자동 수정이 되지 않는다. tsconfig.json에 해당 eslint 파일이 포함되어 있지 않기 때문이라고 한다. 이를 수정하기 위해서는 다음과 같이 tsconfig.app.jsoninclude항목을 수정해 주어야 한다. 이 파일은 tsconfig.json에서 참조해서 컴파일 시에 사용한다.

{
  /* compilerOptions 생략 */
  "include": ["src", "vite.config.ts", ".eslintrc.cjs"],
}

그런데 이렇게 해도 eslint.json을 제대로 인식하지 못할 수 있다. 이는 Vite에서 tsconfig.app.json을 사용하게 되었기 때문이다. eslint의 parserOptions를 변경해서 해결할 수 있다.

// .eslintrc.cjs
module.exports = {
  // 생략
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module",
    // 이렇게 tsconfig의 경로를 명시해주면 된다.
    project: "./tsconfig.app.json",
    tsconfigRootDir: __dirname,
  },
  // 생략
}

그리고 prettier 설정 파일은 다음과 같이 만들어준다. 프로젝트 루트에 .prettierrc.json을 만들어 다음과 같이 작성한다.

{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 120,
  "arrowParens": "avoid"
}

3. 다른 코드 포매터 - Biome

3.1. 사용계기

다른 옵션으로 Biome라는 새로나온 lint 라이브러리도 사용할 수 있다.

2024년 7월 9일, 새로운 프로젝트를 세팅하고 있었다. 위에 적은 대로 하다가 eslint-plugin-unused-imports를 pnpm으로 설치하는데 다음과 같은 경고가 떴다.

Issues with peer dependencies found
.
└─┬ eslint-plugin-unused-imports 4.0.0
  ├── ✕ unmet peer @typescript-eslint/eslint-plugin@8: found 7.16.0
  └── ✕ unmet peer eslint@9: found 8.57.0

그래서 해당 레포지토리 이슈에 들어가 보니 플러그인 제작자가 Biome라는 라이브러리를 회사에서 쓰게 되었다며 해당 라이브러리를 사용하라고 권장하고 있었다. 더 이상 플러그인이 활발하게 유지보수되지 않을 것 같다는 말과 함께.

그래서 Biome를 사용해 보기로 했다. Biome는 eslint와 prettier를 한 번에 설정해주는 라이브러리다. 대략적인 역사는 Biome: 차세대 JS Linter와 Formatter에서 볼 수 있다.

그럼 공식 문서를 따라서 설치하자.

3.2. 설치와 적용

pnpm add --save-dev --save-exact @biomejs/biome

다음 명령으로 설정 파일을 생성하고 초기화한다.

pnpm biome init

그리고 vscode Biome 플러그인을 설치하자. 이 플러그인은 Biome 설정 파일을 읽어서 자동으로 코드를 수정해준다.

설정에서 default formatter를 Biome으로 설정하면 된다. 그런데 아직 Biome가 널리 퍼진 툴은 아니므로 다른 프로젝트에서는 여전히 ESLint + Prettier를 사용하고 있을 것이다. 따라서 vscode의 default formatter를 아예 Biome로 바꾸는 건 부담일 수 있다.

따라서 프로젝트 내부에 .vscode 폴더를 만들고 그 안에 settings.json을 만들어 다음과 같이 설정한다.

{
  "editor.defaultFormatter": "biomejs.biome"
}

앞선 eslint, prettier 설정 파일은 커맨드 명령으로 쉽게 Biome로 마이그레이션할 수 있다. 다음 명령을 실행하면 알아서 biome.json 파일에 eslint, prettier 설정이 마이그레이션되어서 들어간다.

pnpm biome migrate eslint --write
pnpm biome migrate prettier --write

이렇게 하면 위에서 했던 vite-tsconfig-paths와 같은 설정들, 여러 eslint 플러그인들, .eslintrc, .prettierrc 파일 등이 모두 필요없어진다. 전부 삭제하면 깔끔해진 package.json이 될 것이다.

Biome를 사용하는 후기는 이후에 다른 글로 작성할 예정이다.

4. 이외의 설정

4.1. import alias

먼저 vite.config.ts에서 tsconfig 파일의 path alias를 인식하도록 하기 위해 vite-tsconfig-paths를 설치한다.

pnpm add -D vite-tsconfig-paths

그리고 vite.config.ts를 다음과 같이 수정한다.

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
 
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths()],
});

import alias를 통해 @/로 시작하는 경로로 import해올 수 있도록 하자. tsconfig.app.jsoncompilerOptions에 다음과 같이 paths를 추가한다.

{
  "compilerOptions": {
    /* 생략 */
    "paths": {
      "@/*": ["./src/*"]
    }
  }
  /* 생략 */
}

4.2. react-router-dom

기본적인 라우팅을 위해서 react-router-dom을 설치하자.

pnpm add react-router-dom

그리고 src/main.tsx에 다음과 같이 기본적인 라우터를 설정한다.

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
  },
  {
    path: "/about",
    element: <div>about</div>,
  },
]);
 
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

이제 개발 환경을 실행하고 /about라우터에 들어가면 작게 about이라는 글씨가 뜨는 페이지가 나오는 것을 볼 수 있다. 라우팅이 잘 설정된 것이다.

4.3. vanilla extract

tailwind나 styled-components 같이 다른 CSS-in-JS 라이브러리를 사용할 때도 많다. 하지만 기본적인 방식은 모두 똑같으므로 @vanilla-extract/css를 설치할 수 있다.

pnpm add @vanilla-extract/css

vanilla extract 공식 문서의 Integration - Vite를 보고 따라하자. 먼저 플러그인을 설치한다.

pnpm add -D @vanilla-extract/vite-plugin

그리고 vite.config.ts에 다음과 같이 플러그인을 추가한다.

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
 
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import tsconfigPaths from 'vite-tsconfig-paths';
 
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths(), vanillaExtractPlugin()],
});

참고

eslint airbnb 사용하기 https://hayjo.tistory.com/111

shadcn ui 공식 문서 https://ui.shadcn.com/

Biome 공식 문서 https://biomejs.dev/