프론트엔드 프로젝트 초기 설정 가이드
PL 관점에서 정리한 폴더 구조, 린터, 커밋 컨벤션 설정
프로젝트를 새로 시작할 때마다 초기 설정에 대한 고민이 생깁니다.
폴더 구조는 어떻게 잡을지, 린터는 뭘 쓸지, 커밋 규칙은 어떻게 정할지.
PL을 맡으면서 이런 설정들을 여러 번 반복하다 보니 나름의 기준이 생겼는데요,
이번 글에서는 제가 프로젝트 초기 설정 시 적용하는 방식들을 정리해보려고 합니다.
폴더 아키텍처
프론트엔드 프로젝트에서 폴더 구조는 팀의 생산성과 코드 품질에 직접적인 영향을 미칩니다.
어떤 아키텍처를 선택하느냐에 따라 파일을 찾는 시간, 코드 중복, 의존성 관리 방식이 달라집니다.
전통적인 폴더 구조의 한계
과거에는 파일 종류별로 폴더를 나누는 방식이 일반적이었습니다.
src/
├── components/
├── hooks/
├── utils/
├── services/
├── pages/
└── types/
프로젝트가 작을 때는 문제가 없지만, 규모가 커지면 한계가 드러납니다.
로그인 기능 하나를 수정하려면 components, hooks, services, types 폴더를 오가야 합니다.
관련 코드가 여기저기 흩어져 있어서 파악하기 어렵고, 의존성도 복잡해집니다.
Feature First Architecture
이런 문제를 해결하기 위해 등장한 것이 Feature First Architecture입니다.
이 아키텍처는 "파일 종류가 아닌 기능 단위로 코드를 조직화하자" 는 아이디어에서 출발했습니다.
2010년대 중반부터 React, Angular 커뮤니티에서 자연스럽게 퍼지기 시작했고,
특히 대규모 프로젝트에서 유지보수성을 높이기 위한 방법으로 채택되었습니다.
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api/
│ │ └── index.ts
│ ├── user/
│ └── product/
├── shared/
│ ├── components/
│ ├── hooks/
│ └── utils/
└── app/
기능(Feature) 단위로 폴더를 나누고, 각 기능에 필요한 컴포넌트, 훅, API 등을 한 곳에 모아두는 방식입니다.
Barrel Export 패턴
Feature First Architecture에서는 Barrel Export(index.ts)를 함께 사용하는 것이 일반적입니다.
외부에 노출할 것만 export하고, 내부 구현은 숨깁니다.
export { LoginForm } from './components/LoginForm'
export { useAuth } from './hooks/useAuth'
// 내부 구현은 export하지 않음이렇게 하면 다른 feature에서 auth를 사용할 때 내부 구조를 알 필요 없이 깔끔하게 import할 수 있습니다.
// 좋음
import { LoginForm, useAuth } from '@/features/auth'
// 피해야 함
import { LoginForm } from '@/features/auth/components/LoginForm'장점
- 기능별로 코드가 응집되어 있어 찾기 쉬움
- 기능 단위로 팀을 나눠 작업하기 좋음
- 기능 삭제 시 폴더 하나만 제거하면 됨
단점
- 기능 간 공유 코드 위치가 애매해지는 경우가 있음
- 프로젝트가 커지면 features 폴더가 비대해짐
- 레이어 간 의존성 방향이 명시적이지 않음
Feature-Sliced Design (FSD)
최근에는 Feature-Sliced Design을 사용하고 있습니다.
Feature First의 장점을 유지하면서 레이어 간 의존성을 더 명확하게 정의한 아키텍처입니다.
src/
├── app/ # 앱 진입점, 프로바이더, 라우터
├── pages/ # 페이지 컴포넌트
├── widgets/ # 독립적인 UI 블록 (헤더, 사이드바)
├── features/ # 사용자 시나리오 (로그인, 검색)
├── entities/ # 비즈니스 엔티티 (User, Product)
├── shared/ # 공유 유틸, UI 키트
FSD는 레이어 간 의존성 방향이 명확합니다.
상위 레이어는 하위 레이어를 import할 수 있지만, 그 반대는 허용되지 않습니다.
app → pages → widgets → features → entities → shared
FSD의 핵심은 단방향 의존성입니다.
순환 참조를 방지하고, 각 레이어의 역할이 분명해집니다.
FSD에 대한 자세한 내용은 아래 포스팅에서 다루고 있습니다.
린터 & 포매터: Biome
린터와 포매터는 Biome을 사용하고 있습니다.
ESLint + Prettier 조합을 오래 사용했는데, 설정 파일이 많아지고 플러그인 간 충돌 이슈가 종종 발생했습니다.
Biome은 린터와 포매터를 하나의 도구로 통합해서 이런 문제가 없고, 속도도 빠릅니다.
설치
npm install --save-dev --save-exact @biomejs/biome
npx @biomejs/biome init기본 설정
biome.json에 설정을 작성합니다.
제가 주로 사용하는 설정입니다.
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"organizeImports": {
"enabled": true
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedImports": "warn",
"noUnusedVariables": "warn"
},
"style": {
"useImportType": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "es5",
"semicolons": "asNeeded"
}
}
}noUnusedImports를 error로 설정하면 개발 중 빌드가 자주 깨질 수 있습니다.
warn으로 두고 CI에서만 error로 처리하는 방법도 있습니다.
package.json 스크립트
{
"scripts": {
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write ."
}
}Biome 설정에 대한 자세한 내용은 아래 포스팅에서 다루고 있습니다.
커밋 컨벤션
일관된 커밋 메시지는 히스토리 추적과 코드 리뷰에 큰 도움이 됩니다.
Conventional Commits 규칙을 따르고 있습니다.
커밋 메시지 형식
<type>: <subject>
[optional body]
허용하는 타입
| 타입 | 설명 |
|---|---|
feat | 새로운 기능 추가 |
fix | 버그 수정 |
refactor | 리팩토링 (기능 변경 없음) |
chore | 빌드, 설정 등 기타 작업 |
docs | 문서 수정 |
delete | 코드 삭제 |
타입을 많이 두면 오히려 혼란스러워서 6개 정도로 제한하고 있습니다.
style, test, perf 등은 상황에 따라 refactor나 chore에 포함시킵니다.
커밋 메시지 예시
feat: 로그인 폼 유효성 검사 추가
fix: 토큰 갱신 시 무한 루프 수정
refactor: API 클라이언트 에러 핸들링 개선
chore: biome 설정 업데이트
docs: README에 환경 변수 설명 추가
delete: 사용하지 않는 유틸 함수 제거Husky & lint-staged
커밋 전에 린트와 포맷팅을 자동으로 실행하기 위해 Husky와 lint-staged를 사용합니다.
설치
npm install --save-dev husky lint-staged
npx husky initlint-staged 설정
package.json에 추가합니다.
{
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,md}": [
"biome check --write --no-errors-on-unmatched"
]
}
}pre-commit 훅
.husky/pre-commit 파일을 수정합니다.
npx lint-staged이렇게 설정하면 커밋할 때 스테이징된 파일에 대해서만 린트와 포맷팅이 실행됩니다.
전체 프로젝트를 검사하지 않아서 빠릅니다.
commitlint
커밋 메시지 규칙을 강제하기 위해 commitlint를 사용합니다.
설치
npm install --save-dev @commitlint/cli @commitlint/config-conventional설정 파일
commitlint.config.js를 생성합니다.
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'refactor', 'chore', 'docs', 'delete']
],
'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']],
},
}type-enum에서 허용할 타입을 지정합니다.
subject-case는 제목이 대문자로 시작하지 않도록 강제합니다.
commit-msg 훅
.husky/commit-msg 파일을 생성합니다.
npx --no -- commitlint --edit $1이제 규칙에 맞지 않는 커밋 메시지는 거부됩니다.
git commit -m "Add login feature"
# ✖ type may not be empty
# ✖ subject may not be empty
git commit -m "feat: add login feature"
# ✔ 성공Git 훅을 통해 규칙을 자동으로 강제하면 코드 리뷰에서 스타일 관련 논쟁을 줄일 수 있습니다.
TypeScript 공유 설정
규모가 있는 프로젝트에서는 TypeScript 설정을 별도로 분리해서 관리하는 것이 좋습니다.
여러 앱이나 패키지에서 일관된 설정을 사용할 수 있습니다.
공유 설정 구조
packages/
└── typescript-config/
├── base.json
├── react.json
├── node.json
└── package.json
base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"esModuleInterop": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
}
}react.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx"
}
}앱에서 사용
{
"extends": "@repo/typescript-config/react.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}환경 변수 관리
환경 변수는 .env 파일로 관리하고, 타입 안전성을 위해 타입 정의를 추가합니다.
.env 파일 구조
.env # 기본값 (git에 포함 가능)
.env.local # 로컬 오버라이드 (git에서 제외)
.env.development # 개발 환경
.env.production # 프로덕션 환경
Vite 환경 변수 타입
vite-env.d.ts에 타입을 정의합니다.
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
readonly VITE_AUTH_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}환경 변수에 민감한 정보를 넣을 때는 .env.local을 사용하고,
반드시 .gitignore에 추가해야 합니다.
브랜치 전략
브랜치 전략은 팀 규모와 배포 주기에 따라 다르지만, 기본적으로 세 개의 메인 브랜치를 사용합니다.
main # 프로덕션 배포
stg # 스테이징/QA
develop # 개발 통합
브랜치 네이밍
feature/login-form
fix/token-refresh-loop
refactor/api-client
chore/update-dependencies
커밋 타입과 동일하게 prefix를 사용합니다.
이렇게 하면 브랜치 이름만 봐도 어떤 작업인지 파악할 수 있습니다.
정리
프로젝트 초기 설정에서 중요하게 생각하는 포인트입니다.
- 폴더 구조: 팀 규모와 프로젝트 성격에 맞는 아키텍처 선택
- 린터 & 포매터: 하나의 도구로 통합 (Biome)
- 커밋 컨벤션: 타입을 적게 유지하고 자동화로 강제
- Git 훅: 커밋 전에 린트, 커밋 메시지 검증
- TypeScript: 공유 설정으로 일관성 유지
초기 설정에 시간을 투자하면 이후 개발 과정에서 불필요한 논쟁과 실수를 줄일 수 있습니다.
팀원들이 코드 스타일보다 비즈니스 로직에 집중할 수 있는 환경을 만드는 것이 PL의 역할이라고 생각합니다.
감사합니다.