-
AI Agent를 위한 Frontend 개발 가이드: AGETNTS.md 로 Next.js + shadcn/ui 프로젝트 구조 설계실제 경험과 인사이트를 AI와 함께 정리한 글 2025. 11. 24. 13:59
AI Agent를 위한 Frontend 개발 가이드: AGENTS.md로 Next.js + shadcn/ui 프로젝트 구조 설계
작성일: 2025년 11월 24일
카테고리: Frontend, AI, Architecture
키워드: Next.js, React, AI Agent, AGENTS.md, CLAUDE.md, shadcn/ui, TanStack Query, Zustand요약
AI 코딩 어시스턴트(Claude, Copilot 등)와 협업할 때 프로젝트 구조를 명확히 문서화하면 AI가 일관된 코드를 생성합니다. 이 글에서는 Next.js 16 + shadcn/ui 기반 프로젝트에서 AI Agent가 따라야 할 아키텍처 패턴, 네이밍 컨벤션, 데이터 흐름을 정의하는 방법을 공유합니다. 실제 B2B API Gateway 콘솔 프로젝트에서 사용 중인 가이드 문서입니다.
문제 상황
배경
AI 코딩 어시스턴트와 함께 프론트엔드를 개발할 때 다음 문제들이 발생합니다:
- AI가 매번 다른 파일 구조와 네이밍 컨벤션을 사용
- API 호출 로직이 컴포넌트, 훅, 서비스 등 여기저기 흩어짐
- 상태 관리 방식이 일관되지 않음 (useState, Zustand, Context 혼용)
- 새 기능 추가 시 어디에 코드를 배치해야 할지 AI가 판단하지 못함
환경 구성
- Framework: Next.js 16.0.3 (App Router)
- React: 19.0.0
- TypeScript: 5.7.2
- Styling: Tailwind CSS 4.0.0
- UI Components: shadcn/ui (new-york style)
- State Management: Zustand 5.0.2
- Server State: TanStack Query 5.x
- Forms: React Hook Form + Zod
- Auth: Keycloak (OIDC PKCE)
해결 과정: AI Agent 가이드 문서 설계
1. 계층 아키텍처 정의
AI가 코드를 배치할 위치를 명확히 알 수 있도록 계층 구조를 정의합니다:
src/ ├── types/ # TypeScript interfaces & types only ├── services/ # API Layer (object-based services) ├── hooks/ # React Query hooks (domain-specific) ├── store/ # Zustand stores (global UI state) ├── features/ # Feature-Sliced Design modules ├── components/ # Shared UI components ├── lib/ # Utilities (NOT API calls) └── app/ # Pages (UI composition only)2. 데이터 흐름 시각화
AI가 데이터 흐름을 이해할 수 있도록 다이어그램으로 표현합니다:
┌─────────────────────────────────────────────────────────────┐ │ Page (app/) │ │ - UI composition only │ │ - Imports from features/ and hooks/ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Features (features/) │ │ - Domain-specific components │ │ - Uses hooks/ for data, components/ for UI primitives │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Hooks (hooks/) │ │ - React Query hooks (useQuery, useMutation) │ │ - Calls services/ for API operations │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Services (services/) │ │ - API layer (object-based) │ │ - Uses lib/api/client.ts for HTTP requests │ └─────────────────────────────────────────────────────────────┘3. 네이밍 컨벤션 명시
일관된 네이밍을 위해 규칙을 표로 정리합니다:
Item Convention Example Hooks use-*.tsuse-gateways.ts,use-products.tsServices *.tsgateway.ts,product.tsTypes *.tsgateway.ts,product.tsComponents *.tsxgateway-card.tsx,product-form.tsxUtilities *.tsformat-date.ts,cn.tsFeature folders kebab-caseapi-services/,client-apps/핵심 원칙: 모든 파일은
kebab-case를 사용합니다.4. Service 패턴 표준화
API 호출은 모두 service 객체를 통해 수행합니다:
// services/gateway.ts import { apiClient } from '@/lib/api/client'; import type { Gateway, CreateGatewayRequest } from '@/types/gateway'; const BASE_PATH = '/v1/provider/gateways'; export const gatewayService = { /** * GET /v1/provider/gateways - List all gateways */ list: async (): Promise<Gateway[]> => { const { data } = await apiClient.get<{ gateways: Gateway[] }>(BASE_PATH); return data.gateways || []; }, /** * GET /v1/provider/gateways/:id - Get single gateway */ get: async (id: string): Promise<Gateway> => { const { data } = await apiClient.get<{ gateway: Gateway }>(`${BASE_PATH}/${id}`); return data.gateway; }, /** * POST /v1/provider/gateways - Create gateway */ create: async (request: CreateGatewayRequest): Promise<Gateway> => { const { data } = await apiClient.post<{ gateway: Gateway }>(BASE_PATH, request); return data.gateway; }, /** * DELETE /v1/provider/gateways/:id - Delete gateway */ delete: async (id: string): Promise<void> => { await apiClient.delete(`${BASE_PATH}/${id}`); }, };5. React Query Hook 패턴 표준화
서비스를 호출하는 React Query 훅을 표준화합니다:
// hooks/use-gateways.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { gatewayService } from '@/services'; import type { CreateGatewayRequest } from '@/types/gateway'; // Query keys for cache management export const gatewayKeys = { all: ['gateways'] as const, list: () => [...gatewayKeys.all, 'list'] as const, detail: (id: string) => [...gatewayKeys.all, 'detail', id] as const, }; /** * Hook to fetch all gateways */ export function useGateways() { return useQuery({ queryKey: gatewayKeys.list(), queryFn: () => gatewayService.list(), }); } /** * Hook to create gateway */ export function useCreateGateway() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (request: CreateGatewayRequest) => gatewayService.create(request), onSuccess: () => { queryClient.invalidateQueries({ queryKey: gatewayKeys.all }); }, }); }6. Feature-Sliced Design 패턴
각 도메인별로 자체 컴포넌트를 가진 feature 모듈을 구성합니다:
features/<feature-name>/ ├── components/ │ ├── <feature>-card.tsx # Display component │ ├── <feature>-form.tsx # Form component │ ├── <feature>-table.tsx # Table component │ ├── create-<feature>-dialog.tsx # Create dialog │ └── edit-<feature>-dialog.tsx # Edit dialog └── index.ts # Public exports only// features/gateways/index.ts export { GatewayCard } from './components/gateway-card'; export { GatewayForm } from './components/gateway-form'; export { GatewayTable } from './components/gateway-table'; export { CreateGatewayDialog } from './components/create-gateway-dialog';7. 상태 관리 가이드라인
상태 종류별로 어떤 도구를 사용할지 명확히 정의합니다:
상태 종류 도구 용도 Server State TanStack Query API 데이터 (자동 캐싱, 리페칭) Global UI State Zustand 사이드바 상태 등 전역 UI 상태 URL State nuqs 필터, 검색, 페이지네이션 Local State useState 다이얼로그 open/close 등 // Server State const { data, isLoading } = useGateways(); // Global UI State (Zustand) const { isOpen, toggle } = useSidebarStore(); // URL State (nuqs) const [search, setSearch] = useQueryState('q'); // Local State const [isDialogOpen, setIsDialogOpen] = useState(false);8. 새 기능 추가 체크리스트
AI가 새 기능을 추가할 때 따라야 할 순서를 정의합니다:
- Create Types (
types/my-feature.ts) - Create Service (
services/my-feature.ts) - Create Hooks (
hooks/use-my-feature.ts) - Create Feature Components (
features/my-feature/) - Create Page (
app/(admin)/my-feature/page.tsx) - Add to Sidebar (
components/layout/app-sidebar.tsx)
프로젝트 구조 전체 예시
frontend/ ├── src/ │ ├── app/ # Next.js App Router pages │ │ ├── layout.tsx # Root layout (providers, theme) │ │ ├── (auth)/ # Auth route group (no sidebar) │ │ │ ├── login/page.tsx │ │ │ └── callback/page.tsx │ │ ├── (admin)/ # Admin/Provider route group │ │ │ ├── layout.tsx # AppSidebar + Header + auth │ │ │ ├── dashboard/ │ │ │ ├── gateways/ │ │ │ └── products/ │ │ └── (portal)/ # Customer DevPortal route group │ │ ├── layout.tsx # PortalSidebar + Header + auth │ │ ├── home/ │ │ └── market/ │ │ │ ├── features/ # Feature-Sliced Design modules │ │ ├── gateways/ │ │ │ ├── components/ │ │ │ └── index.ts │ │ └── products/ │ │ ├── components/ │ │ └── index.ts │ │ │ ├── hooks/ # React Query hooks │ │ ├── index.ts │ │ ├── use-gateways.ts │ │ └── use-products.ts │ │ │ ├── services/ # API services (object-based) │ │ ├── index.ts │ │ ├── gateway.ts │ │ └── product.ts │ │ │ ├── types/ # TypeScript interfaces │ │ ├── index.ts │ │ ├── gateway.ts │ │ └── product.ts │ │ │ ├── components/ # Shared UI components │ │ ├── ui/ # shadcn/ui primitives │ │ ├── layout/ # App layout components │ │ └── forms/ # Form field wrappers │ │ │ ├── store/ # Zustand stores │ │ └── sidebar.ts │ │ │ └── lib/ # Utilities │ ├── utils.ts │ ├── api/client.ts │ └── auth/keycloak.ts │ ├── components.json # shadcn/ui configuration ├── tailwind.config.js └── next.config.ts교훈
1. 명시적 규칙이 일관된 AI 출력을 만든다
"API 호출은 services/에서만", "상태 관리는 용도별로 분리" 등 명시적인 규칙을 문서화하면 AI가 일관된 코드를 생성합니다.
2. 코드 예시가 설명보다 효과적
"Service는 object-based 패턴을 사용합니다"라는 설명보다 실제 코드 예시가 AI에게 더 명확한 가이드가 됩니다.
3. 계층 구조 다이어그램이 데이터 흐름을 명확히 한다
텍스트 설명만으로는 복잡한 계층 구조를 전달하기 어렵습니다. ASCII 다이어그램으로 시각화하면 AI가 더 잘 이해합니다.
4. 새 기능 추가 체크리스트가 실수를 방지한다
파일 생성 순서와 필요한 파일 목록을 체크리스트로 제공하면 AI가 필요한 파일을 빠뜨리지 않습니다.
5. 네이밍 컨벤션 표가 혼란을 방지한다
kebab-casevscamelCasevsPascalCase등 네이밍 규칙을 표로 정리하면 AI가 일관된 네이밍을 사용합니다.참고 자료
관련 문서
- Feature-Sliced Design - 프론트엔드 아키텍처 방법론
- TanStack Query - 서버 상태 관리
- Zustand - 클라이언트 상태 관리
기술 스택
- Next.js - React 프레임워크
- shadcn/ui - UI 컴포넌트 라이브러리
- Tailwind CSS - 유틸리티 CSS
부록: 전체 AGENTS.md 원본
에이전트별 설정 방법: Claude Code, Gemini CLI, Codex CLI에서 AGENTS.md를 인식하도록 설정하는 방법은 Claude, Gemini, Codex에서 AGENTS.md 설정하기를 참조하세요.
아래는 실제 프로젝트에서 사용 중인 AI Agent 가이드 문서 전체입니다 (752줄):
# Frontend v2 - AI Agent Development Guide This document provides codebase documentation for the imp-gateway frontend v2 (web/). ## Tech Stack | Category | Technology | Version | |----------|------------|---------| | Framework | Next.js (App Router) | 16.x | | React | React | 19.x | | Language | TypeScript | 5.x | | Styling | Tailwind CSS | 4.x | | UI Components | shadcn/ui (new-york style) | - | | State Management | Zustand | 5.x | | Server State | TanStack Query | 5.x | | Forms | React Hook Form + Zod | - | | Auth | Better Auth + Keycloak (Confidential Client) | - | | Icons | Lucide React | - | | Bundler | Turbopack (via Next.js) | - | --- ## Architecture Overview ### Feature-Sliced Design (FSD) Frontend v2는 FSD 아키텍처를 따릅니다.상위 레이어 → 하위 레이어 의존 가능
하위 레이어 → 상위 레이어 의존 불가app → pages → widgets → features → entities → shared
### FSD Standard Layers | Layer | Purpose | Next.js 통합 | |-------|---------|-------------| | **App** | 라우팅, 엔트리포인트, 글로벌 설정 | `app/` - Next.js App Router | | **Pages** | 페이지 단위 UI 조합 | `pages/` - 페이지별 컴포넌트 | | **Widgets** | 독립적인 대규모 UI 블록 | `widgets/` | | **Features** | 비즈니스 기능 (사용자 액션) | `features/` | | **Entities** | 비즈니스 엔티티 (도메인 모델) | `entities/` | | **Shared** | 재사용 가능한 공용 코드 | `shared/` | ### Layer Structuresrc/
├── app/ # [App] Next.js App Router - 라우팅 엔트리포인트
├── pages/ # [Pages] 페이지 단위 UI 조합 컴포넌트
├── widgets/ # [Widgets] 독립적인 복합 UI 블록
├── features/ # [Features] 비즈니스 기능 단위
├── entities/ # [Entities] 도메인 모델 + API
└── shared/ # [Shared] 공용 UI/유틸리티### Data Flow┌─────────────────────────────────────────────────────────────┐
│ App (app/) │
│ - Next.js App Router (라우팅 엔트리포인트) │
│ - layout.tsx, page.tsx (minimal - pages/ 컴포넌트 호출) │
│ - Providers, global styles │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Pages (pages/) │
│ - 페이지 단위 UI 조합 │
│ - widgets/, features/, entities/ 조합 │
│ - 예: ClustersPage, DashboardPage │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Widgets (widgets/) │
│ - 독립적인 대규모 UI 블록 │
│ - 완전한 유스케이스 전달 (Sidebar, DataTable, Wizard) │
│ - Uses features/, entities/, shared/ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Features (features/) │
│ - 비즈니스 기능 (사용자에게 가치를 제공하는 액션) │
│ - 예: CreateCluster, DeleteProduct, PublishAPI │
│ - Uses entities/, shared/ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Entities (entities/) │
│ - 비즈니스 엔티티 (도메인 모델) │
│ - Types, API services, React Query hooks │
│ - 예: Cluster, Product, Subscription │
│ - Uses shared/ only │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Shared (shared/) │
│ - UI primitives (shadcn/ui) │
│ - Utilities (cn, formatDate) │
│ - API client, Auth config │
│ - 프로젝트/비즈니스와 무관한 재사용 코드 │
└─────────────────────────────────────────────────────────────┘--- ## App vs Pages 구분 ### App Layer (`app/`) Next.js App Router의 라우팅 엔트리포인트. **최소한의 코드만** 포함. ```tsx // app/operator/clusters/page.tsx import { ClustersPage } from "@/pages/operator"; export default function Page() { return <ClustersPage />; }Pages Layer (
pages/)실제 페이지 UI 조합 로직. widgets, features, entities를 조합.
// pages/operator/ClustersPage.tsx "use client"; import { ClusterTable, CreateClusterDialog } from "@/features/clusters"; import { useClusters } from "@/entities/cluster"; import { PageContainer } from "@/widgets/layout"; export function ClustersPage() { const { data: clusters, isLoading } = useClusters(); return ( <PageContainer title="Clusters"> <div className="flex justify-between mb-4"> <h1 className="text-2xl font-bold">Clusters</h1> <CreateClusterDialog /> </div> <ClusterTable data={clusters} isLoading={isLoading} /> </PageContainer> ); }
Naming Convention
All files use
kebab-casefor consistency.Item Convention Example App Router page.tsx,layout.tsxapp/operator/clusters/page.tsxPage Components PascalCase.tsxpages/operator/ClustersPage.tsxComponents PascalCase.tsxClusterCard.tsx,ClusterForm.tsxHooks use-*.tsuse-clusters.ts,use-products.tsServices *.service.tscluster.service.tsTypes *.types.tsortypes.tscluster.types.tsUtilities kebab-case.tsformat-date.ts,cn.ts
Project Structure
web/ ├── src/ │ ├── app/ # [App] Next.js App Router │ │ ├── layout.tsx # Root layout (providers, theme) │ │ ├── page.tsx # Landing (redirect) │ │ │ │ │ ├── (auth)/ # Auth route group │ │ │ └── sign-in/ │ │ │ └── page.tsx # → SignInPage │ │ │ │ │ ├── api/auth/[...all]/ # Better Auth API routes │ │ │ └── route.ts │ │ │ │ │ ├── operator/ # Operator Portal routes │ │ │ ├── layout.tsx # OperatorLayout │ │ │ ├── page.tsx # → DashboardPage │ │ │ ├── clusters/page.tsx # → ClustersPage │ │ │ ├── agents/page.tsx # → AgentsPage │ │ │ └── ... │ │ │ │ │ ├── provider/ # Provider Portal routes │ │ │ ├── layout.tsx # ProviderLayout │ │ │ ├── page.tsx # → DashboardPage │ │ │ └── ... │ │ │ │ │ └── consumer/ # Consumer Portal routes │ │ ├── layout.tsx # ConsumerLayout │ │ ├── page.tsx # → DashboardPage │ │ └── ... │ │ │ ├── pages/ # [Pages] 페이지 UI 조합 │ │ ├── operator/ │ │ │ ├── index.ts # Public exports │ │ │ ├── DashboardPage.tsx │ │ │ ├── ClustersPage.tsx │ │ │ └── AgentsPage.tsx │ │ │ │ │ ├── provider/ │ │ │ ├── index.ts │ │ │ ├── DashboardPage.tsx │ │ │ └── ApiServicesPage.tsx │ │ │ │ │ ├── consumer/ │ │ │ ├── index.ts │ │ │ └── DashboardPage.tsx │ │ │ │ │ └── auth/ │ │ ├── index.ts │ │ └── SignInPage.tsx │ │ │ ├── widgets/ # [Widgets] 독립적 UI 블록 │ │ ├── layout/ │ │ │ ├── index.ts │ │ │ ├── Sidebar.tsx │ │ │ ├── OperatorSidebar.tsx │ │ │ ├── ProviderSidebar.tsx │ │ │ ├── ConsumerSidebar.tsx │ │ │ ├── TenantSwitcher.tsx │ │ │ ├── PortalHeader.tsx │ │ │ └── PageContainer.tsx │ │ │ │ │ └── data-table/ │ │ ├── index.ts │ │ └── DataTable.tsx │ │ │ ├── features/ # [Features] 비즈니스 기능 │ │ ├── clusters/ │ │ │ ├── ui/ │ │ │ │ ├── ClusterCard.tsx │ │ │ │ ├── ClusterTable.tsx │ │ │ │ └── CreateClusterDialog.tsx │ │ │ └── index.ts │ │ │ │ │ └── ... (other features) │ │ │ ├── entities/ # [Entities] 도메인 모델 + API │ │ ├── cluster/ │ │ │ ├── model/ │ │ │ │ └── types.ts │ │ │ ├── api/ │ │ │ │ ├── cluster.service.ts │ │ │ │ └── use-clusters.ts │ │ │ └── index.ts │ │ │ │ │ └── ... (other entities) │ │ │ └── shared/ # [Shared] 공용 코드 │ ├── api/ │ │ └── client.ts │ ├── components/ │ │ └── ui/ # shadcn/ui │ ├── hooks/ │ ├── lib/ │ │ ├── auth/ │ │ │ ├── server.ts │ │ │ ├── client.ts │ │ │ └── index.ts │ │ └── utils.ts │ └── config/ │ └── env.ts │ ├── public/ ├── .env ├── components.json ├── tailwind.config.ts ├── tsconfig.json └── next.config.ts
3-Portal Architecture
Portal Overview
Portal Target User Core Features Operator SRE/운영자 Cluster/Agent 관리, Fleet 모니터링 Provider API 제공자 API Service 정의, Product 생성, 배포 Consumer API 소비자 Marketplace 탐색, 구독, Credential 관리 Route Groups
Route Layout Auth Required (auth)/sign-inMinimal No operator/*OperatorSidebar Yes provider/*ProviderSidebar + TenantSwitcher Yes consumer/*ConsumerSidebar Yes Sidebar Menu Structure
Operator Portal:
- Overview: Dashboard, Fleet Monitor
- Infrastructure: Clusters, Agents
- Organization: Tenants, Users
- Audit & Logs
- Settings
Provider Portal (with TenantSwitcher):
- Overview: Dashboard
- API Management: API Services, Gateways
- Product Management: Products, Publishes, Auth Providers
- Consumer Management: Subscriptions
- Settings: Team, Settings
Consumer Portal:
- Overview: Dashboard
- Marketplace: Explore APIs
- My Subscriptions
- Credentials: Applications, API Keys
- Analytics: Usage
- Settings
Entity Pattern (Domain + API)
Each entity contains types, API service, and React Query hooks.
Entity Structure
entities/<entity>/ ├── model/ │ └── types.ts # TypeScript interfaces ├── api/ │ ├── <entity>.service.ts # API service (object-based) │ └── use-<entity>.ts # React Query hooks └── index.ts # Public exportsTypes Example
// entities/cluster/model/types.ts export interface Cluster { id: string; name: string; region: string; status: "active" | "inactive" | "pending"; createdAt: string; updatedAt: string; } export interface CreateClusterRequest { name: string; region: string; } export interface UpdateClusterRequest { name?: string; region?: string; }Service Example
// entities/cluster/api/cluster.service.ts import { apiClient } from "@/shared/api/client"; import type { Cluster, CreateClusterRequest, UpdateClusterRequest } from "../model/types"; const BASE_PATH = "/v1/operator/clusters"; export const clusterService = { list: async (): Promise<Cluster[]> => { const { data } = await apiClient.get<{ clusters: Cluster[] }>(BASE_PATH); return data.clusters || []; }, get: async (id: string): Promise<Cluster> => { const { data } = await apiClient.get<{ cluster: Cluster }>(`${BASE_PATH}/${id}`); return data.cluster; }, create: async (request: CreateClusterRequest): Promise<Cluster> => { const { data } = await apiClient.post<{ cluster: Cluster }>(BASE_PATH, request); return data.cluster; }, update: async (id: string, request: UpdateClusterRequest): Promise<Cluster> => { const { data } = await apiClient.put<{ cluster: Cluster }>(`${BASE_PATH}/${id}`, request); return data.cluster; }, delete: async (id: string): Promise<void> => { await apiClient.delete(`${BASE_PATH}/${id}`); }, };React Query Hooks Example
// entities/cluster/api/use-clusters.ts import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { clusterService } from "./cluster.service"; import type { CreateClusterRequest, UpdateClusterRequest } from "../model/types"; export const clusterKeys = { all: ["clusters"] as const, list: () => [...clusterKeys.all, "list"] as const, detail: (id: string) => [...clusterKeys.all, "detail", id] as const, }; export function useClusters() { return useQuery({ queryKey: clusterKeys.list(), queryFn: () => clusterService.list(), }); } export function useCluster(id: string) { return useQuery({ queryKey: clusterKeys.detail(id), queryFn: () => clusterService.get(id), enabled: !!id, }); } export function useCreateCluster() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (request: CreateClusterRequest) => clusterService.create(request), onSuccess: () => { queryClient.invalidateQueries({ queryKey: clusterKeys.all }); }, }); } export function useUpdateCluster(id: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: (request: UpdateClusterRequest) => clusterService.update(id, request), onSuccess: () => { queryClient.invalidateQueries({ queryKey: clusterKeys.all }); }, }); } export function useDeleteCluster() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (id: string) => clusterService.delete(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: clusterKeys.all }); }, }); }Entity Index
// entities/cluster/index.ts export type { Cluster, CreateClusterRequest, UpdateClusterRequest } from "./model/types"; export { clusterService } from "./api/cluster.service"; export { useClusters, useCluster, useCreateCluster, useUpdateCluster, useDeleteCluster, clusterKeys, } from "./api/use-clusters";
Feature Pattern
Features contain business actions (사용자에게 비즈니스 가치를 제공하는 기능).
Feature Structure
features/<feature>/ ├── ui/ │ ├── <Feature>Card.tsx │ ├── <Feature>Table.tsx │ └── Create<Feature>Dialog.tsx ├── model/ # (optional) feature-specific types │ └── types.ts └── index.tsFeature Index
// features/clusters/index.ts export { ClusterCard } from "./ui/ClusterCard"; export { ClusterTable } from "./ui/ClusterTable"; export { CreateClusterDialog } from "./ui/CreateClusterDialog";
Page Pattern
Pages 레이어는 widgets, features, entities를 조합하여 완전한 페이지 UI를 구성.
Pages Structure
pages/<portal>/ ├── index.ts # Public exports ├── DashboardPage.tsx ├── ClustersPage.tsx └── ClusterDetailPage.tsxPage Example
// pages/operator/ClustersPage.tsx "use client"; import { ClusterTable, CreateClusterDialog } from "@/features/clusters"; import { useClusters } from "@/entities/cluster"; import { PageContainer } from "@/widgets/layout"; import { Button } from "@/shared/components/ui/button"; export function ClustersPage() { const { data: clusters, isLoading } = useClusters(); return ( <PageContainer> <div className="flex items-center justify-between mb-6"> <div> <h1 className="text-2xl font-bold">Clusters</h1> <p className="text-muted-foreground">Manage your Kubernetes clusters</p> </div> <CreateClusterDialog /> </div> <ClusterTable data={clusters ?? []} isLoading={isLoading} /> </PageContainer> ); }App Router Integration
// app/operator/clusters/page.tsx import { ClustersPage } from "@/pages/operator"; export default function Page() { return <ClustersPage />; }
Authentication (Better Auth + Keycloak)
Configuration Files
File Purpose shared/lib/auth/server.tsBetter Auth 서버 설정 (genericOAuth) shared/lib/auth/client.tsBetter Auth 클라이언트 훅 app/api/auth/[...all]/route.tsAPI Route Handler Environment Variables
# .env NEXT_PUBLIC_APP_URL=http://localhost:3000 AUTH_SECRET=<32+ chars secret> # Keycloak (Confidential Client) KC_ISSUER=https://keycloak.admin.imprun.dev/realms/imprun KC_CLIENT_ID=imprun-console KC_CLIENT_SECRET=<client secret>Usage
// Client-side: Sign in with Keycloak import { signInWithKeycloak } from "@/shared/lib/auth/client"; await signInWithKeycloak("/dashboard"); // Client-side: Get session import { useSession } from "@/shared/lib/auth/client"; const { data: session, isPending } = useSession(); // Server-side: Get session import { auth } from "@/shared/lib/auth/server"; import { headers } from "next/headers"; const session = await auth.api.getSession({ headers: await headers() });
Component Patterns
Server vs Client Components
// Server Component (default) export async function PortalHeader({ title }: Props) { const session = await auth.api.getSession({ headers: await headers() }); return <header>...</header>; } // Client Component "use client"; export function Sidebar({ menuGroups }: Props) { const pathname = usePathname(); // ... }Key Rules
- Server Components: 기본값, 데이터 fetching, 세션 조회
- Client Components: 상호작용, hooks, state, browser APIs
- 경계 주의: Server → Client로 함수 직접 전달 불가
Form Pattern
import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/shared/components/ui/form"; const schema = z.object({ name: z.string().min(2, "Name must be at least 2 characters"), region: z.string().min(1, "Region is required"), }); type FormData = z.infer<typeof schema>; export function ClusterForm({ onSubmit }: { onSubmit: (data: FormData) => void }) { const form = useForm<FormData>({ resolver: zodResolver(schema), defaultValues: { name: "", region: "" }, }); return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <FormLabel>Name</FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button type="submit">Create</Button> </form> </Form> ); }
State Management
Server State (TanStack Query)
const { data, isLoading, error, refetch } = useClusters();Global UI State (Zustand)
// shared/store/sidebar.ts import { create } from "zustand"; interface SidebarStore { isOpen: boolean; toggle: () => void; } export const useSidebarStore = create<SidebarStore>((set) => ({ isOpen: true, toggle: () => set((state) => ({ isOpen: !state.isOpen })), }));Local Component State (useState)
const [isDialogOpen, setIsDialogOpen] = useState(false);
Adding New Features
1. Create Entity
src/entities/<entity>/ ├── model/types.ts ├── api/<entity>.service.ts ├── api/use-<entity>.ts └── index.ts2. Create Feature
src/features/<feature>/ ├── ui/<Feature>Table.tsx ├── ui/Create<Feature>Dialog.tsx └── index.ts3. Create Page
src/pages/<portal>/<Feature>Page.tsx4. Add App Router Entry
// app/<portal>/<feature>/page.tsx import { FeaturePage } from "@/pages/<portal>"; export default function Page() { return <FeaturePage />; }5. Add to Sidebar
Update
widgets/layout/<Portal>Sidebar.tsx.
Development Commands
pnpm install # Install dependencies pnpm dev # Dev server (port 3000) pnpm build # Production build pnpm lint # ESLint pnpm format # Prettier
shadcn/ui Configuration
# Add component (installs to src/shared/components/ui/) pnpm dlx shadcn@latest add button card dialog form inputFSD-compatible paths in
components.json:utils→@/shared/lib/utilsui→@/shared/components/uihooks→@/shared/hooks
Code Style Summary
Category Convention File naming kebab-casefor hooks/utils,PascalCasefor componentsComponent naming PascalCaseHook naming use-*.tsfiles,useMyHookin codeService naming Object-based, myService.method()TypeScript Strict mode, explicit types Exports Named exports preferred State React Query for server, Zustand for UI
Key Files
File Purpose src/app/layout.tsxRoot layout with providers src/app/operator/layout.tsxOperator portal layout src/pages/operator/index.tsOperator page exports src/widgets/layout/index.tsLayout widget exports src/shared/api/client.tsAPI client with auth src/shared/lib/auth/server.tsBetter Auth server config
Key Documentation
Document Content docs/epics/EPIC-003-frontend-v2.mdFrontend v2 요구사항 및 스토리 docs/user-flows.md사용자 여정별 플로우 docs/frontend-plan-v2.md개발 계획 '실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
Feature-Sliced Design: 프론트엔드 아키텍처의 표준화된 접근법 (0) 2025.11.24 Claude, Gemini, Codex에서 AGENTS.md 설정하기: AI 에이전트 통합 가이드 (0) 2025.11.24 Claude, Codex, Gemini가 본 API Gateway 콘솔 메뉴 구조: AI 모델별 UX 리뷰 비교 (0) 2025.11.24 Kubernetes 환경에서 Keycloak 커스텀 로그인 테마 배포하기 (0) 2025.11.23 Kubernetes Ephemeral Storage 문제 해결 가이드 (0) 2025.11.23