ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • frontend/CLAUDE.md
    실제 경험과 인사이트를 AI와 함께 정리한 글 2025. 10. 27. 12:02

     

    imprun.dev Console 개발 가이드

    새로운 세션에서도 일관된 개발을 위한 프론트엔드 아키텍처 및 코딩 규칙

    DESIGN_GUIDELINES.md
    0.01MB

    목차

    1. 기술 스택
    2. 아키텍처 패턴
    3. 프로젝트 구조
    4. 페이지 작성 규칙
    5. 컴포넌트 작성 규칙
    6. Hooks 패턴
    7. 타입 시스템
    8. 스타일링 & 디자인 시스템
    9. API 서비스 패턴
    10. 새 기능 추가 가이드
    11. 코드 리뷰 체크리스트

    기술 스택

    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으로 추상화)

    컴포넌트 작성 규칙

    컴포넌트 분리 기준

    다음 조건을 만족하면 별도 컴포넌트로 분리:

    1. 재사용 가능: 2곳 이상에서 사용
    2. 복잡도: 100줄 이상
    3. 독립 로직: 자체 상태 관리 필요
    4. 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-export

    2. 타입 정의 예시

    // 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">       // X

    2. 페이지 레이아웃 패턴

    <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 httpClient

    Service 작성 원칙

    • 도메인별 분리: 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 사용?
    • 아이콘에 size prop 사용?
    • 반응형 디자인 적용?

    API

    • Service Layer 사용?
    • 에러 처리가 적절한가?
    • Toast 알림 제공?

    일반

    • TypeScript 타입 명시?
    • null/undefined 명시적 체크? (strictNullChecks: false)
    • Phase 기반 UI 제어?

    참고 자료

    코드 예시


    마지막 원칙

    "일관성이 완벽함보다 중요하다. 기존 코드 패턴을 따르라."

    새로운 세션에서 작업 시:

    1. 기존 유사 기능 찾기
    2. 패턴 파악 후 동일하게 적용
    3. 의문점은 이 가이드 참조
    4. 가이드에 없으면 기존 코드 우선
Designed by Tistory.