티스토리 뷰
AI Agent를 위한 Frontend 개발 가이드: AGETNTS.md 로 Next.js + shadcn/ui 프로젝트 구조 설계
pak2251 2025. 11. 24. 13:59AI 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-*.ts |
use-gateways.ts, use-products.ts |
| Services | *.ts |
gateway.ts, product.ts |
| Types | *.ts |
gateway.ts, product.ts |
| Components | *.tsx |
gateway-card.tsx, product-form.tsx |
| Utilities | *.ts |
format-date.ts, cn.ts |
| Feature folders | kebab-case |
api-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-case vs camelCase vs PascalCase 등 네이밍 규칙을 표로 정리하면 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 Structure
src/
├── 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-case for consistency.
| Item | Convention | Example |
|---|---|---|
| App Router | page.tsx, layout.tsx |
app/operator/clusters/page.tsx |
| Page Components | PascalCase.tsx |
pages/operator/ClustersPage.tsx |
| Components | PascalCase.tsx |
ClusterCard.tsx, ClusterForm.tsx |
| Hooks | use-*.ts |
use-clusters.ts, use-products.ts |
| Services | *.service.ts |
cluster.service.ts |
| Types | *.types.ts or types.ts |
cluster.types.ts |
| Utilities | kebab-case.ts |
format-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.ts3-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-in |
Minimal | 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.ts |
Better Auth 서버 설정 (genericOAuth) |
shared/lib/auth/client.ts |
Better Auth 클라이언트 훅 |
app/api/auth/[...all]/route.ts |
API 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.ts
2. Create Feature
src/features/<feature>/
├── ui/<Feature>Table.tsx
├── ui/Create<Feature>Dialog.tsx
└── index.ts
3. Create Page
src/pages/<portal>/<Feature>Page.tsx
4. 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 input
FSD-compatible paths in components.json:
utils→@/shared/lib/utilsui→@/shared/components/uihooks→@/shared/hooks
Code Style Summary
| Category | Convention |
|---|---|
| File naming | kebab-case for hooks/utils, PascalCase for components |
| Component naming | PascalCase |
| Hook naming | use-*.ts files, useMyHook in code |
| Service 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.tsx |
Root layout with providers |
src/app/operator/layout.tsx |
Operator portal layout |
src/pages/operator/index.ts |
Operator page exports |
src/widgets/layout/index.ts |
Layout widget exports |
src/shared/api/client.ts |
API client with auth |
src/shared/lib/auth/server.ts |
Better Auth server config |
Key Documentation
| Document | Content |
|---|---|
docs/epics/EPIC-003-frontend-v2.md |
Frontend 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 |
- Total
- Today
- Yesterday
- Developer Tools
- PYTHON
- backend
- AI Development
- Rag
- SHACL
- AI agent
- Tailwind CSS
- claude code
- knowledge graph
- LangChain
- security
- Tax Analysis
- LLM
- architecture
- Kubernetes
- react
- authentication
- authorization
- Next.js
- Go
- troubleshooting
- api gateway
- AI
- frontend
- workflow
- Ontology
- ai 개발 도구
- Claude
- 개발 도구
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |