이전 포스팅에서 PNPM, Vite, React, TypeScript, TailwindCss 를 설치 및 설정하였고, 이번 포스팅에서 이어서 진행해보겠다.
프론트 환경 설정(PNPM, Vite, React, TypeScript, TailwindCss)
프로젝트를 시작할 때 보통 스캐폴딩 도구(CLI)를 사용하거나 이전에 사용했던 설정 파일을 복사해서 쓰곤 했었는데, 모노레포 구조를 연습하는 과정에서 많은 어려움을 겪었고, 프로젝트 설정
miroong.tistory.com
5. eslint + prettier 설치 및 설정
코드 포맷팅을 도와주는 prettier 를 먼저 설정 하도록 하겠다. prettier 도 개발시에만 필요한 패키지이므로 -D 옵션을 주어 설치해준다.
prettier 설치 명령어
pnpm install -D prettier
{
"devDependencies": {
"prettier": "^3.7.4",
}
}
.prettierrc 라는 이름의 파일을 추가하여 코드 포맷팅에 필요한 설정을 작성해준다
{
"singleQuote": true,
"semi": true,
"tabWidth": 2,
"bracketSpacing": true,
"bracketLine": false,
"htmlWhitespaceSensitivity": "css",
"arrowParens": "avoid",
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"insertPragma": false,
"requirePragma": false,
"trailingComma": "all",
"useTabs": false,
"editor.formatOnSave": true,
"printWidth": 120,
"endOfLine": "auto"
}
(prettier 의 각 속성들은 이해하기에 크게 어렵지 않아 속성 하나하나에 대한 설명은 생략하였다.)
다음으로 IDE 에서 제공하는 기능인 파일 저장시 prettier 설정대로 자동 포맷팅 되도록 하는 부분을 설정한다.

보통 eslint 설정할 때 eslint-plugin-prettier 를 사용해서 prettier에 정의된 포맷팅 규칙에 어긋난 부분을 lint 오류로 출력할 수 있게 하는 방법을 사용하기도 하는데, eslint-plugin-prettier 를 사용하게 되면 eslint 가 돌아갈 때마다 매번 prettier 엔진을 다시 돌려서 코드를 검사하게 되기 때문에 프로젝트가 커질 수록 버벅거리는 현상이 생길 수 있고, 포맷팅 에러가 같이 나오게 되면서 정작 중요한 lint 에러를 놓칠 수 있기 때문에 이번엔 사용하지 않았다.
eslint 의 경우 v9 는 아직 호환성이 낮아 설정시 문제가 많기 때문에 v8 을 설치해줬다.
eslint v8 설치 명령어
pnpm install -D eslint@^8.57.1
아래는 eslint 설정과 관련하여 이미 설치 되어 있는 React, TypeScript, Prettier와 관련된 파서(parser)와 플러그인(plugin)들이다.
| 패키지 명 | 역할 |
| eslint-plugin-react | 리액트 문법 및 JSX 관련 규칙 제공 |
| eslint-plugin-react-hooks | 리액트 Hook 규칙 제공 |
| @typescript-eslint/parser | eslint가 TS 문법을 파싱 할 수 있도록 도와줌 |
| @typescript-eslint/eslint-plugin | 타입스크립트 전용 권장 규칙 제공 |
| eslint-import-resolver-typescript | 타입스크립트에서 import하는 모듈의 경로를 해석할 수 있도록 도와줌 |
| eslint-plugin-import | 타입스크립트의 import/export 문법을 위한 규칙 제공 |
| eslint-config-prettier | eslint가 코드 품질에만 집중 하고, 포맷팅 오류는 prettier 에게 넘길 수 있도록 둘 사이의 충돌 해결 |
파서 및 플러그인 설치 명령어
pnpm add -D eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-import-resolver-typescript eslint-plugin-import eslint-config-prettier
설치되어 package.json에 추가된 모습은 다음과 같다
{
...
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.53.0",
"@typescript-eslint/parser": "^8.53.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
}
}
다음으로 eslintrc.cjs 파일을 만들어 규칙을 추가한다.
module.exports = {
env: {
browser: true,
node: true,
es2022: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecma: 2022,
sourceType: 'module',
project: ['./tsconfig.json'],
ecmaFeatures: { jsx: true },
},
plugins: ['react', '@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'prettier',
],
settings: {
react: { version: 'detect' },
'import/resolver': {
typescript: { project: './tsconfig.json' },
node: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
},
},
rules: {
'no-var': 'error',
eqeqeq: ['error', 'always'],
'no-nested-ternary': 'error',
'no-console': 'warn',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
'@typescript-eslint/no-empty-function': 'off',
'react/react-in-jsx-scope': 'off',
'react/jsx-filename-extension': [1, { extensions: ['.tsx'] }],
'react/prop-types': 'off',
'import/no-default-export': 'error',
'import/prefer-default-export': 'off',
'react/function-component-definition': [
2,
{ namedComponents: 'arrow-function', unnamedComponents: 'arrow-function' },
],
'react/jsx-props-no-spreading': 'warn',
'react/require-default-props': 'warn',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/order': [
'error',
{
groups: ['builtin', 'external', ['internal', 'parent', 'sibling', 'index'], 'object', 'type'],
'newlines-between': 'always',
pathGroups: [{ pattern: 'react', group: 'external', position: 'before' }],
pathGroupsExcludedImportTypes: ['react'],
},
],
},
};
- eslintrc.cjs 코드 및 상세 설명
- parser / parserOtions
- 타입스크립트를 사용할 것이기 때문에 parser에 @typescript-eslint/parser 로 명시해준다
- parserOptions 에도 기본적인 내용을 추가해준다. 기준이 될 es 버전, 모듈 방식 등
- plugins / extends
- plugins 에 lint 검사 시에 참고할 개발 도구 목록을 정의한다. 실제로 해당 도구들에 대해서 어떤 lint 규칙을 사용할지는 extends 에 정의한다.
- prettier 를 보통 같이 추가하는데 현재 프로젝트에서는 prettier를 위에서 말했듯 자동 포맷팅으로만 처리할 예정이라 제외하였다.
- extends 에 prettier가 있는 이유는 eslint 에도 코드 포맷팅과 관련된 규칙들이 있는데 이를 설정해둔 prettier의 규칙으로 덮겠다는 의미이다.(prettier 와 충돌시 eslint의 규칙을 강제로 끔)
- settings
- 리액트 17버전부터 파일 상단에 import React from ‘react’를 쓰지 않아도 되기 때문에, react.version: ‘detect’ 옵션으로 eslint-plugin-react 가 리액트 버전을 알아서 찾도록 한다.
- typescript.project: ‘./tsconfig.json’ 은 eslint-plugin-import 플러그인을 위한 설정으로 경로 별칭을 사용하는 경우 tsconfig.json 파일을 참고하도록 추가해준다.
- node.extensions: […] 설정은 import 문에서 확장자를 생략하더라도 [ ] 배열 안에 있는 확장자 중에 일치하는 파일이 있는 경우 알아서 찾을 수 있도롭 해준다.
- rules (기타 세부 규칙들을 정의 하는 곳이며, 한번 더 기억해두면 좋을 규칙들만 설명을 남겨보았다.)
- @typescript-eslint/no-unused-vars
- 안 쓰는 변수를 에러로 처리하고, _로 시작시에만 허용함
- @typescript-eslint/no-explicit-any
- any 타입 사용 금지
- @typescript-eslint/consistent-type-imports
- 타입 import 시 type 강제 및 없는 경우 자동 추가해줌
- 타입과 객체 구분 및 컴파일 시 type 인 코드 모두 삭제하여 번들 사이즈 감소 및 실행 속도 최적화
- react/function-component-definition
- 리액트 컴포넌트 생성 시 화살표 함수 사용 강제
- @typescript-eslint/no-unused-vars
- parser / parserOtions
6. shadcn ui 설정
shadcn ui 사용시 직접 코드를 복사 붙여넣기 하는 방식으로 사용한다면 별도의 설정은 필요없지만, CLI 를 사용하는 경우에 약간의 설정이 필요하며 아래 명령어를 사용하면 필요한 초기 세팅이 완성된다.
shadcn ui 초기 세팅 명령어
pnpm dlx shadcn@latest init
명령어 입력시 base color 를 선택하라는 질문이 나오는데 이부분만 선택하고 나면 아래와 같은 작업들이 알아서 수행된다.
(나의 경우 ssl 인증 문제 때문에 export NODE_TLS_REJECT_UNAUTHORIZED=0 명령어 먼저 실행 후 진행하였다.)

components.json 파일 생성
components.json 파일은 shadcn/ui CLI 가 컴포넌트를 생성할 때 참고하는 설정 파일로, 폴더 구조를 변경하거나 테마 색상, 아이콘 라이브러리 교체, 프레임워크 마이그레이션 하는 상황에 수정하여 사용한다.
{
"$schema": "<https://ui.shadcn.com/schema.json>",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
- $schema : 이 파일이 shadcn의 설정을 따른다는 것을 IDE 에 알림. 자동완성을 도와주도록 함
- style : 테마 색상 선택
- rsc : React Server Components 사용 여부
- tsx : typescript 사용여부
- tailwind css 관련
- config : tailwind v3 을 사용하는 경우 또는 tailwind 설정 파일을 사용하는 경우 tailwind.config.js 파일 경로 명시
- css : shadcn이 사용하는 css 변수들이 저장되어 있는 파일 경로 명시
- baseColor : 사용할 기본 색상 명시
- cssVariables : true 로 하는 경우 index.css 에 정의해둔 변수 값을 사용하고, false인 경우 tailwind 클래스 형태로 코드를 생성하는데 이게 나중에 다크모드 구현시 true 로 해야 알아서 변경되기 때문에 true 권장
- prefix : tailwind 클래스 앞에 접두사 붙일지 여부 ex) tw-h-10
- iconLibrary : 사용할 아이콘 라이브러리 지정
- aliases : shadcn CLI 가 컴포넌트 생성할 때 참고해야 하는 각 폴더의 별칭으로 vite.config.ts 랑 tsconfig.json에 있는 alias 설정과는 다름
- regitries : 직접 만든 공통 UI 라이브러리를 shadcn 방식으로 배포하고 싶은 경우에 사용
class-variance-authority, clsx, tailwind-merge, lucide-react, tw-animate-css 의존성 추가
| class-variance-authority | shadcn ui 컴포넌트 사용시 각 컴포넌트의 ‘Large’, ‘Small’, ‘Primary’ 같이 다양한 버전을 선언적으로 관리하게 도와줌 |
| clsx | 여러 css 속성을 조건에 따라 적용될 수 있게 도와줌. clsx(’alert’, isOpen ? ‘…’ : ‘…’); |
| tailwind-merge | 중복된 tailwind 클래스를 알아서 정리해줌. 가장 나중에 추가된 속성만 남김 |
| lucide-react | shadcn 이 기본으로 사용하는 아이콘 모음 |
| tw-animate-css | tailwind 전용 애니메이션 클래스 제공 |
index.css 에 기본 스타일 속성 추가됨
@import 'tailwindcss';
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
(...생략)
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
(...생략)
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
(...생략)
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
./src/lib/utils.ts 파일 생성됨
해당 파일은 shadcn 컴포넌트의 class 속성을 추가하거나 수정해야하는 상황을 처리하는 로직이 담긴 파일로 clsx 랑 twMerge 사용해서 추가/변경된 class 속성을 알아서 병합 및 덮어씌기 함
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
이제 shadcn 컴포넌트를 추가해보겠다.
shadcn 의 Button 컴포넌트 추가 명령어
pnpm dlx shadcn@latest add button
명령어를 실행하고나면 아래와 같이 설치된 경로가 나온다.

해당 경로를 들어가서 확인해보면 Button 컴포넌트 파일이 추가된 걸 확인 할 수 있다.
App.tsx 에서 button 컴포넌트를 사용해보았다.
import '@/index.css';
import { Button } from '@/components/ui/button';
function App() {
return (
<div className="flex flex-col items-center justify-center gap-4 w-full h-50 bg-amber-200">
<p className="text-pink-500">Vite + React </p>
<Button>Default</Button>
<Button variant="outline">Outline</Button>
</div>
);
}
export default App;

7. MSW + Axios 추가 및 설정
api 통신을 위해 Axios 를 설치해주어야 하는데 이번 프로젝트에서는 실제 api 서버 대신 브라우저에서 요청을 가로채 서버인 척 응답을 주는 역할을 해줄 MSW를 사용할 예정이므로 MSW를 먼저 추가해주기로 한다.
msw 패키지 설치 및 초기 세팅 명령어
pnpm install -D msw
npx msw init public/ —save
init 명령어까지 진행하고 나면 public 폴더에 mockServiceWorker.js 파일이 생성되며 이 파일이 실질적으로 브라우저에서 네트워크 요청을 가로채는 역할을 한다.
mockServiceWorker.js 파일은 따로 수정할 부분은 없고, 추후 msw 버전이 변경되는 경우 다시 초기 세팅 명령어로 최신 버전으로 업데이트 해주기만 하면 된다.
다음으로 브라우저에서 요청을 가로챌 핸들러 로직이 정리된 handlers.ts 파일과 해당 로직대로 실제 브라우저에서 작업을 진행할 worker 를 정의하는 browser.ts 파일을 작성한다.
msw 관련 파일을 따로 모아두기 위해 ./src/mocks 폴더를 생성하고 이 안에 파일을 추가해준다.
// ./src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
const UserData = [
{ age: 23, name: 'userA' },
{ age: 17, name: 'userB' },
];
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json(UserData);
}),
];
/api/users 로 get 요청이 오는 경우 미리 만들어둔 mock 데이터인 UserInfo 배열을 사용해서 응답을 해주는 코드이다.
mock 데이터가 많아질 것을 대비해서 ./src/mocks/data 폴더를 생성하고 따로 .json 파일로 만들어 두는 것이 좋다. data 폴더로 옮긴 후 수정된 코드는 아래와 같다.
// ./src/mocks/data/User.json
[
{ "age": 23, "name": "userA" },
{ "age": 17, "name": "userB" }
]
// ./src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
import UserData from '@/mocks/data/User.json';
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json(UserData);
}),
];
다음으로 browser.ts 파일을 생성한 후 위에서 만든 핸들러를 추가해준다.
// ./src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from '@/mocks/handlers';
export const worker = setupWorker(...handlers);
msw 를 사용하는 경우 msw 의 worker 가 준비되기 전에 화면에서 먼저 api 요청을 하여 404 에러가 발생할 수 있다. 이를 방지하기 위해 worker 가 먼저 준비 된 후에 화면이 나올 수 있도록 아래 코드를 main.tsx 또는 index.tsx 에 추가해준다.
보통 리액트의 진입점 파일(entry point) 이름은 vite 로 생성한 경우 main.tsx, Next.js 로 생성한 경우 index.tsx 로 생성된다고 한다.
// ./src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
async function enableMocking() {
if (import.meta.env.MODE !== 'development') return;
const { worker } = await import('./mocks/browser');
return worker.start({ onUnhandledRequest: 'warn' });
}
enableMocking().then(() => {
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
});
이제 api 서버를 대신해줄 msw 가 준비되었으니 이 가짜 서버와 통신할 Axios 를 설치한다.
axios 설치 명령어
pnpm install axios
"dependencies": {
"axios": "^1.13.2",
}
다음으로 서버 통신에 필요한 기본 URL 정보, 헤더 등과 같은 정보를 매번 작성하지 않아도 되도록 axios 인스턴스를 생성해준다.
api 서버를 msw 로 대체하는 경우 baseURL 을 ‘localhost’ 로 하지 않고 ‘api’ 등 임의로 설정 해줘야 하는 것을 명심하자.
// ./src/aimport axios from 'axios';
import axios from 'axios';
const axiosApi = axios.create({
baseURL: '/api',
timeout: 3000,
headers: {
'Content-Type': 'application/json',
},
});
export { axiosApi };
화면에서 /users 로 요청을 보내보았다. 다행히 잘 나온다.
// ./src/App.tsx
import '@/index.css';
import { Button } from '@/components/ui/button';
import { axiosApi } from '@/api/axiosInstans';
import { useEffect, useState } from 'react';
interface UserInfo {
name: string;
age: number;
}
function App() {
const [userInfo, setUserInfo] = useState<UserInfo[]>([]);
useEffect(() => {
axiosApi.get(`/users`).then(res => {
console.log('res : ', res.data);
setUserInfo(res.data);
});
}, []);
return (
<div className="flex flex-col items-center justify-center gap-4 w-full h-fit bg-amber-200 py-10">
<p className="text-pink-500">Vite + React </p>
<Button>Default</Button>
<Button variant="outline">Outline</Button>
{userInfo.map(user => (
<div>
<p className="text-pink-500">
이름 : {user.name} / 나이 : {user.age}
</p>
</div>
))}
</div>
);
}
export default App;

프로젝트 크기에 맞춰 타입.ts 파일이나 api.ts 파일들을 따로 만들어 풀더 구조를 잡고 관리 해주는 것이 좋고, 아래에서 tanstack query 를 추가하면서 조금 구조를 변경해 볼 예정이다.
8. Tanstack Query 추가
실제 api 서버와 통신하는 경우 데이터를 신선하게 관리해주고, 중복 요청 방지 등을 도와주는 외부 라이브러리 Tanstack Query를 설치한다. 추가로 eslint 가 tanstack query 에 대한 규칙도 같이 검사 할 수 있도록 @tanstack/eslint-plugin-query 도 같이 설치해준다.
tanstack query 및 eslint 용 플러그인 설치 명령어
pnpm install @tanstack/react-query
pnpm i -D @tanstack/eslint-plugin-query
{
"dependencies": {
"@tanstack/react-query": "^5.90.17",
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.91.2",
}
}
이어서 eslint 설정 파일에 @tanstack/eslint-plugin-query 플러그인도 추가해준다. prettier 보다 상위에 위치할 수 있도록 추가해준다.
module.exports = {
...
extends: [
...
'plugin:@tanstack/eslint-plugin-query/recommended',
'prettier',
]
}
이제 main.tsx 에 리액트 앱에서 발생하는 모든 api 요청과 데이터를 관리하고 제어해 줄 queryClient 인스턴스를 생성하고, 이 인스턴스를 모든 컴포넌트에서 사용할 수 있도록 QueryClientProvider 컴포넌트를 추가해줘야 하는데 코드는 다음과 같다.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
async function enableMocking() {
if (import.meta.env.MODE !== 'development') return;
const { worker } = await import('./mocks/browser');
return worker.start({ onUnhandledRequest: 'warn' });
}
const queryClient = new QueryClient();
enableMocking().then(() => {
createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<StrictMode>
<App />
</StrictMode>
</QueryClientProvider>,
);
});
이제 기존에 작성해둔 api 요청 코드를 tanstack query 사용하는 버전으로 변경해보도록 하겠다.
우선 App.tsx 에 포함되어 있던 UserInfo 타입정의 부분과 api 요청 로직을 별도의 파일을 생성해서 분리 해주도록 한다.
// ./src/types/UserInfo.ts
export interface UserInfo {
name: string;
age: number;
}
// ./src/api/user.ts
import { axiosApi } from '@/api/axiosInstans';
import type { UserInfo } from '@/types/UserInfo';
export const fetchUsers = async (): Promise<UserInfo[]> => {
const { data } = await axiosApi.get<UserInfo[]>('/users');
return data;
};
그 다음 분리된 api 로직을 Tanstack Query의 useQuery 훅으로 감싸서 App.tsx 화면에서 사용할 수 있게 해준다.
// ./src/hooks/useUserInfo.ts
import { useQuery } from '@tanstack/react-query'
import { fetchUsers } from '@/api/user'
export const useUserInfo = () => {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
}
import '@/index.css';
import { Button } from '@/components/ui/button';
import { useUserInfo } from '@/hooks/useUserInfo';
function App() {
const { data: userInfo, isLoading } = useUserInfo();
return (
<div className="flex flex-col items-center justify-center gap-4 w-full h-fit bg-amber-200 py-10">
<p className="text-pink-500">Vite + React </p>
<Button>Default</Button>
<Button variant="outline">Outline</Button>
{!isLoading &&
userInfo?.map(user => (
<div key={user.id}>
<p className="text-pink-500">
이름 : {user.name} / 나이 : {user.age}
</p>
</div>
))}
</div>
);
}
export default App;
프로젝트 규모나 각 팀의 컨벤션에 따라 폴더 구조나 로직의 분리 수준은 달라질 수 있으며, 아키텍처에 대한 부분은 좀 더 경험을 쌓고 공부한 후에 남겨보도록 하겠다.
'Front' 카테고리의 다른 글
| 프론트 환경 설정(PNPM, Vite, React, TypeScript, TailwindCss) (1) | 2026.01.21 |
|---|