create-react-app(CRA)과 타입스크립트 템플릿의 조합은 현대 프론트엔드 개발의 시작을 알리는 가장 강력하고 편리한 도구임에 틀림없습니다. 단 한 줄의 명령어, npx create-react-app my-app --template typescript를 실행하는 순간, 우리 앞에는 복잡하기로 악명 높은 웹팩(Webpack)과 바벨(Babel)의 설정, 그리고 타입스크립트와의 유기적인 연동까지 완벽하게 구현된 개발 환경이 펼쳐집니다. 개발자는 이제 설정의늪에서 헤매는 대신, 오롯이 비즈니스 로직 구현과 사용자 경험(UX) 향상이라는 본질적인 가치에 집중할 수 있게 됩니다. 이것이 CRA가 우리에게 주는 가장 큰 선물입니다.
하지만 이 눈부신 편리함의 이면에는 '추상화'라는 거대한 장막이 존재합니다. CRA가 제공하는 기본 설정은 마치 잘 만들어진 '밀키트'와 같습니다. 누구나 쉽고 빠르게 괜찮은 결과물을 만들 수 있지만, 레스토랑의 메인 셰프가 되기에는 부족합니다. 이 기본 설정은 수많은 프로젝트를 아우르기 위한 '최소 공통 분모'일 뿐, 실제 현업에서 마주하게 될 복잡다단한 요구사항, 팀 고유의 개발 문화, 그리고 장기적인 확장성을 담아내기엔 그릇이 너무나도 작습니다.
처음 생성된 src 폴더는 교육용 샘플 코드로 어수선하고, 타입스크립트의 강력함을 100% 활용하기에 tsconfig.json은 너무 관대합니다. 여러 명의 개발자가 각자의 스타일로 코드를 작성할 때 발생할 혼란을 막아줄 코드 스타일 가이드라인은 전무하며, 프로젝트 구조가 조금만 깊어져도 우리는 어김없이 import Thing from '../../../components/common/Thing';과 같은 '상대 경로 지옥'에 빠져 허우적대게 됩니다. 이는 단순히 보기 싫은 코드를 넘어, 개발 생산성을 저하시키고 잠재적인 버그의 온상이 됩니다.
따라서 성공적인 프로덕트를 향한 여정의 첫걸음은, CRA가 마련해 준 안락한 요람에서 벗어나 우리의 손으로 직접 환경을 재구성하고 단단한 기반을 다지는 것에서부터 시작되어야 합니다. 이 글은 단순히 CRA를 사용하는 방법을 넘어, CRA 타입스크립트 프로젝트를 '튜토리얼' 수준에서 '프로덕션 레디(Production-Ready)' 상태로 격상시키기 위한 구체적이고 실전적인 전략과 철학을 심도 있게 공유하고자 합니다. 이제, 전문가 수준의 리액트 애플리케이션을 위한 견고한 토대를 함께 만들어 봅시다.
1. 견고한 아키텍처의 시작: 폴더 구조 재설계 및 초기 파일 정리
훌륭한 건축물이 정교한 설계도에서 시작되듯, 위대한 소프트웨어는 잘 설계된 아키텍처에서 탄생합니다. 개발에서 아키텍처의 가장 가시적인 표현은 바로 '폴더 구조'입니다. CRA가 생성해 준 초기 파일 구조는 리액트가 어떻게 동작하는지 보여주기 위한 최소한의 예제일 뿐, 수십, 수백 개의 컴포넌트와 페이지가 생겨날 실제 프로젝트의 청사진으로는 턱없이 부족합니다. 시작이 반이라는 말처럼, 코드를 한 줄이라도 더 작성하기 전에 불필요한 것들을 덜어내고 미래의 성장을 담을 수 있는 체계적인 구조를 갖추는 것이 무엇보다 중요합니다.
1.1. 불필요한 파일 과감히 제거하기: 미니멀리즘의 미학
프로젝트를 처음 생성했을 때 src 폴더 내부는 다음과 같은 파일들로 채워져 있습니다.
/src
├── App.css
├── App.test.tsx
├── App.tsx
├── index.css
├── index.tsx
├── logo.svg
├── react-app-env.d.ts
└── reportWebVitals.ts
└── setupTests.ts
이 파일들은 각자의 역할이 있지만, 우리만의 견고한 성을 쌓기 위해서는 먼저 땅을 고르는 작업이 필요합니다. 아래 파일들은 과감히 정리 대상입니다.
- logo.svg: 리액트의 상징적인 로고 파일입니다. 우리 서비스의 브랜딩과는 무관하므로 주저 없이 삭제합니다. 이는 단순한 파일 삭제를 넘어, "이제부터는 우리의 것을 만든다"는 상징적인 행위이기도 합니다.
- App.css, index.css: 전통적인 CSS 파일을 이용한 스타일링 예제입니다. 하지만 현대 리액트 생태계에서는 컴포넌트 기반 아키텍처의 장점을 극대화하기 위해 CSS-in-JS (예: Styled-components, Emotion)나 Utility-First CSS (예: Tailwind CSS)와 같은 고도화된 스타일링 솔루션을 도입하는 것이 일반적입니다. 이러한 솔루션은 스타일의 스코프를 컴포넌트 단위로 제한하여 전역 네임스페이스 충돌을 원천적으로 방지하고, 동적 스타일링을 쉽게 구현하게 해줍니다. 따라서 이 파일들은 새로운 스타일링 전략에 맞춰 삭제하거나, 모든 페이지에 일관되게 적용될 최소한의 전역 스타일(폰트 설정, CSS 리셋 등)을 담는 단일 파일(예: `styles/global.css`)로 대체하는 것이 바람직합니다.
- App.test.tsx: `App` 컴포넌트에 대한 샘플 테스트 코드입니다. 테스트 주도 개발(TDD)은 매우 중요한 개발 방법론이지만, 이 샘플 파일은 실제 비즈니스 로직과 전혀 무관합니다. 그대로 두면 오히려 실제 테스트 코드 작성을 방해하는 노이즈가 될 수 있습니다. 삭제 후, 앞으로 작성될 각각의 컴포넌트와 기능에 맞춰 의미 있는 테스트 코드를 직접 추가하는 것이 올바른 접근 방식입니다.
- setupTests.ts: Jest 테스트 환경을 위한 설정 파일입니다. 주로 `testing-library/jest-dom`을 불러와 DOM 관련 matcher(예: `toBeInTheDocument()`)를 확장하는 역할을 합니다. 초기 단계에서 커스텀 설정이 없다면 삭제 후, 테스트 라이브러리 설정이 복잡해지는 시점에 다시 생성해도 늦지 않습니다.
- reportWebVitals.ts: 구글이 제창한 웹 성능 핵심 지표(Core Web Vitals)를 측정하고 리포팅하는 유틸리티 함수입니다. LCP, FID, CLS와 같은 지표는 사용자 경험과 직결되므로 성능 최적화 단계에서 매우 중요합니다. 하지만, 기능 개발이 한창인 초기 단계에서는 필수적이지 않으며, 오히려 코드에 불필요한 복잡성을 더할 수 있습니다. 일단 삭제하고, 추후 성능 모니터링이 필요한 시점에 Google Analytics나 Vercel Analytics 같은 전문 분석 도구와 연동하여 체계적으로 관리하는 것을 권장합니다.
위 파일들을 모두 삭제했다면, 이들을 참조하고 있던 `App.tsx`와 `index.tsx`에서 관련 import 구문을 깨끗하게 정리해야 합니다.
// src/index.tsx (정리 후)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
// 만약 전역 스타일 파일을 사용하기로 했다면 여기서 import 합니다.
// 예: import './styles/global.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// src/App.tsx (정리 후)
import React from 'react';
function App() {
return (
<div>
<h1>Production-Ready React Project Boilerplate</h1>
</div>
);
}
export default App;
이제 src 폴더에는 `App.tsx`, `index.tsx`, 그리고 타입스크립트의 전역 타입 선언을 위한 `react-app-env.d.ts` 단 세 개의 파일만 남게 되었습니다. 비로소 우리만의 그림을 그릴 수 있는 깨끗한 캔버스가 준비된 셈입니다.
1.2. 확장성을 고려한 폴더 구조 설계: 미래를 위한 청사진
깨끗해진 src 폴더에 체계적인 구조를 도입할 차례입니다. 폴더 구조에는 절대적인 정답이 없으며 프로젝트의 성격과 팀의 합의에 따라 달라질 수 있습니다. 하지만 일반적으로 많이 사용되며 그 효율성이 검증된 '기능(역할) 기반' 폴더 구조는 다음과 같습니다. 이 구조는 파일의 역할과 책임을 명확히 분리하여, 프로젝트가 거대해지더라도 코드를 찾고 이해하는 인지적 비용을 크게 낮춰줍니다.
/src
├── apis/ # ✅ API 호출 함수 및 관련 타입 정의 (예: axios 인스턴스, react-query hooks)
├── assets/ # ✅ 이미지, 폰트, SVG 등 정적 에셋
│ ├── images/
│ ├── fonts/
│ └── icons/
├── components/ # ✅ 재사용 가능한 공용 컴포넌트
│ ├── common/ # ато믹(atomic) 단위의 범용 컴포넌트 (Button, Input, Modal, Spinner 등)
│ └── domain/ # 특정 도메인/페이지에 종속된 컴포넌트 (예: products/ProductCard, cart/CartItem)
├── constants/ # ✅ 변하지 않는 상수 값 (API 엔드포인트, 에러 메시지, UI 텍스트 등)
├── hooks/ # ✅ 재사용 가능한 커스텀 훅 (예: useDebounce, useLocalStorage)
├── layouts/ # ✅ 페이지의 공통적인 레이아웃 구조 (Header, Footer, Sidebar 포함)
├── pages/ # ✅ 라우팅의 단위가 되는 페이지 컴포넌트
├── stores/ # ✅ 전역 상태 관리 로직 (Recoil, Zustand, Jotai 등의 atoms, slices)
├── styles/ # ✅ 전역 스타일, 테마, mixin, reset.css 등
├── types/ # ✅ 프로젝트 전반에서 공유되는 공용 타입 정의 (예: User, Product)
├── utils/ # ✅ 순수 함수로 구성된 유틸리티 모음 (date, string, validation, formatters 등)
├── App.tsx # ✅ 최상위 컴포넌트, 라우터 설정, 전역 Provider 래핑
└── index.tsx # ✅ 애플리케이션의 진입점(Entry Point)
심화: 기능(Feature) 기반 폴더 구조와의 비교
위 구조의 대안으로 '기능(Feature) 기반' 구조도 있습니다. 이는 관련된 컴포넌트, 훅, API 호출 등을 하나의 폴더 안에 모으는 방식입니다. (예:
/src/features/authentication/components/LoginForm.tsx,/src/features/authentication/hooks/useAuth.ts). 이 방식은 기능의 응집도를 높여 코드 수정 시 여러 폴더를 오갈 필요가 없다는 장점이 있지만, 기능 간 의존성이 복잡해질 경우 구조가 더 어려워질 수 있습니다. 프로젝트의 복잡도와 팀의 선호도에 따라 적절한 방식을 선택하는 것이 중요합니다.
지금 당장 이 모든 폴더를 미리 만들어 둘 필요는 없습니다. 중요한 것은 이러한 청사진을 팀원들과 공유하고 개발을 시작하는 것입니다. 일관된 구조는 새로운 팀원이 프로젝트에 합류했을 때 온보딩 비용을 줄여주고, 코드 리뷰 시 불필요한 논쟁을 막아주며, 결과적으로 프로젝트 전체의 유지보수성을 극적으로 향상시킵니다.
2. 코드의 안정성을 극대화하는 `tsconfig.json` 고급 설정
tsconfig.json 파일은 타입스크립트 프로젝트의 '헌법'과도 같습니다. 이 파일은 타입스크립트 컴파일러(TSC)에게 우리 프로젝트의 코드를 어떻게 해석하고, 어떤 규칙으로 검사하며, 최종적으로 어떤 형태의 자바스크립트로 변환할지를 지시하는 지휘자 역할을 합니다. CRA가 제공하는 기본 `tsconfig.json`은 "strict": true 옵션을 포함하고 있어 꽤 훌륭한 출발점이지만, 여기서 멈춰서는 안 됩니다. 몇 가지 옵션을 추가하고 조정하는 것만으로도 잠재적인 런타임 에러를 컴파일 시점에 잡아내고, 개발 경험(DX)을 한 차원 높일 수 있습니다.
2.1. 절대 경로(Absolute Imports)와 경로 별칭(Path Aliases) 설정
프로젝트의 규모가 커지고 폴더의 깊이가 깊어질수록, 우리는 다음과 같은 끔찍한 import 구문을 마주하게 됩니다.
// src/pages/Products/Details/components/ReviewForm.tsx
// 악몽의 시작
import { Button } from '../../../../components/common/Button';
import { useCurrentUser } from '../../../../hooks/useCurrentUser';
import { postReview } from '../../../../apis/review';
이러한 상대 경로는 코드의 가독성을 심각하게 해칠 뿐만 아니라, 파일의 위치를 조금이라도 옮기면 관련된 모든 import 경로를 수동으로 수정해야 하는 유지보수의 재앙을 초래합니다. 이 문제를 해결하기 위해 `baseUrl`과 `paths` 옵션을 사용하여 깔끔한 절대 경로를 설정할 수 있습니다.
tsconfig.json 파일의 `compilerOptions` 객체에 다음 내용을 추가합니다.
{
"compilerOptions": {
// ... 기존 CRA 옵션 (target, lib, jsx, module 등) ...
"strict": true,
/* 절대 경로 설정 */
"baseUrl": "src",
"paths": {
"@/*": ["*"], // @/components/Button 등으로 사용할 수 있게 함
"@apis/*": ["apis/*"],
"@assets/*": ["assets/*"],
"@components/*": ["components/*"],
"@constants/*": ["constants/*"],
"@hooks/*": ["hooks/*"],
"@layouts/*": ["layouts/*"],
"@pages/*": ["pages/*"],
"@stores/*": ["stores/*"],
"@styles/*": ["styles/*"],
"@types/*": ["types/*"],
"@utils/*": ["utils/*"]
}
},
"include": ["src"]
}
"baseUrl": "src": 모든 모듈 경로 해석의 기준이 되는 디렉토리를src로 설정합니다. 이제 타입스크립트는 항상src폴더부터 모듈을 찾기 시작합니다."paths": `baseUrl`을 기준으로 경로에 대한 별칭(alias)을 지정합니다.@기호를 접두사로 사용하는 것은 다른 npm 패키지(예: `react`, `lodash`)와 이름이 충돌하는 것을 방지하기 위한 업계 표준 관례입니다."@components/*": ["components/*"]설정은 `@components/`로 시작하는 모든 경로는 `src/components/`로 해석하라는 의미입니다.
이 설정을 적용하면, 앞서 보았던 지저분한 코드가 다음과 같이 우아하게 바뀝니다.
// src/pages/Products/Details/components/ReviewForm.tsx (변경 후)
// 놀라운 가독성 향상
import { Button } from '@components/common/Button';
import { useCurrentUser } from '@hooks/useCurrentUser';
import { postReview } from '@apis/review';
이제 컴포넌트 파일의 위치가 어디로 이동하든 import 구문은 전혀 수정할 필요가 없습니다. 이는 코드의 유연성과 재사용성을 극대화하는 매우 중요한 설정입니다.
2.2. 더 엄격하고 명시적인 컴파일러 옵션으로 버그 원천 봉쇄
"strict": true는 사실 `noImplicitAny`, `strictNullChecks`, `strictFunctionTypes` 등 여러 유용한 규칙들의 집합체입니다. 여기에 몇 가지 옵션을 더 추가하면, 인간의 실수가 개입할 여지를 더욱 줄여 코드의 견고함을 한 차원 높일 수 있습니다. 마치 숙련된 코드 리뷰어가 24시간 내 옆에서 지켜보는 것과 같은 효과를 줍니다.
{
"compilerOptions": {
// ...
"strict": true,
/* === 추가적인 엄격함으로 잠재적 버그 방지 === */
// 파일 이름의 대소문자를 일관되게 사용하도록 강제
"forceConsistentCasingInFileNames": true,
// 사용되지 않는 지역 변수가 있으면 에러 발생
"noUnusedLocals": true,
// 사용되지 않는 함수 파라미터가 있으면 에러 발생
"noUnusedParameters": true,
// 함수 내 모든 코드 경로가 명시적으로 값을 반환하도록 강제
"noImplicitReturns": true,
// switch-case 문에서 fall-through(break 누락)가 발생하면 에러
"noFallthroughCasesInSwitch": true,
/* 모듈 관련 설정 강화 */
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true
// ...
}
}
-
forceConsistentCasingInFileNames: true: 개발 환경(macOS, Windows)의 파일 시스템은 대소문자를 구분하지 않지만, 배포 환경(Linux)은 구분합니다. 이로 인해MyComponent.tsx를import from './mycomponent'로 불러와도 로컬에서는 잘 동작하다가 빌드/배포 후 런타임 에러가 발생하는 끔찍한 상황이 발생할 수 있습니다. 이 옵션은 이러한 불일치를 컴파일 시점에 막아주는 생명줄과도 같은 설정입니다. -
noUnusedLocals: true/noUnusedParameters: true: 선언되었지만 사용되지 않는 변수나 함수 파라미터는 '죽은 코드(dead code)'일 가능성이 높습니다. 이는 코드의 가독성을 해치고 리팩토링 시 혼란을 야기합니다. 이 옵션들은 이러한 코드를 에러로 표시하여 항상 깨끗한 코드를 유지하도록 돕습니다. 만약 의도적으로 파라미터를 사용하지 않는 경우(예: `array.map((_, index) => ...)`), 변수명 앞에 언더스코어(_)를 붙여 타입스크립트에게 의도를 명확히 알려줄 수 있습니다. -
noImplicitReturns: true: 함수가 어떤 조건에서는 값을 반환하지만 다른 조건에서는 반환하지 않는 실수를 막아줍니다. 예를 들어, `if (condition) { return true; }`만 있고 `else` 블록에 반환문이 없다면 이 함수는 `true` 또는 `undefined`를 반환할 수 있게 됩니다. 이 옵션은 모든 코드 경로가 명시적으로 값을 반환하도록 강제하여 예측 불가능성을 제거합니다.
이러한 엄격한 규칙들은 처음에는 다소 귀찮게 느껴질 수 있지만, 장기적으로는 디버깅에 소요되는 막대한 시간을 절약해주고 코드의 신뢰도를 비약적으로 향상시키는 가장 효과적인 투자입니다.
3. 개발 생산성 극대화: ESLint, Prettier, 그리고 Husky 연동
혼자가 아닌 팀으로 일할 때, '일관성'은 단순한 미덕이 아니라 생산성과 직결되는 필수 요건입니다. 들여쓰기는 탭(tab)을 쓸 것인가 스페이스(space)를 쓸 것인가, 문자열은 홑따옴표인가 쌍따옴표인가와 같은 사소한 논쟁으로 코드 리뷰 시간을 낭비해서는 안 됩니다. 또한, 잠재적인 버그를 유발할 수 있는 코드 패턴을 사람의 눈에만 의존하여 찾아내는 것은 비효율적이고 실수가 잦습니다. 이 모든 문제를 자동화하여 해결하고 개발자가 오직 로직에만 집중하게 만드는 삼위일체가 바로 ESLint, Prettier, 그리고 Husky입니다.
3.1. ESLint와 Prettier 연동: 역할 분담의 미학
ESLint와 Prettier는 종종 혼동되지만, 그 역할은 명확히 다릅니다. 이 둘의 관계를 정확히 이해하고 역할을 분담시키는 것이 핵심입니다.
- ESLint (The Code Quality Inspector): 코드의 품질을 검사하는 린터(Linter)입니다. 사용되지 않는 변수, 무한 루프를 유발할 수 있는 조건문, 리액트 훅의 잘못된 사용법 등 논리적인 오류나 안티 패턴을 찾아내 경고하거나 에러를 발생시킵니다.
- Prettier (The Code Style Enforcer): 코드의 스타일을 강제하는 포맷터(Formatter)입니다. 들여쓰기, 줄 바꿈, 쉼표 스타일, 괄호 안 공백 등 순전히 미적인 부분, 즉 코드의 '생김새'를 통일시키는 역할을 합니다.
문제는 ESLint의 규칙 중에도 스타일과 관련된 것들이 있어 Prettier와 충돌할 수 있다는 점입니다. 따라서 우리의 전략은 '코드 품질은 ESLint에게, 코드 스타일은 Prettier에게' 완전히 위임하는 것입니다.
1. 필요 패키지 설치
이 역할 분담을 위해 필요한 라이브러리들을 개발 의존성(devDependencies)으로 설치합니다.
# npm 사용 시
npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier
# yarn 사용 시
yarn add -D prettier eslint-config-prettier eslint-plugin-prettier
prettier: Prettier의 핵심 라이브러리.eslint-config-prettier: Prettier와 충돌 가능성이 있는 모든 ESLint의 '스타일 관련' 규칙들을 비활성화 시켜줍니다. (예: `indent`, `quotes` 규칙 등)eslint-plugin-prettier: Prettier의 포맷팅 규칙을 ESLint 규칙의 일부로 실행시켜 줍니다. 즉, Prettier 스타일 가이드에 맞지 않는 코드가 발견되면, ESLint가 이를 하나의 '린트 에러'로 보고하게 만들어 줍니다.
2. 설정 파일 구성
프로젝트의 루트 디렉토리에 각 도구의 설정 파일을 명시적으로 생성하여 관리합니다. 이는 package.json 내부에 설정하는 것보다 훨씬 유연하고 확장성이 좋습니다.
.prettierrc.js: Prettier의 스타일 규칙을 정의하는 파일입니다. JSON 대신 JS 파일을 사용하면 주석을 추가하거나 필요에 따라 동적인 로직을 넣을 수 있어 편리합니다.
// .prettierrc.js
module.exports = {
printWidth: 100, // 한 줄의 최대 길이를 100자로 제한합니다.
tabWidth: 2, // 탭 너비는 2칸으로 설정합니다.
useTabs: false, // 탭 대신 스페이스를 사용합니다.
semi: true, // 모든 문장의 끝에 세미콜론을 붙입니다.
singleQuote: true, // 문자열은 홑따옴표(')를 사용합니다.
trailingComma: 'all', // 객체나 배열 등 마지막 요소 뒤에도 항상 쉼표를 붙입니다. (Git Diff 등에서 유리)
arrowParens: 'always', // 화살표 함수에서 파라미터가 하나일 때도 괄호를 항상 사용합니다. (예: (x) => x)
bracketSpacing: true, // 객체 리터럴에서 괄호 양쪽에 공백을 추가합니다. { foo: bar }
jsxSingleQuote: false, // JSX 속성에는 홑따옴표를 사용하지 않습니다. (예: <div name="main" />)
endOfLine: 'auto', // OS에 따른 EOL(줄바꿈 문자) 문제를 자동으로 해결합니다.
};
.eslintrc.js: ESLint의 동작을 설정하는 파일입니다. CRA는 기본적으로 package.json에 `eslintConfig`를 생성하지만, 이를 별도 파일로 분리하여 관리하는 것이 좋습니다. 기존 `eslintConfig` 내용은 이 파일로 옮겨오고 확장합니다.
// .eslintrc.js
module.exports = {
root: true, // 이 설정 파일이 있는 디렉토리를 ESLint의 루트로 간주합니다. 상위 폴더의 설정 파일을 찾지 않습니다.
env: {
browser: true, // 브라우저 환경의 전역 변수 (window, document 등)를 인식합니다.
es2021: true, // ES2021 문법을 사용합니다.
},
extends: [
'eslint:recommended', // ESLint에서 권장하는 기본 규칙
'plugin:react/recommended', // React 프로젝트를 위한 추천 규칙
'plugin:react-hooks/recommended', // React Hooks 사용에 대한 추천 규칙
'plugin:@typescript-eslint/recommended', // TypeScript 프로젝트를 위한 추천 규칙
'plugin:prettier/recommended', // **가장 중요!** eslint-plugin-prettier와 eslint-config-prettier를 활성화. 반드시 배열의 마지막에 위치해야 합니다.
],
parser: '@typescript-eslint/parser', // ESLint가 TypeScript 코드를 파싱할 수 있도록 파서를 설정합니다.
parserOptions: {
ecmaFeatures: {
jsx: true, // JSX 파싱을 활성화합니다.
},
ecmaVersion: 'latest', // 최신 ECMAScript 버전을 지원합니다.
sourceType: 'module', // ES 모듈 시스템을 사용합니다.
},
plugins: [
'react',
'react-hooks',
'@typescript-eslint'
],
settings: {
react: {
version: 'detect', // 설치된 React 버전을 자동으로 감지합니다.
},
},
rules: {
// 여기에 개별적으로 재정의하거나 추가할 규칙을 작성합니다.
'prettier/prettier': 'error', // Prettier 규칙을 위반하는 경우, ESLint가 이를 에러로 처리합니다.
'react/react-in-jsx-scope': 'off', // React 17 이상에서는 import React from 'react'가 없어도 되므로 끕니다.
'react/prop-types': 'off', // TypeScript를 사용하므로 prop-types는 필요 없습니다.
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], // 사용하지 않는 변수는 경고 처리하되, '_'로 시작하는 변수는 무시합니다.
'@typescript-eslint/explicit-module-boundary-types': 'off', // export되는 함수의 반환 타입을 명시적으로 선언하도록 강제하는 규칙을 끕니다 (취향에 따라 'on'으로 설정 가능).
},
};
.eslintrc.js 파일의 extends 배열에서 'plugin:prettier/recommended'는 반드시 가장 마지막에 와야 합니다. 이렇게 해야 이전에 설정된 다른 규칙들(예: `eslint:recommended`)과 충돌하는 스타일 관련 규칙들을 Prettier의 규칙으로 성공적으로 덮어쓸 수 있습니다.
3.2. Husky와 lint-staged로 커밋 전 코드 품질 자동 검사
위대한 설정을 마쳤다 하더라도, 개발자가 커밋하기 전에 린트나 포맷 명령을 실행하는 것을 잊는다면 무용지물입니다. 사람은 실수를 하기 마련이므로, 이 과정을 시스템으로 자동화하고 강제해야 합니다. `husky`는 Git hooks를 쉽게 관리하게 해주는 도구이며, `lint-staged`는 Git의 스테이징 영역에 올라간 파일에 대해서만 특정 명령을 실행하게 해주는 효율적인 도구입니다. 이 둘을 조합하면 "품질 기준을 통과하지 못한 코드는 절대로 Git 저장소에 커밋될 수 없도록" 하는 강력한 '품질 게이트(Quality Gate)'를 구축할 수 있습니다.
1. 설치 및 초기 설정
# husky와 lint-staged 설치
npx husky-init && npm install
npm install --save-dev lint-staged
# 또는 yarn 사용 시
npx husky-init && yarn
yarn add -D lint-staged
npx husky-init 명령은 두 가지 중요한 일을 합니다. 첫째, Git hooks를 관리하기 위한 .husky 디렉토리를 생성합니다. 둘째, package.json에 prepare 스크립트를 추가합니다. 이 `prepare` 스크립트는 npm install (또는 `yarn`) 실행 후 항상 호출되어, 다른 팀원이 이 프로젝트를 클론받아 의존성을 설치할 때마다 자동으로 husky가 Git hook을 설정하도록 보장해줍니다.
2. `pre-commit` 훅 설정
`pre-commit` 훅은 개발자가 git commit 명령을 실행할 때, 실제 커밋이 생성되기 '직전에' 실행되는 스크립트입니다. 우리는 이 시점에 `lint-staged`를 실행하여 코드를 검사하고 싶습니다. .husky/pre-commit 파일을 열어 다음과 같이 수정합니다.
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# lint-staged를 실행합니다.
# 여기서 0이 아닌 종료 코드로 실패하면 커밋은 취소됩니다.
npx lint-staged
3. `lint-staged` 설정
이제 `lint-staged`가 어떤 파일에 대해 어떤 명령을 실행할지 정의해야 합니다. 이 설정은 `package.json` 파일에 추가하는 것이 일반적입니다. `lint-staged`의 가장 큰 장점은 전체 프로젝트가 아닌, 현재 커밋에 포함될 파일(정확히는 스테이징된 파일)에 대해서만 린터와 포맷터를 실행하므로 매우 빠르고 효율적이라는 점입니다.
// package.json
{
// ... (name, version, dependencies 등)
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,css,scss,md}": [
"prettier --write"
]
}
}
이 설정의 의미는 다음과 같습니다.
- 스테이징된 파일 중 확장자가
.js,.jsx,.ts,.tsx인 파일에 대해,eslint --fix를 먼저 실행하여 자동으로 수정 가능한 문제를 해결하고, 그 다음prettier --write를 실행하여 코드를 포맷팅합니다. .json,.css,.md등 다른 파일에 대해서는 Prettier 포맷팅만 적용합니다.
이제 모든 설정이 끝났습니다. 개발자가 코드를 수정하고 git add . 후 git commit -m "..."를 시도하면, 보이지 않는 곳에서 다음과 같은 과정이 자동으로 일어납니다.
- Husky가 `pre-commit` 훅을 트리거합니다.
lint-staged가 실행되어 스테이징된 파일들을 대상으로 설정된 명령(ESLint, Prettier)을 실행합니다.- 자동 수정이 가능한 모든 문제가 해결되고 코드가 포맷팅됩니다.
- 만약 자동으로 해결할 수 없는 ESLint 에러(예: 심각한 논리 오류)가 남아있다면, `lint-staged`는 0이 아닌 종료 코드로 실패하고, 커밋 자체가 자동으로 취소됩니다.
- 모든 검사를 통과해야만 비로소 커밋이 성공적으로 생성됩니다.
이로써 우리 팀의 Git 저장소에는 항상 일관된 스타일과 최소한의 품질 기준을 통과한 코드만이 기록된다는 것을 시스템적으로 보장할 수 있게 되었습니다.
4. `eject` 없이 CRA 설정 커스터마이징: Craco 도입
CRA의 철학은 '설정의 단순화'입니다. 이를 위해 Webpack, Babel, PostCSS 등 복잡한 빌드 도구들의 설정을 내부로 감추고 개발자가 신경 쓰지 않도록 만듭니다. 하지만 프로젝트가 성장함에 따라 우리는 이 '감춰진 설정'을 수정하고 싶어지는 순간을 반드시 마주하게 됩니다. 예를 들어, 앞서 설정한 경로 별칭을 Webpack에 알려주거나, 특정 Babel 플러그인을 추가해야 하는 경우입니다.
CRA가 공식적으로 제공하는 유일한 방법은 npm run eject입니다. 하지만 이 명령을 실행하는 것은 '판도라의 상자'를 여는 것과 같습니다. eject는 CRA가 숨겨왔던 모든 복잡한 설정 파일들(수백 줄의 Webpack 설정 등)을 우리 프로젝트의 scripts와 config 폴더로 꺼내놓습니다. 이 순간부터 우리는 더 이상 CRA의 편리한 버전 업데이트(예: react-scripts 개선)를 지원받을 수 없으며, 이 모든 설정 파일의 유지보수 책임을 직접 떠안아야 합니다. 이는 엄청난 부담이며, 대부분의 경우 피해야 할 선택입니다.
이 딜레마를 우아하게 해결해주는 도구가 바로 Craco (Create React App Configuration Override)입니다. Craco는 'CRA 설정 덮어쓰기'라는 이름처럼, eject 없이도 프로젝트 루트에 있는 단 하나의 간단한 설정 파일(craco.config.js)을 통해 Webpack, Babel, ESLint 등의 설정을 덮어쓰거나 확장할 수 있게 해줍니다.
1. 설치 및 `package.json` 스크립트 수정
# npm 사용 시
npm install @craco/craco
# yarn 사용 시
yarn add @craco/craco
설치가 완료되면, package.json의 `scripts` 부분을 `react-scripts` 대신 `craco`를 사용하도록 수정해야 합니다. Craco CLI는 내부적으로 `react-scripts`를 실행하되, 실행 전에 우리가 정의한 커스텀 설정을 주입하는 역할을 합니다.
// package.json
"scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
+ "start": "craco start",
+ "build": "craco build",
+ "test": "craco test",
"eject": "react-scripts eject" // eject는 만약을 위해 그대로 둡니다.
},
2. `craco.config.js` 파일 생성 및 경로 별칭 연동
이제 프로젝트 루트에 craco.config.js 파일을 생성합니다. 이곳에서 모든 마법이 일어납니다. 가장 시급한 과제는 앞서 tsconfig.json에서 설정했던 경로 별칭(Path Aliases)을 Webpack이 실제로 알아들을 수 있도록 설정하는 것입니다. 이 작업을 매우 쉽게 만들어주는 `craco-alias` 플러그인을 추가로 설치합니다.
npm install --save-dev craco-alias
이제 Craco 설정 파일에 플러그인을 적용합니다.
// craco.config.js
const CracoAlias = require('craco-alias');
module.exports = {
plugins: [
{
plugin: CracoAlias,
options: {
// 경로 별칭의 소스를 어디서 가져올지 설정합니다.
source: 'tsconfig',
// tsconfig.json 파일의 경로를 지정합니다.
// baseUrl과 paths를 자동으로 읽어와 Webpack의 resolve.alias에 설정해줍니다.
tsConfigPath: 'tsconfig.paths.json',
},
},
],
// Babel 설정을 커스터마이징할 수 있습니다.
babel: {
presets: [],
plugins: [
// 예: 감성적인 CSS-in-JS 라이브러리를 위한 설정
// 'babel-plugin-styled-components',
],
},
// Webpack 설정을 직접 커스터마이징합니다.
webpack: {
alias: {
// 플러그인을 사용하지 않고 직접 설정할 수도 있습니다.
},
configure: (webpackConfig, { env, paths }) => {
// 웹팩 설정을 직접 수정하는 강력한 기능입니다.
// 예: 프로덕션 빌드에서만 console.log를 제거하는 플러그인 추가
// if (env === 'production') {
// webpackConfig.optimization.minimizer.push(new TerserPlugin({ ... }));
// }
return webpackConfig;
},
},
};
여기서 한 가지 더 깔끔한 구성을 위해, 타입스크립트의 컴파일 관련 옵션과 경로 별칭 옵션을 분리하여 관리하는 것이 좋습니다. `tsconfig.json`에서 `paths` 부분을 `tsconfig.paths.json`이라는 별도의 파일로 분리하고, 원래 `tsconfig.json`에서는 이 파일을 확장(extends)하도록 설정합니다. 이렇게 하면 설정의 책임이 명확해집니다.
tsconfig.paths.json (경로 별칭 전용)
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@apis/*": ["apis/*"],
"@assets/*": ["assets/*"],
"@components/*": ["components/*"],
"@constants/*": ["constants/*"],
"@hooks/*": ["hooks/*"],
"@layouts/*": ["layouts/*"],
"@pages/*": ["pages/*"],
"@stores/*": ["stores/*"],
"@styles/*": ["styles/*"],
"@types/*": ["types/*"],
"@utils/*": ["utils/*"]
}
}
}
tsconfig.json (메인 설정 파일)
{
"extends": "./tsconfig.paths.json", // paths 설정 파일을 여기서 불러옵니다.
"compilerOptions": {
// ... target, lib, jsx, strict 등 기존 컴파일 옵션 ...
// baseUrl과 paths는 이제 extends를 통해 상속받으므로 여기서 제거해도 됩니다.
},
"include": ["src"],
"exclude": ["node_modules"]
}
이제 npm start (내부적으로는 `craco start`)를 실행하면, Craco가 `tsconfig.paths.json` 파일을 읽어 Webpack 설정에 주입해주므로, `@components` 같은 절대 경로가 타입스크립트 컴파일러와 웹팩 번들러 양쪽에서 모두 완벽하게 동작하게 됩니다. 이처럼 Craco를 사용하면 eject의 위험 부담 없이도 Babel에 매크로 플러그인을 추가하거나, Webpack의 로더 설정을 미세 조정하고, 빌드 최적화 플러그인을 추가하는 등 사실상 거의 모든 수준의 커스터마이징이 가능해집니다. 이는 프로젝트의 확장성과 유연성을 보장하는 매우 강력한 무기입니다.
5. 결론: 전문가 수준의 프로젝트를 위한 탄탄한 초석
Create React App은 의심할 여지 없이 리액트 생태계로 들어서는 가장 빠르고 편안한 길을 열어주었습니다. 하지만 그 길의 끝이 프로덕션이라는 정상은 아닙니다. CRA는 훌륭한 '베이스캠프'를 제공할 뿐, 정상까지 오르기 위해서는 우리 스스로 장비를 점검하고, 루트를 계획하며, 팀원들과 규율을 정하는 과정이 반드시 필요합니다. 실무 레벨의 견고하고 확장 가능한 애플리케이션을 구축하기 위해서는, CRA가 제공하는 추상화된 편리함 너머에 있는 설정들을 우리 프로젝트의 맥락에 맞게 다듬고 강화하는 과정이 필수적입니다.
우리는 이 글을 통해 튜토리얼 수준의 CRA 프로젝트를 프로덕션 레벨로 격상시키기 위한 네 가지 핵심 기둥을 세웠습니다.
- 체계적인 아키텍처 설계: 불필요한 파일을 제거하고, 확장성을 고려한 명확한 폴더 구조를 설계하여 수백 개의 파일이 생겨나도 혼란스럽지 않을 유지보수성의 기틀을 마련했습니다.
-
코드 안정성 확보: 절대 경로와 경로 별칭을 도입하여 개발 경험을 극적으로 향상시켰고, 더욱 엄격한
tsconfig.json컴파일러 옵션을 통해 잠재적인 런타임 버그를 컴파일 시점에 원천 봉쇄하여 코드의 신뢰도를 비약적으로 높였습니다. - 품질 자동화 시스템 구축: ESLint(품질)와 Prettier(스타일)의 역할을 명확히 분리하고, Husky와 lint-staged를 통해 이를 자동화함으로써, 사람의 실수에 의존하지 않고 팀 전체의 코드 품질을 일관되게 유지하는 강력한 '품질 게이트'를 구축했습니다.
-
설정의 유연성 확보: Craco를 도입하여
eject라는 돌아올 수 없는 강을 건너지 않고도 Webpack과 Babel 설정을 자유롭게 확장할 수 있는 길을 열어, 미래의 어떤 기술적 요구사항에도 유연하게 대처할 수 있는 능력을 갖추었습니다.
이러한 과정들은 프로젝트 초기에 투입해야 하는 분명한 '비용'입니다. 처음에는 다소 번거롭고 복잡하게 느껴질 수도 있습니다. 하지만 이는 프로젝트가 성장하면서 필연적으로 마주하게 될 '기술 부채'라는 눈덩이에 대한 선제적인 투자와도 같습니다. 초기에 잘 닦아놓은 개발 환경이라는 고속도로는, 프로젝트가 복잡해지고 팀원이 늘어나는 미래에 마주할 수많은 장애물과 교통체증을 피해 목적지까지 빠르고 안전하게 도달할 수 있도록 안내하는 최고의 내비게이션이 될 것입니다.
이 글에서 제시된 전략들을 바탕으로 여러분만의, 혹은 여러분 팀만의 '골든 스탠다드' 프로젝트 템플릿을 만들어보시길 바랍니다. 새로운 프로젝트를 시작할 때마다 이 템플릿에서 출발한다면, 매번 반복되는 설정의 고통에서 벗어나 첫날부터 가치 있는 비즈니스 로직 구현에 집중할 수 있을 것입니다. 그것이야말로 평범한 '프로젝트'와 성공적인 '프로덕트'의 차이를 만드는 전문가의 첫걸음입니다.
Post a Comment