-
frontend/CLAUDE.md실제 경험과 인사이트를 AI와 함께 정리한 글 2025. 10. 27. 12:02
imprun.dev Console 개발 가이드
새로운 세션에서도 일관된 개발을 위한 프론트엔드 아키텍처 및 코딩 규칙
DESIGN_GUIDELINES.md0.01MB
목차
- 기술 스택
- 아키텍처 패턴
- 프로젝트 구조
- 페이지 작성 규칙
- 컴포넌트 작성 규칙
- Hooks 패턴
- 타입 시스템
- 스타일링 & 디자인 시스템 ⭐
- API 서비스 패턴
- 새 기능 추가 가이드
- 코드 리뷰 체크리스트
기술 스택
Core
- React 19: UI 라이브러리
- react-router-dom v6: 클라이언트 사이드 라우팅
- TypeScript:
strictNullChecks: false,noImplicitAny: false - Vite: 빌드 도구
상태 관리
- Zustand: 전역 상태 (auth, ui)
- TanStack Query v5: 서버 상태 관리
UI
- Tailwind CSS v4: 유틸리티 기반 스타일링
- shadcn/ui: UI 컴포넌트 라이브러리
- lucide-react: 아이콘
기타
- Monaco Editor: 코드 에디터
- React Hook Form + Zod: 폼 관리 및 검증
- Recharts: 차트
- Sonner: 토스트 알림
- axios: HTTP 클라이언트
배포
- nginx: 정적 파일 서빙
- Docker: 컨테이너화
- Kubernetes: 오케스트레이션
아키텍처 패턴
핵심 철학
"프레임워크는 UI 레이어에만 영향을 줘야 한다. 비즈니스 로직과 데이터 레이어는 독립적이어야 한다."
이 프로젝트는 프레임워크에 독립적인 아키텍처를 채택하여:
- ✅ 프레임워크 마이그레이션 용이성 확보
- ✅ 테스트 가능성 향상
- ✅ 재사용성 극대화
- ✅ 유지보수성 개선
1. Container/Presentational Pattern
데이터 로직과 UI 렌더링을 완전히 분리합니다.
// ✅ Container Component (데이터 + 로직) export function ApplicationListContainer() { // 데이터 페칭 (Hook Layer) const { data: applications, isLoading } = useApplications() // 비즈니스 로직 const activeApps = applications?.filter(app => app.phase !== 'Deleted') // Presentational Component에 데이터 전달 return <ApplicationList applications={activeApps} isLoading={isLoading} /> } // ✅ Presentational Component (렌더링만) interface ApplicationListProps { applications: TApplicationDetail[] isLoading: boolean } export function ApplicationList({ applications, isLoading }: ApplicationListProps) { if (isLoading) return <Spinner /> return ( <div className="grid grid-cols-3 gap-4"> {applications.map(app => ( <ApplicationCard key={app.appid} app={app} /> ))} </div> ) }장점:
- 테스트 용이: Presentational 컴포넌트는 props만 검증
- 재사용성: 동일한 UI를 다른 데이터 소스에 연결 가능
- 프레임워크 독립성: 렌더링 로직은 라우터 변경에 영향 없음
2. Layered Architecture (4계층)
┌──────────────────────────────────────┐ │ Component Layer │ ← UI 렌더링 │ - React 컴포넌트 │ │ - 사용자 인터랙션 │ │ - UI 상태 (useState) │ ├──────────────────────────────────────┤ │ Hook Layer │ ← 비즈니스 로직 │ - TanStack Query (use-*.ts) │ │ - 로직 Hook (use-*-control.ts) │ │ - 서버 상태 관리 │ ├──────────────────────────────────────┤ │ Service Layer │ ← API 인터페이스 │ - *.service.ts │ │ - 도메인별 API 그룹화 │ │ - 요청/응답 변환 │ ├──────────────────────────────────────┤ │ HTTP Client Layer │ ← 네트워크 통신 │ - axios (httpClient.ts) │ │ - 인증 토큰 주입 │ │ - 공통 에러 처리 │ └──────────────────────────────────────┘3. 실제 데이터 흐름
// 1️⃣ Component Layer export function ApplicationCard({ app }: { app: TApplicationDetail }) { const { actions, state } = useApplicationControl(app) // Hook 호출 return ( <Card> <Button onClick={actions.start} disabled={!state.canStart}> 시작 </Button> </Card> ) } // 2️⃣ Hook Layer (use-application-control.ts) export function useApplicationControl(app: TApplicationDetail) { const queryClient = useQueryClient() const start = async () => { await applicationService.start(app.appid) // Service 호출 queryClient.invalidateQueries({ queryKey: ['applications'] }) toast.success("시작했습니다") } return { actions: { start }, state: { canStart: app.phase === 'Stopped' } } } // 3️⃣ Service Layer (application.service.ts) export const applicationService = { async start(appid: string): Promise<void> { await httpClient.post(`/v1/applications/${appid}/start`) // HTTP 호출 } } // 4️⃣ HTTP Client Layer (httpclient.ts) const httpClient = axios.create({ baseURL: env.API_URL, timeout: 10000, }) httpClient.interceptors.request.use((config) => { const token = useAuthStore.getState().token if (token) config.headers.Authorization = `Bearer ${token}` return config })4. 프레임워크 독립성
프레임워크 변경 시 영향 범위:
- 변경 필요 (5%): 라우팅, 페이지 구조
- 변경 불필요 (95%): Hooks, Services, Components, Store
프로젝트 구조
frontend/ └── src/ ├── pages/ # 페이지 컴포넌트 │ ├── Home.tsx │ ├── Applications.tsx │ ├── ApplicationDetail.tsx │ └── FunctionEditor.tsx │ ├── routes/ # 라우트 정의 │ └── index.tsx # react-router-dom 설정 │ ├── components/ │ ├── layout/ # 레이아웃 컴포넌트 │ │ ├── AppBar.tsx │ │ ├── MainNavBar.tsx │ │ └── AppNavBar.tsx │ ├── applications/ # 도메인별 컴포넌트 │ ├── functions/ │ ├── database/ │ ├── triggers/ │ ├── modals/ # 모달 컴포넌트 │ └── ui/ # shadcn/ui 컴포넌트 │ ├── hooks/ # TanStack Query + 로직 훅 │ ├── use-applications.ts │ ├── use-functions.ts │ └── use-application-control.ts │ ├── services/ # API 서비스 │ ├── application.service.ts │ ├── function.service.ts │ └── database.service.ts │ ├── store/ # Zustand 스토어 │ ├── auth.ts │ └── ui.ts │ ├── types/ # 도메인별 타입 │ ├── application.ts │ ├── function.ts │ ├── common.ts │ └── index.ts # ⚠️ 필수 re-export │ └── lib/ # 유틸리티 ├── httpclient.ts └── env.ts레이아웃 계층 구조
┌──────────────────────────────────────────┐ │ AppBar (전역) │ ← 모든 페이지 │ Logo + Breadcrumb [User Menu] │ ├──────────────────────────────────────────┤ │ MainNavBar (메인 영역) │ ← /projects, /billing │ 프로젝트 | 빌링 | 설정 │ ├──────────────────────────────────────────┤ │ AppNavBar (Application 영역) │ ← /applications/:appid/* │ 개요 | Functions | Database | ... │ ├──────────────────────────────────────────┤ │ Page Content │ └──────────────────────────────────────────┘
페이지 작성 규칙
React Router DOM 기반 페이지
// src/routes/index.tsx import { createBrowserRouter } from 'react-router-dom' export const router = createBrowserRouter([ { path: '/', element: <RootLayout />, children: [ { index: true, element: <HomePage /> }, { path: 'applications', children: [ { index: true, element: <ApplicationList /> }, { path: ':appid', element: <ApplicationLayout />, children: [ { index: true, element: <ApplicationOverview /> }, { path: 'functions', element: <FunctionList /> }, { path: 'functions/:name/edit', element: <FunctionEditor /> }, ], }, ], }, ], }, ])페이지 컴포넌트 패턴
// src/pages/ApplicationList.tsx import { useApplications } from '@/hooks/use-applications' import { ApplicationCard } from '@/components/applications/ApplicationCard' import { CreateApplicationModal } from '@/components/modals/CreateApplicationModal' export function ApplicationList() { // 1. 데이터 페칭 (Hook Layer) const { data: applications, isLoading } = useApplications() // 2. 로딩 처리 if (isLoading) return <Spinner /> // 3. 페이지 구조 정의 return ( <div className="p-6 h-full overflow-auto"> <div className="max-w-7xl mx-auto flex flex-col gap-6"> {/* 헤더 */} <div className="flex items-center justify-between"> <h1 className="text-2xl font-bold">Applications</h1> <CreateApplicationModal /> </div> {/* 메인 콘텐츠 */} {applications.length === 0 ? ( <EmptyState /> ) : ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> {applications.map(app => ( <ApplicationCard key={app.appid} app={app} /> ))} </div> )} </div> </div> ) }페이지 작성 원칙
✅ 해야 할 것:
- Hook으로 데이터 페칭
- 컴포넌트 조합으로 UI 구성
- 페이지 레벨 레이아웃 정의
❌ 하지 말아야 할 것:
- 페이지에 비즈니스 로직 직접 작성 (Hook으로 분리)
- 인라인 API 호출 (Service Layer 사용)
- 복잡한 상태 관리 (Hook으로 추상화)
컴포넌트 작성 규칙
컴포넌트 분리 기준
다음 조건을 만족하면 별도 컴포넌트로 분리:
- 재사용 가능: 2곳 이상에서 사용
- 복잡도: 100줄 이상
- 독립 로직: 자체 상태 관리 필요
- Client 인터랙션: 이벤트 핸들러 필요
컴포넌트 배치 규칙
src/components/ ├── layout/ # 레이아웃 (AppBar, NavBar) ├── applications/ # Application 도메인 ├── functions/ # Function 도메인 ├── database/ # Database 도메인 ├── triggers/ # Trigger 도메인 ├── modals/ # 모든 모달 └── ui/ # shadcn/ui 컴포넌트컴포넌트 작성 패턴
// src/components/applications/ApplicationCard.tsx import { Card, CardHeader, CardContent } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { useApplicationControl } from "@/hooks/use-application-control" import type { TApplicationDetail } from "@/types" interface ApplicationCardProps { app: TApplicationDetail } export function ApplicationCard({ app }: ApplicationCardProps) { // Hook으로 로직 추상화 const { actions, state } = useApplicationControl(app) return ( <Card> <CardHeader> <h3 className="text-lg font-semibold">{app.name}</h3> <StatusBadge phase={app.phase} /> </CardHeader> <CardContent> <div className="flex gap-2"> <Button onClick={actions.start} disabled={!state.canStart || state.isActioning} size="sm" > 시작 </Button> <Button onClick={actions.stop} disabled={!state.canStop || state.isActioning} size="sm" variant="secondary" > 정지 </Button> </div> </CardContent> </Card> ) }
Hooks 패턴
1. Data Hooks (TanStack Query)
// src/hooks/use-applications.ts import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { applicationService } from "@/services/application.service" import type { TApplicationDetail, TCreateApplicationDto } from "@/types" // 조회 export function useApplications() { return useQuery({ queryKey: ["applications"], queryFn: () => applicationService.getApplications(), }) } export function useApplication(appid: string) { return useQuery({ queryKey: ["applications", appid], queryFn: () => applicationService.getApplication(appid), enabled: !!appid, }) } // 생성 export function useCreateApplication() { const queryClient = useQueryClient() return useMutation({ mutationFn: (data: TCreateApplicationDto) => applicationService.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["applications"] }) toast.success("생성되었습니다") }, onError: (error: any) => { toast.error(error.response?.data?.message || "생성 실패") }, }) }2. Logic Hooks (비즈니스 로직)
// src/hooks/use-application-control.ts import { useState } from "react" import { useQueryClient } from "@tanstack/react-query" import { applicationService } from "@/services/application.service" import { ApplicationPhase } from "@/types" import { toast } from "sonner" export function useApplicationControl(app: TApplicationDetail | null) { const [isActioning, setIsActioning] = useState(false) const queryClient = useQueryClient() // 상태 계산 const canStart = app?.phase === ApplicationPhase.Stopped const canStop = app?.phase === ApplicationPhase.Started // 액션 정의 const start = async () => { if (!app) return setIsActioning(true) try { await applicationService.start(app.appid) queryClient.invalidateQueries({ queryKey: ["applications"] }) toast.success("시작했습니다") } catch (error: any) { toast.error(error.response?.data?.message || "시작 실패") } finally { setIsActioning(false) } } const stop = async () => { // 동일한 패턴 } return { actions: { start, stop }, state: { canStart, canStop, isActioning }, } }Hooks 작성 원칙
- Data Hooks: TanStack Query 사용, 캐시 관리
- Logic Hooks: 재사용 가능한 비즈니스 로직
- 명명 규칙:
use-도메인명.ts(Data),use-도메인명-동사.ts(Logic)
타입 시스템
1. 도메인별 타입 분리
src/types/ ├── application.ts # Application 도메인 ├── function.ts # Function 도메인 ├── database.ts # Database 도메인 ├── trigger.ts # Trigger 도메인 ├── common.ts # 공통 타입 └── index.ts # ⚠️ 필수 re-export2. 타입 정의 예시
// src/types/application.ts export enum ApplicationPhase { Starting = "Starting", Started = "Started", Stopping = "Stopping", Stopped = "Stopped", } export type TApplicationDetail = { _id: string appid: string name: string phase: string // ApplicationPhase state: string region: string createdAt: string updatedAt: string } export type TCreateApplicationDto = { name: string region: string }3. 중앙 re-export (필수!)
// src/types/index.ts export * from "./application" export * from "./function" export * from "./database" export * from "./trigger" export * from "./common"4. 타입 Import 규칙
// ✅ 중앙 re-export 사용 import { TApplicationDetail, ApplicationPhase } from "@/types" // ❌ 개별 파일 직접 import 금지 import { TApplicationDetail } from "@/types/application"5. State vs Phase
- State: 사용자가 설정한 목표 상태 (Running, Stopped)
- Phase: 실제 시스템 상태 (Starting, Started, Stopping, Stopped)
// ✅ Phase 기반 UI 제어 const canStart = app.phase === ApplicationPhase.Stopped const canStop = app.phase === ApplicationPhase.Started const isTransitioning = ["Starting", "Stopping"].includes(app.phase)6. 주의사항
.ts사용 (.d.ts아님): enum은 런타임 코드- TypeScript 설정:
strictNullChecks: false→ null/undefined 명시적 체크 필수
스타일링 & 디자인 시스템
⭐ 중요: 상세한 UI/UX 가이드라인은 DESIGN_GUIDELINES.md 참조
디자인 시스템에는 다음이 포함되어 있습니다:
- ✨ 일관된 타이포그래피 (그라디언트 헤더, semantic colors)
- 🎨 세련된 카드 디자인 (border-2, hover 효과, 그라디언트 아이콘)
- 🎯 인터랙티브 애니메이션 (duration-300, -translate-y-1)
- 📱 반응형 레이아웃 (grid, gap-8, md:, lg:)
- 🌙 다크모드 자동 지원 (semantic colors 사용)
아래는 기본 스타일링 규칙입니다. 새 페이지/컴포넌트 작성 시 반드시 DESIGN_GUIDELINES.md를 함께 참조하세요.
1. Tailwind 기본 패턴
// ✅ flex + gap 사용 <div className="flex flex-col gap-6"> <div className="flex items-center gap-2"> ... </div> </div> // ❌ margin 사용 금지 <div className="space-y-4"> // X <div className="mb-4"> // X2. 페이지 레이아웃 패턴
<div className="p-6 h-full overflow-auto"> <div className="max-w-7xl mx-auto flex flex-col gap-6"> {/* 헤더 */} <div className="flex items-center justify-between"> <h1 className="text-2xl font-bold">Title</h1> <Button>Action</Button> </div> {/* 콘텐츠 */} <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> {items.map(item => <Card key={item.id} />)} </div> </div> </div>3. 색상 시스템 (Semantic Colors)
// ✅ Tailwind semantic colors 사용 text-foreground bg-background border-border bg-primary text-primary-foreground bg-muted text-muted-foreground // ❌ 하드코딩된 색상 금지 text-gray-900 bg-white border-gray-200이유: 다크모드 자동 지원
4. 아이콘 사용
import { Settings, Info, AlertCircle } from "lucide-react" // ✅ size prop 사용 <Settings size={16} /> // 기본 (대부분) <Info size={14} /> // 버튼 내부 <Settings size={20} /> // 헤더 // ❌ className 크기 금지 <Settings className="h-4 w-4" />5. 반응형 디자인
// ✅ Tailwind breakpoints 사용 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> {/* 모바일: 1열, 태블릿: 2열, 데스크톱: 3열 */} </div>
API 서비스 패턴
Service Layer 구조
// src/services/application.service.ts import { httpClient } from "@/lib/httpclient" import type { TApplicationDetail, TCreateApplicationDto, TUpdateApplicationDto, } from "@/types" export const applicationService = { // 조회 async getApplications(): Promise<TApplicationDetail[]> { return await httpClient.get("/v1/applications") }, async getApplication(appid: string): Promise<TApplicationDetail> { return await httpClient.get(`/v1/applications/${appid}`) }, // 생성 async create(dto: TCreateApplicationDto): Promise<TApplicationDetail> { return await httpClient.post("/v1/applications", dto) }, // 수정 async update( appid: string, dto: TUpdateApplicationDto ): Promise<TApplicationDetail> { return await httpClient.patch(`/v1/applications/${appid}`, dto) }, // 삭제 async delete(appid: string): Promise<void> { await httpClient.delete(`/v1/applications/${appid}`) }, // 액션 async start(appid: string): Promise<void> { await httpClient.post(`/v1/applications/${appid}/start`) }, async stop(appid: string): Promise<void> { await httpClient.post(`/v1/applications/${appid}/stop`) }, }HTTP Client 설정
// src/lib/httpclient.ts import axios from "axios" import { useAuthStore } from "@/store/auth" import { env } from "./env" const httpClient = axios.create({ baseURL: env.API_URL, timeout: 10000, }) // 요청 인터셉터: 토큰 주입 httpClient.interceptors.request.use((config) => { const token = useAuthStore.getState().token if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 응답 인터셉터: 공통 에러 처리 httpClient.interceptors.response.use( (response) => response.data, (error) => { if (error.response?.status === 401) { useAuthStore.getState().logout() window.location.href = "/login" } return Promise.reject(error) } ) export default httpClientService 작성 원칙
- 도메인별 분리:
application.service.ts,function.service.ts - 명명 규칙:
도메인명.service.ts - 타입 명시: 요청/응답 타입 명확히
- 에러 처리: HTTP Client에서 일괄 처리
새 기능 추가 가이드
1. 라우트 추가
// src/routes/index.tsx { path: ':appid', element: <ApplicationLayout />, children: [ // ... 기존 라우트 { path: '새기능', element: <새기능Page /> }, ], }2. NavBar 업데이트
// src/components/layout/AppNavBar.tsx const navItems = [ // ... 기존 항목 { label: "새기능", href: "새기능", icon: YourIcon }, ]3. 타입 정의
// src/types/새기능.ts export type T새기능 = { id: string name: string // ... } export type TCreate새기능Dto = { name: string // ... } // src/types/index.ts에 추가 export * from "./새기능"4. Service 작성
// src/services/새기능.service.ts import { httpClient } from "@/lib/httpclient" import type { T새기능, TCreate새기능Dto } from "@/types" export const 새기능Service = { async getList(appid: string): Promise<T새기능[]> { return await httpClient.get(`/v1/apps/${appid}/새기능`) }, async create(appid: string, dto: TCreate새기능Dto): Promise<T새기능> { return await httpClient.post(`/v1/apps/${appid}/새기능`, dto) }, }5. Hook 작성
// src/hooks/use-새기능.ts import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { 새기능Service } from "@/services/새기능.service" export function use새기능s(appid: string) { return useQuery({ queryKey: ["새기능", appid], queryFn: () => 새기능Service.getList(appid), enabled: !!appid, }) } export function useCreate새기능(appid: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: (data: TCreate새기능Dto) => 새기능Service.create(appid, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["새기능", appid] }) }, }) }6. 컴포넌트 작성
// src/components/새기능/새기능List.tsx import type { T새기능 } from "@/types" interface 새기능ListProps { items: T새기능[] appid: string } export function 새기능List({ items, appid }: 새기능ListProps) { return ( <div className="grid grid-cols-3 gap-4"> {items.map(item => ( <새기능Card key={item.id} item={item} appid={appid} /> ))} </div> ) }7. 페이지 작성
// src/pages/새기능Page.tsx import { useParams } from "react-router-dom" import { use새기능s } from "@/hooks/use-새기능" import { 새기능List } from "@/components/새기능/새기능List" import { Create새기능Modal } from "@/components/modals/Create새기능Modal" export function 새기능Page() { const { appid } = useParams<{ appid: string }>() const { data: items, isLoading } = use새기능s(appid!) if (isLoading) return <Spinner /> return ( <div className="p-6 h-full overflow-auto"> <div className="max-w-7xl mx-auto flex flex-col gap-6"> <div className="flex items-center justify-between"> <h1 className="text-2xl font-bold">새기능</h1> <Create새기능Modal appid={appid!} /> </div> <새기능List items={items} appid={appid!} /> </div> </div> ) }
코드 리뷰 체크리스트
아키텍처
- Container/Presentational 패턴 준수?
- 4계층 구조 유지? (Component → Hook → Service → HTTP Client)
- 비즈니스 로직이 Hook Layer에 있는가?
- 페이지 컴포넌트가 단순 래퍼가 아닌가?
컴포넌트
- 컴포넌트가 도메인별 폴더에 위치?
- Presentational 컴포넌트가 props만 받는가?
- 100줄 이상이면 분리 검토했는가?
Hooks
- Data Hook은 TanStack Query 사용?
- Logic Hook은 재사용 가능한가?
- Mutation 성공 시 Query Invalidation?
타입
- 타입이 도메인별 파일에 정의?
-
types/index.ts에서 re-export? - 중앙 re-export로 import?
스타일링
-
flex + gap사용,margin회피? - Semantic colors 사용?
- 아이콘에
sizeprop 사용? - 반응형 디자인 적용?
API
- Service Layer 사용?
- 에러 처리가 적절한가?
- Toast 알림 제공?
일반
- TypeScript 타입 명시?
- null/undefined 명시적 체크? (
strictNullChecks: false) - Phase 기반 UI 제어?
참고 자료
코드 예시
- 페이지: src/pages/Applications.tsx
- 컴포넌트: src/components/applications/ApplicationCard.tsx
- Hook: src/hooks/use-application-control.ts
- Service: src/services/application.service.ts
- 타입: src/types/application.ts
- 디자인: DESIGN_GUIDELINES.md - ⭐ 필수 참조
마지막 원칙
"일관성이 완벽함보다 중요하다. 기존 코드 패턴을 따르라."
새로운 세션에서 작업 시:
- 기존 유사 기능 찾기
- 패턴 파악 후 동일하게 적용
- 의문점은 이 가이드 참조
- 가이드에 없으면 기존 코드 우선
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
Kubernetes에서 특권 포트 피하기: NodePort + iptables 포워딩 패턴 (0) 2025.10.27 Kubernetes Gateway API 실전 가이드: Kong Ingress에서 표준 API로 전환하기 (0) 2025.10.27 Claude AI와 함께하는 프론트엔드 개발: imprun.dev의 CLAUDE.md 가이드 공개 (0) 2025.10.27 Next.js를 버리고 순수 React로 돌아온 이유: 실무 관점의 프레임워크 선택 여정 (0) 2025.10.27 Sequential Thinking MCP: AI의 구조화된 사고 프로세스 (0) 2025.10.27