create-react-app
(이하 CRA)은 의심할 여지 없이 리액트 생태계의 문을 여는 가장 쉽고 빠른 열쇠입니다. 특히 타입스크립트 템플릿(npx create-react-app my-app --template typescript
)은 단 한 줄의 명령어로 복잡한 웹팩(Webpack)과 바벨(Babel) 설정, 타입스크립트 연동까지 마법처럼 해결해 줍니다. 개발자는 비로소 비즈니스 로직과 UI 구현이라는 본질에 집중할 수 있게 됩니다. 하지만 이 편리함은 '보편성'이라는 이름 아래 많은 것을 추상화하고 숨깁니다. CRA가 제공하는 기본 설정은 모든 사용자를 위한 최소한의 공통분모일 뿐, 실제 프로덕션 환경에서 마주할 복잡성과 팀의 고유한 개발 문화를 담아내기에는 부족한 점이 많습니다.
처음 생성된 프로젝트의 src
폴더는 샘플 코드로 가득 차 있고, tsconfig.json
은 더 엄격하게 관리될 여지가 많습니다. 여러 개발자가 함께 코드를 작성할 때 일관성을 유지해 줄 코드 스타일 가이드라인은 부재하며, 프로젝트가 복잡해질수록 상대 경로 지옥(../../../
)에 빠지기 십상입니다. 이는 단순히 불편함을 넘어 생산성 저하와 잠재적 버그의 원인이 됩니다. 따라서 성공적인 프로젝트의 첫 단추는 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)와 같은 고도화된 스타일링 솔루션을 도입하는 것이 일반적이므로, 이 파일들은 새로운 전략에 맞춰 삭제하거나 전역 스타일을 위한 최소한의 파일(예: `styles/global.css`)로 대체됩니다.
- App.test.tsx: `App` 컴포넌트에 대한 샘플 테스트 코드입니다. 테스트는 매우 중요하지만, 이 샘플 파일은 실제 비즈니스 로직과 무관하므로 삭제하고, 앞으로 작성될 컴포넌트에 맞춰 필요한 테스트 코드를 직접 추가하는 것이 좋습니다.
- setupTests.ts: Jest 테스트 환경을 위한 설정 파일입니다. `testing-library/jest-dom`을 불러오는 역할 등을 하지만, 초기 단계에서 커스텀 설정이 없다면 삭제 후 필요시 다시 생성해도 무방합니다.
- reportWebVitals.ts: 웹 바이탈(Core Web Vitals)을 측정하여 성능 리포팅을 돕는 유틸리티입니다. 프로젝트 성능 최적화 단계에서 중요한 역할을 하지만, 개발 초기 단계에서는 필수적이지 않습니다. 일단 삭제하고, 추후 성능 모니터링이 필요할 때 다시 추가하거나 GA(Google Analytics) 등 다른 분석 도구와 연동하는 것을 고려할 수 있습니다.
위 파일들을 모두 삭제한 후에는 관련 코드를 `App.tsx`와 `index.tsx`에서 제거해야 합니다.
// 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>프로젝트가 성공적으로 설정되었습니다.</h1>
</div>
);
}
export default App;
이제 `src` 폴더는 `App.tsx`, `index.tsx`, `react-app-env.d.ts` 단 세 개의 파일만 남아 훨씬 명확하고 깔끔한 출발점이 되었습니다.
1.2. 확장성을 고려한 폴더 구조 설계
정리가 끝난 `src` 폴더에 체계적인 구조를 도입할 차례입니다. 정답은 없지만, 일반적으로 많이 사용되는 도메인 기반의 폴더 구조는 다음과 같습니다.
/src
├── apis/ # API 호출 함수 및 관련 타입 정의
├── assets/ # 이미지, 폰트 등 정적 에셋
│ ├── images/
│ └── fonts/
├── components/ # 재사용 가능한 공용 컴포넌트
│ ├── common/ # 아주 작은 단위의 범용 컴포넌트 (Button, Input, Modal 등)
│ └── domain/ # 특정 도메인에 종속된 컴포넌트 (예: a-service/PostCard)
├── constants/ # 변하지 않는 상수 값 (API 경로, 에러 메시지 등)
├── hooks/ # 재사용 가능한 커스텀 훅
├── layouts/ # 페이지의 전체적인 레이아웃 컴포넌트 (Header, Footer 포함)
├── pages/ # 라우팅 단위가 되는 페이지 컴포넌트
├── stores/ # 전역 상태 관리 (Recoil, Zustand, Jotai 등)
├── styles/ # 전역 스타일, 테마, mixin 등
├── types/ # 프로젝트 전반에서 사용되는 공용 타입 정의
├── utils/ # 순수 함수 유틸리티 (date, string, validation 등)
├── App.tsx # 최상위 컴포넌트 및 라우터 설정
└── index.tsx # 애플리케이션 진입점
이러한 구조는 파일의 역할과 책임을 명확히 분리하여, 프로젝트가 커지더라도 코드를 찾고 이해하기 쉽게 만들어 줍니다. 지금 당장 모든 폴더를 만들 필요는 없습니다. 다만, 이러한 청사진을 가지고 개발을 시작하면 일관성을 유지하는 데 큰 도움이 됩니다.
2. 코드의 안정성을 극대화하는 `tsconfig.json` 고급 설정
tsconfig.json
은 타입스크립트 컴파일러에게 우리 프로젝트의 코드를 어떻게 해석하고 검사할지 알려주는 지휘자와 같습니다. CRA의 기본 설정도 `strict: true`를 포함하여 꽤 훌륭하지만, 몇 가지 옵션을 추가하고 조정하면 개발 경험(DX)과 코드의 견고함을 한 차원 높일 수 있습니다.
2.1. 절대 경로(Absolute Imports)와 경로 별칭(Path Aliases) 설정
프로젝트의 깊이가 깊어질수록 import MyComponent from '../../../components/common/MyComponent'
와 같은 상대 경로 참조는 코드를 지저분하게 만들고, 파일 이동 시 모든 경로를 수정해야 하는 유지보수의 악몽을 초래합니다. `baseUrl`과 `paths` 옵션을 사용해 이 문제를 해결할 수 있습니다.
tsconfig.json
의 `compilerOptions`에 다음 내용을 추가합니다.
{
"compilerOptions": {
// ... 기존 CRA 옵션 ...
"baseUrl": "src",
"paths": {
"@components/*": ["components/*"],
"@hooks/*": ["hooks/*"],
"@pages/*": ["pages/*"],
"@utils/*": ["utils/*"],
"@styles/*": ["styles/*"],
"@apis/*": ["apis/*"],
"@assets/*": ["assets/*"]
}
},
"include": ["src"]
}
- "baseUrl": "src"
: 모든 모듈 탐색의 기준 디렉토리를 src
로 설정합니다.
- "paths"
: `baseUrl`을 기준으로 경로에 대한 별칭(alias)을 지정합니다. `@` 기호를 사용하는 것은 다른 npm 패키지와 충돌을 피하기 위한 일반적인 관례입니다.
이제 깊은 곳에 있는 파일에서도 다음과 같이 깔끔하게 모듈을 불러올 수 있습니다.
// 변경 전
import { formatDate } from '../../../utils/dateUtils';
import { Button } from '../../../components/common/Button';
// 변경 후
import { formatDate } from '@utils/dateUtils';
import { Button } from '@components/common/Button';
주의: 이 설정은 타입스크립트 컴파일러에게만 경로를 알려줍니다. CRA의 웹팩(Webpack)이 이 경로를 인식하도록 하려면 별도의 설정이 필요합니다. 이는 후술할 Craco를 이용한 설정 커스터마이징 부분에서 다루겠습니다.
2.2. 더 엄격하고 명시적인 컴파일러 옵션
`strict: true`는 여러 엄격한 타입 검사 규칙들의 집합입니다. 여기에 몇 가지 옵션을 더 추가하여 잠재적 오류를 컴파일 시점에 원천 봉쇄할 수 있습니다.
{
"compilerOptions": {
// ...
"strict": true,
/* 추가적인 엄격함 */
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
/* 모듈 관련 설정 강화 */
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
// ...
}
}
forceConsistentCasingInFileNames: true
: 파일 이름의 대소문자를 일관되게 사용하도록 강제합니다. 예를 들어 `MyComponent.tsx`를 `import from 'mycomponent'`로 불러오려고 하면 에러가 발생합니다. 이는 대소문자를 구분하지 않는 개발 환경(macOS, Windows)과 구분하는 배포 환경(Linux) 사이의 불일치로 인한 런타임 오류를 방지하는 매우 중요한 설정입니다.noUnusedLocals: true
: 선언되었지만 사용되지 않는 지역 변수가 있으면 컴파일 에러를 발생시킵니다. 코드를 깔끔하게 유지하고 '죽은 코드'를 제거하는 데 도움이 됩니다.noUnusedParameters: true
: 사용되지 않는 함수 파라미터가 있으면 에러를 발생시킵니다. 라이브러리의 콜백 시그니처를 따라야 하는 등 의도적으로 사용하지 않는 파라미터는 이름 앞에 언더스코어(_
)를 붙여(예: `(event, _unusedValue) => {}`) 타입스크립트에 알릴 수 있습니다.noImplicitReturns: true
: 함수 내의 모든 코드 경로가 명시적으로 값을 반환하도록 강제합니다. `if` 문에서는 값을 반환하는데 `else` 문에서는 반환하지 않는 등의 실수를 막아줍니다.
3. 개발 생산성 극대화: ESLint, Prettier, 그리고 Husky 연동
협업 환경에서 일관된 코드 스타일과 품질을 유지하는 것은 매우 중요합니다. 이를 자동화하기 위해 ESLint(코드 품질 검사), Prettier(코드 스타일 포맷팅), Husky(Git Hooks 관리)를 조합하는 것은 현대 프론트엔드 개발의 표준과도 같습니다.
3.1. ESLint와 Prettier 연동: 역할 분담의 미학
ESLint는 잠재적인 버그나 안티 패턴(논리적 오류)을 찾아내는 린터(Linter)이고, Prettier는 들여쓰기, 줄 바꿈, 따옴표 등 코드의 스타일을 강제하는 포맷터(Formatter)입니다. 이 둘의 역할이 일부 겹치기 때문에 충돌이 발생할 수 있습니다. 따라서 ESLint는 코드 품질에만 집중하고, 스타일 관련 모든 규칙은 Prettier에게 위임하는 것이 좋습니다.
1. 필요 패키지 설치
# 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의 스타일 관련 규칙들을 모두 비활성화합니다.eslint-plugin-prettier
: Prettier의 포맷팅 규칙을 ESLint 규칙의 일부로 실행합니다. 즉, Prettier 스타일 가이드에 맞지 않는 코드가 있으면 ESLint가 에러로 보고하게 해줍니다.
2. 설정 파일 구성
프로젝트 루트에 각 도구의 설정 파일을 생성합니다.
.prettierrc.js (Prettier 설정 파일)
JSON 대신 JS 파일을 사용하면 주석을 추가하거나 동적인 로직을 넣을 수 있어 더 유연합니다.
// .prettierrc.js
module.exports = {
printWidth: 100, // 한 줄의 최대 길이를 100자로 제한
tabWidth: 2, // 탭 너비는 2칸
useTabs: false, // 탭 대신 스페이스 사용
semi: true, // 문장 끝에 세미콜론 추가
singleQuote: true, // 문자열은 홑따옴표로
trailingComma: 'all', // 객체나 배열 마지막에 항상 쉼표
arrowParens: 'always', // 화살표 함수 파라미터가 하나일 때도 괄호 사용
bracketSpacing: true, // 객체 리터럴에서 괄호 양쪽에 공백 추가 { foo: bar }
jsxSingleQuote: false, // JSX 속성에는 홑따옴표 사용 안 함
};
.eslintrc.js (ESLint 설정 파일)
package.json
내부의 `eslintConfig` 대신 별도 파일을 사용하면 더 복잡한 설정을 관리하기 용이합니다. 기존 `eslintConfig` 내용은 이 파일로 옮겨옵니다.
// .eslintrc.js
module.exports = {
root: true, // 이 설정 파일이 있는 디렉토리를 루트로 간주
env: {
browser: true,
es6: true,
},
extends: [
'react-app', // CRA 기본 설정
'react-app/jest',
'plugin:@typescript-eslint/recommended', // 타입스크립트 추천 규칙
'plugin:prettier/recommended', // eslint-plugin-prettier와 eslint-config-prettier를 한 번에 설정
],
parser: '@typescript-eslint/parser', // ESLint가 타입스크립트 코드를 파싱하게 함
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: [
'react',
'@typescript-eslint',
// 'import' // (선택) import 순서 등을 관리하고 싶다면 설치 후 추가
],
rules: {
// 여기에 개별적으로 재정의할 규칙을 추가합니다.
'prettier/prettier': 'error', // Prettier 규칙을 위반하면 에러 발생
'@typescript-eslint/no-unused-vars': 'warn', // 사용하지 않는 변수는 경고
'@typescript-eslint/explicit-module-boundary-types': 'off', // export되는 함수의 반환 타입 명시 강제 끄기
},
};
중요: extends
배열에서 plugin:prettier/recommended
는 반드시 가장 마지막에 와야 합니다. 그래야 이전 설정들의 스타일 규칙을 Prettier의 규칙으로 덮어쓸 수 있습니다.
3.2. Husky와 lint-staged로 커밋 전 코드 품질 자동 검사
위 설정들을 마쳤더라도 개발자가 직접 린트나 포맷 명령을 실행하는 것을 잊을 수 있습니다. `husky`와 `lint-staged`는 이 과정을 자동화하여, Git에 커밋하기 전(pre-commit hook)에 코드가 팀의 규칙을 준수하는지 강제합니다.
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
husky-init
명령은 `.husky` 디렉토리를 생성하고 `package.json`에 `prepare` 스크립트를 추가합니다. 이 `prepare` 스크립트는 다른 사람이 `npm install`을 실행할 때마다 husky가 Git hook을 설정하도록 보장합니다.
2. `pre-commit` 훅 설정
먼저, .husky/pre-commit
파일을 수정하여 `lint-staged`를 실행하도록 합니다.
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
3. `lint-staged` 설정
이제 package.json
에 `lint-staged` 설정을 추가합니다. `lint-staged`는 Git의 스테이징 영역(staged files)에 있는 파일에 대해서만 린터와 포맷터를 실행하므로 매우 효율적입니다.
// package.json
{
// ...
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,css,md}": [
"prettier --write"
]
}
}
이제부터 개발자가 커밋을 시도하면, `husky`가 `pre-commit` 훅을 트리거하여 `lint-staged`를 실행합니다. `lint-staged`는 스테이징된 `js, tsx` 등의 파일에 대해 `eslint --fix`와 `prettier --write`를 순차적으로 실행하여 자동으로 코드를 수정하고 포맷팅합니다. 만약 자동으로 해결할 수 없는 ESLint 에러가 남아있다면, 커밋 자체가 실패하게 됩니다. 이로써 Git 저장소에는 항상 일관된 품질의 코드만 남게 됩니다.
4. `eject` 없이 CRA 설정 커스터마이징: Craco 도입
CRA의 가장 큰 단점 중 하나는 Webpack이나 Babel 설정을 직접 수정할 수 없다는 것입니다. 설정을 변경하려면 npm run eject
명령을 실행해야 하는데, 이는 CRA가 숨겨왔던 모든 복잡한 설정 파일을 밖으로 꺼내버리는 '돌아올 수 없는 강'을 건너는 것과 같습니다. `eject`를 하는 순간부터 우리는 CRA의 편리한 업데이트를 더 이상 지원받을 수 없게 됩니다.
이 문제를 우아하게 해결해주는 도구가 바로 Craco (Create React App Configuration Override)입니다. Craco를 사용하면 `eject` 없이도 프로젝트 루트에 있는 간단한 설정 파일(`craco.config.js`)을 통해 Webpack, Babel, PostCSS 등의 설정을 덮어쓰거나 확장할 수 있습니다.
1. 설치 및 `package.json` 스크립트 수정
npm install @craco/craco
설치 후, package.json
의 `scripts` 부분을 `react-scripts` 대신 `craco`를 사용하도록 수정합니다.
// package.json
"scripts": {
"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.config.js
const CracoAlias = require('craco-alias');
module.exports = {
plugins: [
{
plugin: CracoAlias,
options: {
source: 'tsconfig',
// tsconfig.json에서 baseUrl과 paths를 자동으로 읽어옵니다.
tsConfigPath: 'tsconfig.paths.json',
},
},
],
babel: {
// 여기에 Babel 설정을 추가할 수 있습니다.
},
webpack: {
// 여기에 Webpack 설정을 추가할 수 있습니다.
configure: (webpackConfig, { env, paths }) => {
// 웹팩 설정을 직접 수정합니다.
// 예: 프로덕션 빌드에서만 특정 플러그인 추가 등
return webpackConfig;
},
},
};
tsconfig.json
에 `paths`가 이미 있다면 `craco-alias`는 별도의 설정 없이도 잘 작동합니다. 하지만, 컴파일과 관련된 옵션과 경로 별칭 옵션을 분리하여 관리하는 것이 더 깔끔할 수 있습니다. `tsconfig.json`에서 `paths` 부분을 `tsconfig.paths.json`이라는 별도 파일로 분리하고, `tsconfig.json`에서 이를 확장(extends)하도록 설정합니다.
tsconfig.paths.json
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@components/*": ["components/*"],
"@hooks/*": ["hooks/*"],
"@pages/*": ["pages/*"],
"@utils/*": ["utils/*"],
"@styles/*": ["styles/*"],
"@apis/*": ["apis/*"],
"@assets/*": ["assets/*"]
}
}
}
tsconfig.json
{
"extends": "./tsconfig.paths.json", // paths 설정 파일을 불러옵니다.
"compilerOptions": {
// ... 기존 컴파일 옵션 ...
// baseUrl과 paths는 이제 extends를 통해 가져옵니다.
},
"include": ["src"]
}
이제 `craco start`를 실행하면 Webpack이 `@components` 같은 경로를 정상적으로 해석하게 됩니다. 이처럼 Craco를 사용하면 Babel에 새로운 플러그인(예: `babel-plugin-macros`)을 추가하거나, Webpack의 loader 설정을 미세 조정하는 등 `eject` 없이도 높은 수준의 커스터마이징이 가능해집니다.
5. 결론: 전문가 수준의 프로젝트를 위한 탄탄한 초석
CRA는 훌륭한 시작점을 제공하지만, 그 자체로는 종착지가 아닙니다. 실무 레벨의 견고하고 확장 가능한 애플리케이션을 구축하기 위해서는 CRA의 추상화된 편리함 너머에 있는 설정들을 우리 프로젝트의 맥락에 맞게 다듬고 강화하는 과정이 필수적입니다.
우리는 이 글을 통해 다음과 같은 핵심적인 개선 작업을 수행했습니다.
- 폴더 구조 재설계: 불필요한 파일을 제거하고, 확장성을 고려한 체계적인 폴더 구조를 설계하여 유지보수성의 기틀을 마련했습니다.
- 타입스크립트 설정 강화: 절대 경로와 경로 별칭을 도입하여 개발 경험을 향상시키고, 더욱 엄격한 컴파일러 옵션을 통해 코드의 안정성을 비약적으로 높였습니다.
- 코드 품질 자동화 시스템 구축: ESLint와 Prettier를 연동하여 코드의 논리적 오류와 스타일을 분리해 관리하고, Husky와 lint-staged를 통해 이를 자동화함으로써 팀 전체의 코드 품질을 일관되게 유지하는 강력한 시스템을 구축했습니다.
- 설정 커스터마이징 유연성 확보: Craco를 도입하여
eject
라는 위험한 선택 없이도 Webpack과 Babel 설정을 자유롭게 확장할 수 있는 길을 열었습니다.
이러한 과정들은 처음에는 다소 번거롭고 복잡하게 느껴질 수 있습니다. 하지만 이는 프로젝트 초기에 반드시 지불해야 할 '기술 부채'의 이자와도 같습니다. 초기에 잘 닦아놓은 개발 환경이라는 길은, 프로젝트가 복잡해지고 팀원이 늘어나는 미래에 마주할 수많은 장애물들을 피해 목적지까지 빠르고 안전하게 도달할 수 있도록 안내하는 최고의 내비게이션이 될 것입니다. 이 글에서 제시된 전략들을 바탕으로 여러분만의, 혹은 여러분 팀만의 '골든 스탠다드' 프로젝트 템플릿을 만들어보시길 바랍니다. 그것이 바로 평범한 프로젝트와 성공적인 프로덕트의 차이를 만드는 전문가의 첫걸음입니다.
0 개의 댓글:
Post a Comment