vinext + pnpm + Turborepo 기반 모노레포 프로젝트 세팅 가이드
| 카테고리 | 기술 |
|---|---|
| Framework | vinext (Vite 기반 Next.js API 재구현) |
| Package Manager | pnpm |
| Monorepo Tool | Turborepo |
| Architecture | FSD (Feature-Sliced Design) + 모노레포 패키지 분리 |
| Linting | ESLint (Flat Config) |
| Formatting | Prettier |
| Testing | Vitest |
| Commit Convention | Commitlint + Husky + lint-staged |
| Code Review | CodeRabbit |
| Coverage | Codecov |
root/
├── apps/
│ ├── web/ # vinext 메인 앱
│ │ └── src/
│ │ ├── app/ # FSD: app 레이어 (라우팅, 프로바이더)
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── providers/
│ │ ├── pages/ # FSD: pages 레이어 (페이지 컴포지션)
│ │ │ └── home/
│ │ ├── widgets/ # FSD: widgets 레이어 (독립적 UI 블록)
│ │ │ └── header/
│ │ ├── features/ # FSD: features 레이어 (유저 시나리오)
│ │ │ └── auth/
│ │ ├── entities/ # FSD: entities 레이어 (비즈니스 엔티티)
│ │ │ └── user/
│ │ └── shared/ # FSD: shared 레이어 (공유 유틸, UI)
│ │ ├── ui/
│ │ ├── lib/
│ │ ├── api/
│ │ └── config/
│ └── admin/ # (선택) 어드민 앱
├── packages/
│ ├── ui/ # 공유 디자인 시스템
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── config/ # 공유 설정 (eslint, tsconfig, prettier)
│ │ ├── eslint/
│ │ ├── typescript/
│ │ └── prettier/
│ └── utils/ # 공유 유틸리티
│ ├── src/
│ └── package.json
├── .github/
│ └── workflows/
│ └── ci.yml
├── .husky/
│ ├── pre-commit
│ └── commit-msg
├── .coderabbit.yaml
├── codecov.yml
├── turbo.json
├── pnpm-workspace.yaml
├── package.json
├── commitlint.config.js
└── README.md
app → pages → widgets → features → entities → shared
↓ ↓ ↓ ↓ ↓ ✗
상위 레이어는 하위 레이어만 import 가능 (단방향 의존)
각 slice 내부 구조:
features/auth/
├── ui/ # 컴포넌트
│ └── LoginForm.tsx
├── model/ # 상태, 비즈니스 로직
│ └── useAuth.ts
├── api/ # API 호출
│ └── authApi.ts
├── lib/ # 유틸리티
├── types/ # 타입 정의
└── index.ts # Public API (반드시 이것만 export)
# 프로젝트 디렉토리 생성
mkdir my-project && cd my-project
# pnpm 초기화
pnpm init
# pnpm-workspace.yaml 생성
cat > pnpm-workspace.yaml << 'EOF'
packages:
- "apps/*"
- "packages/*"
EOF
# 디렉토리 구조 생성
mkdir -p apps/web packages/{ui,config,utils}pnpm add -Dw turboturbo.json:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".vinext/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"cache": false
},
"test:coverage": {
"cache": false
}
}
}root package.json scripts:
{
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"lint": "turbo lint",
"test": "turbo test",
"test:coverage": "turbo test:coverage",
"format": "prettier --write .",
"format:check": "prettier --check .",
"prepare": "husky"
}
}cd apps/web
# vinext 프로젝트 초기화
npx vinext init
# 또는 기존 Next.js 프로젝트를 마이그레이션하는 경우
# package.json의 "next" 스크립트를 "vinext"로 변경apps/web/package.json:
{
"name": "@app/web",
"private": true,
"scripts": {
"dev": "vinext dev",
"build": "vinext build",
"start": "vinext start",
"lint": "eslint .",
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"vinext": "latest",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@app/ui": "workspace:*",
"@app/utils": "workspace:*"
}
}packages/config/eslint/package.json:
{
"name": "@app/eslint-config",
"private": true,
"dependencies": {
"@eslint/js": "^9.0.0",
"typescript-eslint": "^8.0.0",
"eslint-plugin-react": "latest",
"eslint-plugin-react-hooks": "latest",
"eslint-config-prettier": "latest"
}
}packages/config/eslint/base.js:
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";
export default [
js.configs.recommended,
...tseslint.configs.recommended,
prettierConfig,
{
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_" },
],
"@typescript-eslint/no-explicit-any": "warn",
},
},
{
ignores: ["node_modules/", "dist/", ".vinext/"],
},
];packages/config/eslint/react.js:
import baseConfig from "./base.js";
import reactPlugin from "eslint-plugin-react";
import reactHooksPlugin from "eslint-plugin-react-hooks";
export default [
...baseConfig,
{
plugins: {
react: reactPlugin,
"react-hooks": reactHooksPlugin,
},
rules: {
...reactPlugin.configs.recommended.rules,
...reactHooksPlugin.configs.recommended.rules,
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
},
settings: {
react: { version: "detect" },
},
},
];apps/web/eslint.config.js:
import reactConfig from "@app/eslint-config/react";
export default [
...reactConfig,
{
// 앱 특화 규칙
rules: {},
},
];packages/config/prettier/package.json:
{
"name": "@app/prettier-config",
"private": true,
"type": "module",
"main": "index.js"
}packages/config/prettier/index.js:
/** @type {import("prettier").Config} */
export default {
semi: true,
singleQuote: false,
tabWidth: 2,
trailingComma: "all",
printWidth: 80,
bracketSpacing: true,
arrowParens: "always",
endOfLine: "lf",
};root .prettierrc.js:
export { default } from "@app/prettier-config";.prettierignore:
node_modules
dist
.vinext
pnpm-lock.yaml
coverage
# 루트에서 설치
pnpm add -Dw husky lint-staged @commitlint/cli @commitlint/config-conventional
# Husky 초기화
npx husky init.husky/pre-commit:
pnpm lint-staged.husky/commit-msg:
npx --no -- commitlint --edit "$1"root package.json에 추가:
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,yml,yaml}": ["prettier --write"]
}
}commitlint.config.js:
export default {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"feat", // 새로운 기능
"fix", // 버그 수정
"docs", // 문서 변경
"style", // 코드 스타일 (포매팅 등)
"refactor", // 리팩토링
"perf", // 성능 개선
"test", // 테스트 추가/수정
"build", // 빌드 시스템 변경
"ci", // CI 설정 변경
"chore", // 기타 변경
"revert", // 되돌리기
],
],
"subject-case": [2, "never", ["start-case", "pascal-case", "upper-case"]],
"subject-max-length": [2, "always", 72],
},
};feat(auth): 소셜 로그인 기능 추가
fix(cart): 수량 변경 시 총액 미갱신 버그 수정
docs: README 프로젝트 구조 섹션 추가
refactor(entities/user): 유저 모델 타입 정리
ci: codecov 업로드 step 추가
pnpm add -Dw vitest @vitest/coverage-v8apps/web/vitest.config.ts:
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./src/shared/lib/test-setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
coverage: {
provider: "v8",
reporter: ["text", "lcov", "json-summary"],
reportsDirectory: "./coverage",
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/**/*.d.ts",
"src/**/*.test.{ts,tsx}",
"src/**/*.spec.{ts,tsx}",
"src/**/index.ts",
"src/app/**",
],
thresholds: {
statements: 80,
branches: 80,
functions: 80,
lines: 80,
},
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});apps/web/src/shared/lib/test-setup.ts:
import "@testing-library/jest-dom/vitest";features/auth/
├── ui/
│ ├── LoginForm.tsx
│ └── LoginForm.test.tsx # 컴포넌트 테스트
├── model/
│ ├── useAuth.ts
│ └── useAuth.test.ts # 훅 테스트
└── api/
├── authApi.ts
└── authApi.test.ts # API 호출 테스트
.coderabbit.yaml:
language: "ko"
reviews:
profile: "chill"
request_changes_workflow: false
high_level_summary: true
poem: false
review_status: true
collapse_walkthrough: false
auto_review:
enabled: true
drafts: false
chat:
auto_reply: true- CodeRabbit에서 GitHub 앱 설치
- 레포지토리에
.coderabbit.yaml파일 추가 - PR을 올리면 자동으로 AI 코드 리뷰가 동작
coverage:
status:
project:
default:
target: 80%
threshold: 5%
patch:
default:
target: 80%
comment:
layout: "reach,diff,flags,files"
behavior: default
require_changes: false
ignore:
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.spec.ts"
- "**/test-setup.ts"
- "packages/config/**".github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm format:check
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./apps/web/coverage/lcov.info
fail_ci_if_error: false
build:
name: Build
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm build- Codecov에서 GitHub로 로그인
- 레포지토리 연결
CODECOV_TOKEN을 GitHub repository secrets에 추가- PR마다 커버리지 리포트가 자동으로 댓글에 표시됨
1. pnpm init + pnpm-workspace.yaml 생성
2. Turborepo 설치 및 turbo.json 설정
3. packages/config 생성 (eslint, prettier, tsconfig)
4. apps/web에서 vinext init
5. ESLint + Prettier 설정 연결
6. Husky + lint-staged + commitlint 설치 및 설정
7. Vitest 설치 및 vitest.config.ts 작성
8. .github/workflows/ci.yml 작성
9. .coderabbit.yaml 추가
10. codecov.yml 추가 + Codecov 연동
11. packages/ui, packages/utils 기본 구조 생성
- vinext는 실험적 프레임워크입니다. 프로덕션 사용 시 주의가 필요합니다
- Next.js API의 약 94%를 지원하지만, 일부 edge case에서 동작이 다를 수 있습니다
- 빌드 출력 디렉토리는
.vinext/입니다 (Next.js의.next/대신) npx vinext dev,npx vinext build,npx vinext deploy명령어 사용
- Public API 규칙: 각 slice는 반드시
index.ts를 통해서만 export - 단방향 의존: 상위 레이어 → 하위 레이어만 import 가능
- cross-import 금지: 같은 레이어 내 slice 간 직접 import 금지
- 패키지 간 의존은
"workspace:*"프로토콜 사용 - 새 패키지 추가 시
pnpm-workspace.yaml패턴과 일치하는지 확인 - Turborepo 캐시는
.turbo/디렉토리에 저장됨