# 아키텍처 통합 위험 요소 분석 ## 📋 문서 개요 이 문서는 현재 구성된 기반 설정에 추가 설계 가이드를 병합할 때 예상되는 위험 요소와 해결 방안을 제시합니다. **작성일**: 2025-11-06 **업데이트**: 2025-11-06 (Next.js 15.5.6으로 다운그레이드, React Hook Form + Zod 추가) **프로젝트**: Multi-tenant ERP System **기술 스택**: - Frontend: Next.js 15.5.6, React 19, next-intl, React Hook Form, Zod, TypeScript 5 - Backend: PHP Laravel + Sanctum (API) - Deployment: Vercel (Frontend) --- ## 🏗️ 현재 아키텍처 구성 ### 1. 기술 스택 ```yaml Frontend (Next.js): - Next.js: 15.5.6 (stable, production-ready) - React: 19.2.0 (latest) - TypeScript: 5.x - Deployment: Vercel Internationalization: - next-intl: 4.4.0 - Locales: ko (default), en, ja Form Management & Validation: - React Hook Form: 7.54.2 - Zod: 3.24.1 - @hookform/resolvers: 3.9.1 Styling: - Tailwind CSS: 4.x (latest) - PostCSS: 4.x Backend (Laravel): - PHP Laravel: 10.x+ - Database: MySQL/PostgreSQL - Authentication: Laravel Sanctum (SPA Token Authentication) - API: RESTful JSON API - Deployment: 별도 서버 (Git 관리) Architecture: - Frontend: Next.js (Vercel) - UI/UX, i18n - Backend: Laravel - Business Logic, DB, API - Communication: HTTP/HTTPS API calls - Auth Flow: Laravel Sanctum → Token → Next.js Storage ``` ### 2. 디렉토리 구조 ``` src/ ├── app/[locale]/ # 다국어 라우팅 ├── components/ # 공용 컴포넌트 ├── i18n/ # i18n 설정 ├── messages/ # 번역 파일 (ko, en, ja) └── middleware.ts # 통합 미들웨어 ``` ### 3. 구현된 기능 - ✅ 다국어 지원 (ko, en, ja) - ✅ SEO 최적화 (noindex, robots.txt) - ✅ 봇 차단 미들웨어 - ✅ 보안 헤더 설정 - ✅ TypeScript 엄격 모드 - ✅ 폼 관리 및 유효성 검증 (React Hook Form + Zod) --- ## ⚠️ 주요 위험 요소 ### 🔴 HIGH PRIORITY #### 1. 멀티 테넌시 + i18n 복잡도 **문제**: 테넌트 격리와 다국어 라우팅의 충돌 가능성 **예상 시나리오**: ``` ❌ 잠재적 충돌: /[locale]/[tenant]/dashboard vs /[tenant]/[locale]/dashboard 어떤 구조를 선택할 것인가? ``` **위험도**: 🔴 높음 **영향 범위**: - URL 구조 전체 - 라우팅 로직 - 미들웨어 복잡도 - SEO 구조 **해결 방안**: **옵션 A: Locale 우선 (현재 구조 유지)** ```typescript // URL 구조: /[locale]/[tenant]/dashboard // 장점: i18n 우선, 언어 전환 간편 // 단점: 테넌트별 커스텀 도메인 어려움 /ko/acme-corp/dashboard → ACME 한국어 대시보드 /en/acme-corp/dashboard → ACME 영어 대시보드 /ko/beta-inc/dashboard → Beta Inc. 한국어 대시보드 ``` **옵션 B: Tenant 우선** ```typescript // URL 구조: /[tenant]/[locale]/dashboard // 장점: 테넌트 격리 명확, 커스텀 도메인 용이 // 단점: 언어 전환 시 URL 복잡도 증가 /acme-corp/ko/dashboard /acme-corp/en/dashboard ``` **옵션 C: 서브도메인 분리 (권장)** ```typescript // URL 구조: {tenant}.domain.com/[locale]/dashboard // 장점: 완벽한 테넌트 격리, 깔끔한 URL // 단점: DNS 설정 필요, 미들웨어 복잡도 증가 acme-corp.erp.com/ko/dashboard acme-corp.erp.com/en/dashboard beta-inc.erp.com/ko/dashboard ``` **권장 전략**: ```typescript // 1단계: 개발 환경 (Locale 우선) /[locale]/[tenant]/dashboard // 2단계: 프로덕션 (서브도메인) {tenant}.domain.com/[locale]/dashboard // 미들웨어에서 처리 export function middleware(request: NextRequest) { const hostname = request.headers.get('host'); // 서브도메인에서 테넌트 추출 const tenant = extractTenantFromHostname(hostname); // 로케일은 기존 로직 사용 const locale = detectLocale(request); // 컨텍스트에 테넌트 정보 주입 request.headers.set('x-tenant-id', tenant); } ``` --- #### 3. 미들웨어 성능 및 복잡도 **현재 미들웨어 책임**: ```typescript 1. 로케일 감지 및 리다이렉션 2. 봇 차단 (User-Agent 검사) 3. 보안 헤더 추가 4. 로깅 향후 추가 예상: 5. 인증 검증 (JWT/Session) 6. 권한 확인 (RBAC) 7. 테넌트 식별 및 격리 8. Rate Limiting 9. API 키 검증 10. CORS 처리 ``` **위험도**: 🔴 높음 (복잡도 증가) **성능 영향**: ```typescript // 미들웨어는 모든 요청마다 실행됨 // 현재: ~5-10ms // 인증 추가: ~20-50ms // DB 조회 추가: ~100-200ms ⚠️ 위험! ``` **해결 방안**: **1. 미들웨어 분리 전략** ```typescript // src/middleware/index.ts import { chainMiddleware } from '@/lib/middleware-chain'; import { i18nMiddleware } from './i18n'; import { botBlockingMiddleware } from './bot-blocking'; import { authMiddleware } from './auth'; import { tenantMiddleware } from './tenant'; export default chainMiddleware([ i18nMiddleware, // 1순위: 로케일 감지 botBlockingMiddleware, // 2순위: 봇 차단 (빠른 종료) tenantMiddleware, // 3순위: 테넌트 식별 authMiddleware, // 4순위: 인증 (DB 조회 최소화) ]); ``` **2. 성능 최적화** ```typescript // ✅ 캐싱 활용 const tenantCache = new Map(); // ✅ DB 조회 최소화 // 미들웨어: 토큰 검증만 // API Route: DB 조회 // ✅ Edge Runtime 활용 (Vercel/Cloudflare) export const config = { runtime: 'edge', // 빠른 실행 }; ``` **3. 조건부 실행** ```typescript export function middleware(request: NextRequest) { const { pathname } = request.nextUrl; // 정적 파일은 스킵 if (pathname.startsWith('/_next/static')) { return NextResponse.next(); } // 공개 경로는 인증 스킵 if (PUBLIC_PATHS.includes(pathname)) { return i18nOnly(request); } // 보호된 경로만 전체 검증 return fullMiddleware(request); } ``` --- ### 🟡 MEDIUM PRIORITY #### 4. 데이터베이스 스키마와 다국어 (Laravel 백엔드) **✅ 확정**: 데이터베이스 및 API는 Laravel에서 관리 **Laravel 다국어 처리 전략**: **옵션 A: JSON 컬럼 (Laravel에서 간편)** ```php // Laravel Migration Schema::create('products', function (Blueprint $table) { $table->uuid('id')->primary(); $table->string('sku', 50)->unique(); $table->json('name'); // {"ko": "제품명", "en": "Product Name", "ja": "製品名"} $table->json('description')->nullable(); $table->timestamps(); }); // Laravel Model class Product extends Model { protected $casts = [ 'name' => 'array', 'description' => 'array', ]; public function getTranslatedName($locale = 'ko') { return $this->name[$locale] ?? $this->name['ko']; } } ``` **옵션 B: 번역 테이블 (권장 - 성능 최적화)** ```php // Laravel Migration - products table Schema::create('products', function (Blueprint $table) { $table->uuid('id')->primary(); $table->string('sku', 50)->unique(); $table->timestamps(); }); // Laravel Migration - product_translations table Schema::create('product_translations', function (Blueprint $table) { $table->uuid('product_id'); $table->string('locale', 5); $table->string('name'); $table->text('description')->nullable(); $table->primary(['product_id', 'locale']); $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); $table->index('locale'); }); // Laravel Model class Product extends Model { public function translations() { return $this->hasMany(ProductTranslation::class); } public function translation($locale = 'ko') { return $this->translations()->where('locale', $locale)->first(); } } class ProductTranslation extends Model { public $timestamps = false; protected $fillable = ['locale', 'name', 'description']; } ``` **Laravel API 응답 예시**: ```php // API Controller public function show(Product $product, Request $request) { $locale = $request->header('X-Locale', 'ko'); return response()->json([ 'id' => $product->id, 'sku' => $product->sku, 'name' => $product->translation($locale)->name, 'description' => $product->translation($locale)->description, ]); } ``` **Next.js에서 사용**: ```typescript // API 호출 with 로케일 const fetchProduct = async (id: string, locale: string) => { const res = await fetch(`${LARAVEL_API_URL}/api/products/${id}`, { headers: { 'X-Locale': locale, 'Authorization': `Bearer ${token}`, }, }); return res.json(); }; ``` **권장**: 옵션 B (번역 테이블) - Laravel Eloquent ORM과 잘 동작 --- #### 5. 인증 시스템 통합 (Laravel Sanctum) **✅ 확정**: 인증은 Laravel Sanctum에서 처리, Next.js는 토큰 관리만 **Laravel Sanctum 인증 플로우**: ``` 1. 로그인 요청 (Next.js) ↓ 2. Laravel API 인증 (/api/login) ↓ 3. Sanctum Token 발급 ↓ 4. Next.js에 토큰 저장 (Cookie/LocalStorage) ↓ 5. 이후 모든 API 요청에 토큰 포함 ``` **Laravel API 설정**: ```php // routes/api.php Route::post('/login', [AuthController::class, 'login']); Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum'); Route::get('/user', function (Request $request) { return $request->user(); })->middleware('auth:sanctum'); // app/Http/Controllers/AuthController.php public function login(Request $request) { $credentials = $request->validate([ 'email' => 'required|email', 'password' => 'required', ]); if (!Auth::attempt($credentials)) { return response()->json(['message' => 'Invalid credentials'], 401); } $user = Auth::user(); $token = $user->createToken('auth-token')->plainTextToken; return response()->json([ 'user' => $user, 'token' => $token, ]); } ``` **Next.js 미들웨어 (토큰 검증만)**: ```typescript // src/middleware.ts export function middleware(request: NextRequest) { const { pathname } = request.nextUrl; // 1단계: i18n 먼저 처리 (로케일 정규화) const intlResponse = intlMiddleware(request); // 2단계: 정규화된 경로로 인증 체크 const locale = getLocaleFromPath(intlResponse.url); const pathWithoutLocale = removeLocale(pathname, locale); // 3단계: 보호된 경로인지 확인 if (requiresAuth(pathWithoutLocale)) { // 쿠키에서 토큰 확인 const token = request.cookies.get('auth_token')?.value; if (!token) { // 로케일 포함하여 로그인 페이지로 리다이렉트 const loginUrl = new URL(`/${locale}/login`, request.url); loginUrl.searchParams.set('callbackUrl', request.url); return NextResponse.redirect(loginUrl); } // ⚠️ 주의: 미들웨어에서는 토큰 유효성 검증 안 함 // → Laravel API 호출 시 자동으로 검증됨 // → 성능 최적화 (매 요청마다 DB 조회 방지) } return intlResponse; } ``` **Next.js API 호출 유틸리티**: ```typescript // src/lib/api.ts const LARAVEL_API_URL = process.env.NEXT_PUBLIC_LARAVEL_API_URL; export async function apiCall(endpoint: string, options: RequestInit = {}) { const token = getCookie('auth_token'); const res = await fetch(`${LARAVEL_API_URL}${endpoint}`, { ...options, headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json', 'Content-Type': 'application/json', ...options.headers, }, }); if (res.status === 401) { // 토큰 만료 → 로그아웃 처리 deleteCookie('auth_token'); window.location.href = '/login'; } return res.json(); } // 로그인 export async function login(email: string, password: string) { const data = await apiCall('/api/login', { method: 'POST', body: JSON.stringify({ email, password }), }); // 토큰 저장 setCookie('auth_token', data.token, { maxAge: 60 * 60 * 24 * 7 }); // 7일 return data.user; } // 로그아웃 export async function logout() { await apiCall('/api/logout', { method: 'POST' }); deleteCookie('auth_token'); } ``` **주요 특징**: - ✅ **Next.js 미들웨어**: 토큰 존재 여부만 확인 (빠름) - ✅ **Laravel API**: 실제 토큰 검증 및 사용자 인증 - ✅ **토큰 저장**: HTTP-only Cookie (XSS 방지) - ✅ **토큰 갱신**: Laravel Sanctum 자동 처리 --- #### 6. 빌드 및 배포 설정 **정적 생성 vs 동적 렌더링**: **현재 문제**: ```typescript // 모든 로케일 × 모든 페이지 조합 생성 // 3개 언어 × 100개 페이지 = 300개 정적 페이지 // → 빌드 시간 증가 export function generateStaticParams() { return locales.map((locale) => ({ locale })); } ``` **해결 방안**: ```typescript // 옵션 1: ISR (Incremental Static Regeneration) export const revalidate = 3600; // 1시간마다 재생성 // 옵션 2: 동적 렌더링 (인증 필요 페이지) export const dynamic = 'force-dynamic'; // 옵션 3: 하이브리드 (공개 페이지는 정적, 대시보드는 동적) // src/app/[locale]/(public)/page.tsx → 정적 // src/app/[locale]/(protected)/dashboard/page.tsx → 동적 ``` **권장 전략**: ```typescript // 1. 공개 페이지 export const dynamic = 'force-static'; export const revalidate = 3600; // 2. 대시보드/ERP 기능 export const dynamic = 'force-dynamic'; // 3. 리포트 페이지 export const dynamic = 'force-dynamic'; export const revalidate = 300; // 5분 캐시 ``` --- ### 🟢 LOW PRIORITY #### 7. UI 컴포넌트 라이브러리 선택 **예상 추가 의존성**: ```json { "dependencies": { // 옵션 1: shadcn/ui (권장) "@radix-ui/react-*": "^latest", // 옵션 2: Material-UI "@mui/material": "^latest", // 옵션 3: Ant Design "antd": "^latest" } } ``` **i18n 통합 고려사항**: ```typescript // shadcn/ui: next-intl과 잘 작동 import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/button'; const t = useTranslations('common'); // Material-UI: 별도 LocalizationProvider 필요 import { LocalizationProvider } from '@mui/x-date-pickers'; // → next-intl과 중복 가능성 ``` **권장**: shadcn/ui (Tailwind 기반, next-intl 호환) --- #### 8. 상태 관리 라이브러리 **예상 추가 의존성**: ```json { "dependencies": { // 옵션 1: Zustand (권장) "zustand": "^latest", // 옵션 2: Redux Toolkit "@reduxjs/toolkit": "^latest", "react-redux": "^latest", // 옵션 3: Jotai "jotai": "^latest" } } ``` **다국어 통합**: ```typescript // Zustand + next-intl import { create } from 'zustand'; import { useLocale } from 'next-intl'; const useStore = create((set) => ({ locale: 'ko', setLocale: (locale) => set({ locale }), })); // 컴포넌트 const locale = useLocale(); // next-intl const { setLocale } = useStore(); // 전역 상태 ``` **충돌 가능성**: 낮음 (독립적 동작) --- ## 🛡️ 통합 체크리스트 ### 설계 가이드 병합 전 확인사항 #### Phase 1: 라우팅 구조 확정 - [ ] 멀티 테넌시 전략 결정 (서브도메인 vs URL 기반) - [ ] URL 구조 최종 확정 (`/[locale]/[tenant]` vs `{tenant}.domain/[locale]`) - [ ] 미들웨어 실행 순서 정의 - [ ] 404/에러 페이지 다국어 처리 #### Phase 2: 데이터베이스 설계 - [ ] 다국어 데이터 저장 방식 결정 (JSON vs 번역 테이블) - [ ] Prisma 스키마 작성 - [ ] 마이그레이션 전략 수립 - [ ] 시드 데이터 다국어 준비 #### Phase 3: 인증 시스템 - [ ] 인증 라이브러리 선택 (NextAuth.js, Clerk, Supabase Auth 등) - [ ] 세션 관리 전략 (JWT vs Database Session) - [ ] 미들웨어 통합 (i18n + auth 순서) - [ ] 로그인/로그아웃 플로우 다국어 처리 #### Phase 4: UI/UX - [ ] 컴포넌트 라이브러리 선택 - [ ] 디자인 시스템 정의 - [ ] 반응형 레이아웃 전략 - [ ] 다크모드 지원 여부 #### Phase 5: 성능 최적화 - [ ] ISR vs SSR vs SSG 전략 - [ ] 이미지 최적화 (next/image) - [ ] 폰트 최적화 - [ ] 번들 크기 모니터링 #### Phase 6: 배포 준비 - [ ] 환경 변수 관리 (.env.local, .env.production) - [ ] CI/CD 파이프라인 - [ ] 도메인 및 DNS 설정 - [ ] 모니터링 도구 (Sentry, LogRocket 등) --- ## 🔧 권장 마이그레이션 전략 ### 단계별 통합 플랜 #### Week 1-2: 기반 구조 검증 ```bash ✓ 현재 구조 분석 ✓ 설계 가이드 리뷰 ✓ 충돌 포인트 식별 ✓ 통합 전략 수립 ``` #### Week 3-4: 라우팅 및 미들웨어 ```bash - 멀티 테넌시 구조 구현 - 미들웨어 리팩토링 (체이닝) - 테넌트 격리 테스트 - 성능 벤치마크 ``` #### Week 5-6: 데이터베이스 및 인증 ```bash - Prisma 스키마 완성 - 인증 시스템 통합 - 테넌트별 데이터 격리 - 권한 시스템 구현 ``` #### Week 7-8: UI 컴포넌트 및 기능 ```bash - 컴포넌트 라이브러리 설치 - 공통 컴포넌트 개발 - ERP 모듈 구현 시작 - E2E 테스트 작성 ``` --- ## 📊 위험도 매트릭스 | 위험 요소 | 발생 확률 | 영향도 | 우선순위 | 대응 전략 | |---------|---------|--------|---------|---------| | 멀티테넌시 + i18n 충돌 | 중간 | 높음 | 🔴 P1 | 서브도메인 전략 채택 | | 미들웨어 성능 저하 | 중간 | 중간 | 🟡 P2 | 체이닝, 캐싱 최적화 | | DB 스키마 복잡도 | 낮음 | 중간 | 🟡 P2 | 번역 테이블 패턴 | | 인증 통합 충돌 | 중간 | 중간 | 🟡 P2 | 순서 정의, 테스트 | | 빌드 시간 증가 | 중간 | 낮음 | 🟢 P3 | ISR, 하이브리드 렌더링 | | UI 라이브러리 충돌 | 낮음 | 낮음 | 🟢 P3 | shadcn/ui 선택 | | 상태 관리 복잡도 | 낮음 | 낮음 | 🟢 P3 | Zustand 권장 | --- ## 🚀 즉시 적용 가능한 개선 사항 ### 1. 미들웨어 체이닝 유틸리티 추가 ```typescript // src/lib/middleware-chain.ts import { NextRequest, NextResponse } from 'next/server'; type Middleware = (request: NextRequest) => NextResponse | Promise; export function chainMiddleware(middlewares: Middleware[]) { return async (request: NextRequest) => { let response = NextResponse.next(); for (const middleware of middlewares) { response = await middleware(request); // 리다이렉트나 에러 응답 시 체인 중단 if (response.status !== 200) { return response; } } return response; }; } ``` ### 2. 환경 변수 검증 ```typescript // src/lib/env.ts import { z } from 'zod'; const envSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']), DATABASE_URL: z.string().url(), NEXTAUTH_SECRET: z.string().min(32), NEXTAUTH_URL: z.string().url(), }); export const env = envSchema.parse(process.env); ``` ### 3. 타입 안전성 강화 ```typescript // src/types/tenant.ts export type TenantId = string & { readonly __brand: 'TenantId' }; export function createTenantId(id: string): TenantId { return id as TenantId; } // 사용 예 const tenantId = createTenantId('acme-corp'); // 일반 string과 혼용 불가 → 타입 안전성 ``` --- ## 📞 의사결정이 필요한 사항 ### 즉시 결정 필요 (개발 시작 전) 1. **멀티 테넌시 전략** - [ ] 서브도메인 방식 (`{tenant}.domain.com`) - [ ] URL 기반 방식 (`/[tenant]`) - [ ] 하이브리드 (개발: URL, 프로덕션: 서브도메인) 2. **데이터베이스** - [ ] PostgreSQL - [ ] MySQL - [ ] Supabase (PostgreSQL + Auth) 3. **인증 시스템** - [ ] NextAuth.js (오픈소스) - [ ] Clerk (상용) - [ ] Supabase Auth - [ ] 자체 구현 4. **배포 플랫폼** - [ ] Vercel - [ ] AWS - [ ] Google Cloud - [ ] Azure ### 개발 중 결정 가능 5. **UI 컴포넌트 라이브러리** 6. **상태 관리 라이브러리** 7. **차트 라이브러리** (Recharts, Chart.js 등) ### ✅ 이미 결정됨 - **폼 라이브러리**: React Hook Form + Zod (타입 안전성, 성능, 다국어 지원) --- ## 🎯 결론 및 권장사항 ### ✅ 현재 기반 설정은 프로덕션 준비 완료 현재 구성된 **Next.js 15.5.6 + Laravel Sanctum + next-intl + React Hook Form + Zod + TypeScript** 기반은 **멀티 테넌트 ERP 시스템 개발에 최적화**되었습니다. **주요 강점**: - ✅ Next.js 15.5.6: 안정적이고 검증된 버전 (middleware 경고 없음) - ✅ Laravel Sanctum: 토큰 기반 인증으로 프론트엔드/백엔드 완전 분리 - ✅ next-intl 4.4.0: 다국어 지원 완벽 통합 - ✅ React Hook Form + Zod: 타입 안전한 폼 관리 및 유효성 검증 - ✅ React 19.2.0: 최신 기능 활용 가능 - ✅ Tailwind CSS 4.x: 최신 스타일링 시스템 ### ⚠️ 주의가 필요한 영역 1. **멀티테넌시 URL 구조** → 서브도메인 방식 권장 2. **미들웨어 복잡도 관리** → 체이닝 패턴 도입 필요 3. **Laravel API 엔드포인트 설정** → 환경 변수 구성 필수 ### 🚦 진행 가능 여부 **판정**: ✅ **즉시 진행 가능** **충족 조건**: - ✅ 안정적인 기술 스택 (Next.js 15.5.6) - ✅ 명확한 아키텍처 분리 (Frontend/Backend) - ✅ 다국어 지원 구조 완성 - ✅ 인증 플로우 설계 완료 **진행 전 결정 필요**: - 멀티 테넌시 전략 (서브도메인 vs URL 기반) - Laravel API URL 환경 변수 설정 ### 📋 Next Steps 1. **즉시**: 멀티 테넌시 전략 결정 + Laravel API URL 설정 2. **1주차**: 미들웨어 체이닝 구현 + 환경 변수 구성 3. **2주차**: Laravel API 통합 테스트 + 인증 플로우 검증 4. **3주차**: 첫 ERP 모듈 구현 시작 5. **4주차**: UI 컴포넌트 라이브러리 통합 (shadcn/ui 권장) --- **문서 유효기간**: 2025-11-06 ~ 2025-12-06 (1개월) **다음 리뷰**: 설계 가이드 통합 후 또는 주요 아키텍처 변경 시 **작성자**: Claude Code **승인 필요**: 프로젝트 매니저, 시니어 개발자