mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-08 18:33:28 +08:00
docs(ko-KR): add Korean translation for skills
- 15 skill categories (17 files): coding-standards, tdd-workflow, frontend-patterns, backend-patterns, security-review (2 files), postgres-patterns, verification-loop, continuous-learning, continuous-learning-v2, eval-harness, iterative-retrieval, strategic-compact, golang-patterns, golang-testing, clickhouse-io, project-guidelines-example
This commit is contained in:
598
docs/ko-KR/skills/backend-patterns/SKILL.md
Normal file
598
docs/ko-KR/skills/backend-patterns/SKILL.md
Normal file
@@ -0,0 +1,598 @@
|
|||||||
|
---
|
||||||
|
name: backend-patterns
|
||||||
|
description: Node.js, Express, Next.js API 라우트를 위한 백엔드 아키텍처 패턴, API 설계, 데이터베이스 최적화 및 서버 사이드 모범 사례.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# 백엔드 개발 패턴
|
||||||
|
|
||||||
|
확장 가능한 서버 사이드 애플리케이션을 위한 백엔드 아키텍처 패턴과 모범 사례.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- REST 또는 GraphQL API 엔드포인트를 설계할 때
|
||||||
|
- Repository, Service 또는 Controller 레이어를 구현할 때
|
||||||
|
- 데이터베이스 쿼리를 최적화할 때 (N+1, 인덱싱, 커넥션 풀링)
|
||||||
|
- 캐싱을 추가할 때 (Redis, 인메모리, HTTP 캐시 헤더)
|
||||||
|
- 백그라운드 작업이나 비동기 처리를 설정할 때
|
||||||
|
- API를 위한 에러 처리 및 유효성 검사를 구조화할 때
|
||||||
|
- 미들웨어를 구축할 때 (인증, 로깅, 요청 제한)
|
||||||
|
|
||||||
|
## API 설계 패턴
|
||||||
|
|
||||||
|
### RESTful API 구조
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Resource-based URLs
|
||||||
|
GET /api/markets # List resources
|
||||||
|
GET /api/markets/:id # Get single resource
|
||||||
|
POST /api/markets # Create resource
|
||||||
|
PUT /api/markets/:id # Replace resource
|
||||||
|
PATCH /api/markets/:id # Update resource
|
||||||
|
DELETE /api/markets/:id # Delete resource
|
||||||
|
|
||||||
|
// ✅ Query parameters for filtering, sorting, pagination
|
||||||
|
GET /api/markets?status=active&sort=volume&limit=20&offset=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Repository 패턴
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Abstract data access logic
|
||||||
|
interface MarketRepository {
|
||||||
|
findAll(filters?: MarketFilters): Promise<Market[]>
|
||||||
|
findById(id: string): Promise<Market | null>
|
||||||
|
create(data: CreateMarketDto): Promise<Market>
|
||||||
|
update(id: string, data: UpdateMarketDto): Promise<Market>
|
||||||
|
delete(id: string): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
class SupabaseMarketRepository implements MarketRepository {
|
||||||
|
async findAll(filters?: MarketFilters): Promise<Market[]> {
|
||||||
|
let query = supabase.from('markets').select('*')
|
||||||
|
|
||||||
|
if (filters?.status) {
|
||||||
|
query = query.eq('status', filters.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters?.limit) {
|
||||||
|
query = query.limit(filters.limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await query
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other methods...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service 레이어 패턴
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Business logic separated from data access
|
||||||
|
class MarketService {
|
||||||
|
constructor(private marketRepo: MarketRepository) {}
|
||||||
|
|
||||||
|
async searchMarkets(query: string, limit: number = 10): Promise<Market[]> {
|
||||||
|
// Business logic
|
||||||
|
const embedding = await generateEmbedding(query)
|
||||||
|
const results = await this.vectorSearch(embedding, limit)
|
||||||
|
|
||||||
|
// Fetch full data
|
||||||
|
const markets = await this.marketRepo.findByIds(results.map(r => r.id))
|
||||||
|
|
||||||
|
// Sort by similarity
|
||||||
|
return markets.sort((a, b) => {
|
||||||
|
const scoreA = results.find(r => r.id === a.id)?.score || 0
|
||||||
|
const scoreB = results.find(r => r.id === b.id)?.score || 0
|
||||||
|
return scoreA - scoreB
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async vectorSearch(embedding: number[], limit: number) {
|
||||||
|
// Vector search implementation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 미들웨어 패턴
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Request/response processing pipeline
|
||||||
|
export function withAuth(handler: NextApiHandler): NextApiHandler {
|
||||||
|
return async (req, res) => {
|
||||||
|
const token = req.headers.authorization?.replace('Bearer ', '')
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await verifyToken(token)
|
||||||
|
req.user = user
|
||||||
|
return handler(req, res)
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(401).json({ error: 'Invalid token' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
export default withAuth(async (req, res) => {
|
||||||
|
// Handler has access to req.user
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 데이터베이스 패턴
|
||||||
|
|
||||||
|
### 쿼리 최적화
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Select only needed columns
|
||||||
|
const { data } = await supabase
|
||||||
|
.from('markets')
|
||||||
|
.select('id, name, status, volume')
|
||||||
|
.eq('status', 'active')
|
||||||
|
.order('volume', { ascending: false })
|
||||||
|
.limit(10)
|
||||||
|
|
||||||
|
// ❌ BAD: Select everything
|
||||||
|
const { data } = await supabase
|
||||||
|
.from('markets')
|
||||||
|
.select('*')
|
||||||
|
```
|
||||||
|
|
||||||
|
### N+1 쿼리 방지
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: N+1 query problem
|
||||||
|
const markets = await getMarkets()
|
||||||
|
for (const market of markets) {
|
||||||
|
market.creator = await getUser(market.creator_id) // N queries
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: Batch fetch
|
||||||
|
const markets = await getMarkets()
|
||||||
|
const creatorIds = markets.map(m => m.creator_id)
|
||||||
|
const creators = await getUsers(creatorIds) // 1 query
|
||||||
|
const creatorMap = new Map(creators.map(c => [c.id, c]))
|
||||||
|
|
||||||
|
markets.forEach(market => {
|
||||||
|
market.creator = creatorMap.get(market.creator_id)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 트랜잭션 패턴
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function createMarketWithPosition(
|
||||||
|
marketData: CreateMarketDto,
|
||||||
|
positionData: CreatePositionDto
|
||||||
|
) {
|
||||||
|
// Use Supabase transaction
|
||||||
|
const { data, error } = await supabase.rpc('create_market_with_position', {
|
||||||
|
market_data: marketData,
|
||||||
|
position_data: positionData
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) throw new Error('Transaction failed')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQL function in Supabase
|
||||||
|
CREATE OR REPLACE FUNCTION create_market_with_position(
|
||||||
|
market_data jsonb,
|
||||||
|
position_data jsonb
|
||||||
|
)
|
||||||
|
RETURNS jsonb
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Start transaction automatically
|
||||||
|
INSERT INTO markets VALUES (market_data);
|
||||||
|
INSERT INTO positions VALUES (position_data);
|
||||||
|
RETURN jsonb_build_object('success', true);
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
-- Rollback happens automatically
|
||||||
|
RETURN jsonb_build_object('success', false, 'error', SQLERRM);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 캐싱 전략
|
||||||
|
|
||||||
|
### Redis 캐싱 레이어
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class CachedMarketRepository implements MarketRepository {
|
||||||
|
constructor(
|
||||||
|
private baseRepo: MarketRepository,
|
||||||
|
private redis: RedisClient
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<Market | null> {
|
||||||
|
// Check cache first
|
||||||
|
const cached = await this.redis.get(`market:${id}`)
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
return JSON.parse(cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss - fetch from database
|
||||||
|
const market = await this.baseRepo.findById(id)
|
||||||
|
|
||||||
|
if (market) {
|
||||||
|
// Cache for 5 minutes
|
||||||
|
await this.redis.setex(`market:${id}`, 300, JSON.stringify(market))
|
||||||
|
}
|
||||||
|
|
||||||
|
return market
|
||||||
|
}
|
||||||
|
|
||||||
|
async invalidateCache(id: string): Promise<void> {
|
||||||
|
await this.redis.del(`market:${id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache-Aside 패턴
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function getMarketWithCache(id: string): Promise<Market> {
|
||||||
|
const cacheKey = `market:${id}`
|
||||||
|
|
||||||
|
// Try cache
|
||||||
|
const cached = await redis.get(cacheKey)
|
||||||
|
if (cached) return JSON.parse(cached)
|
||||||
|
|
||||||
|
// Cache miss - fetch from DB
|
||||||
|
const market = await db.markets.findUnique({ where: { id } })
|
||||||
|
|
||||||
|
if (!market) throw new Error('Market not found')
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
await redis.setex(cacheKey, 300, JSON.stringify(market))
|
||||||
|
|
||||||
|
return market
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 에러 처리 패턴
|
||||||
|
|
||||||
|
### 중앙화된 에러 핸들러
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class ApiError extends Error {
|
||||||
|
constructor(
|
||||||
|
public statusCode: number,
|
||||||
|
public message: string,
|
||||||
|
public isOperational = true
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
Object.setPrototypeOf(this, ApiError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorHandler(error: unknown, req: Request): Response {
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}, { status: error.statusCode })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Validation failed',
|
||||||
|
details: error.errors
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log unexpected errors
|
||||||
|
console.error('Unexpected error:', error)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Internal server error'
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const data = await fetchData()
|
||||||
|
return NextResponse.json({ success: true, data })
|
||||||
|
} catch (error) {
|
||||||
|
return errorHandler(error, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 지수 백오프를 이용한 재시도
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function fetchWithRetry<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
maxRetries = 3
|
||||||
|
): Promise<T> {
|
||||||
|
let lastError: Error
|
||||||
|
|
||||||
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error as Error
|
||||||
|
|
||||||
|
if (i < maxRetries - 1) {
|
||||||
|
// Exponential backoff: 1s, 2s, 4s
|
||||||
|
const delay = Math.pow(2, i) * 1000
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const data = await fetchWithRetry(() => fetchFromAPI())
|
||||||
|
```
|
||||||
|
|
||||||
|
## 인증 및 인가
|
||||||
|
|
||||||
|
### JWT 토큰 검증
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
|
||||||
|
interface JWTPayload {
|
||||||
|
userId: string
|
||||||
|
email: string
|
||||||
|
role: 'admin' | 'user'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyToken(token: string): JWTPayload {
|
||||||
|
try {
|
||||||
|
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload
|
||||||
|
return payload
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(401, 'Invalid token')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requireAuth(request: Request) {
|
||||||
|
const token = request.headers.get('authorization')?.replace('Bearer ', '')
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new ApiError(401, 'Missing authorization token')
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifyToken(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in API route
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const user = await requireAuth(request)
|
||||||
|
|
||||||
|
const data = await getDataForUser(user.userId)
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, data })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 역할 기반 접근 제어
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type Permission = 'read' | 'write' | 'delete' | 'admin'
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string
|
||||||
|
role: 'admin' | 'moderator' | 'user'
|
||||||
|
}
|
||||||
|
|
||||||
|
const rolePermissions: Record<User['role'], Permission[]> = {
|
||||||
|
admin: ['read', 'write', 'delete', 'admin'],
|
||||||
|
moderator: ['read', 'write', 'delete'],
|
||||||
|
user: ['read', 'write']
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasPermission(user: User, permission: Permission): boolean {
|
||||||
|
return rolePermissions[user.role].includes(permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requirePermission(permission: Permission) {
|
||||||
|
return (handler: (request: Request, user: User) => Promise<Response>) => {
|
||||||
|
return async (request: Request) => {
|
||||||
|
const user = await requireAuth(request)
|
||||||
|
|
||||||
|
if (!hasPermission(user, permission)) {
|
||||||
|
throw new ApiError(403, 'Insufficient permissions')
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler(request, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage - HOF wraps the handler
|
||||||
|
export const DELETE = requirePermission('delete')(
|
||||||
|
async (request: Request, user: User) => {
|
||||||
|
// Handler receives authenticated user with verified permission
|
||||||
|
return new Response('Deleted', { status: 200 })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 요청 제한
|
||||||
|
|
||||||
|
### 간단한 인메모리 요청 제한기
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class RateLimiter {
|
||||||
|
private requests = new Map<string, number[]>()
|
||||||
|
|
||||||
|
async checkLimit(
|
||||||
|
identifier: string,
|
||||||
|
maxRequests: number,
|
||||||
|
windowMs: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
const now = Date.now()
|
||||||
|
const requests = this.requests.get(identifier) || []
|
||||||
|
|
||||||
|
// Remove old requests outside window
|
||||||
|
const recentRequests = requests.filter(time => now - time < windowMs)
|
||||||
|
|
||||||
|
if (recentRequests.length >= maxRequests) {
|
||||||
|
return false // Rate limit exceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add current request
|
||||||
|
recentRequests.push(now)
|
||||||
|
this.requests.set(identifier, recentRequests)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const limiter = new RateLimiter()
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const ip = request.headers.get('x-forwarded-for') || 'unknown'
|
||||||
|
|
||||||
|
const allowed = await limiter.checkLimit(ip, 100, 60000) // 100 req/min
|
||||||
|
|
||||||
|
if (!allowed) {
|
||||||
|
return NextResponse.json({
|
||||||
|
error: 'Rate limit exceeded'
|
||||||
|
}, { status: 429 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with request
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 백그라운드 작업 및 큐
|
||||||
|
|
||||||
|
### 간단한 큐 패턴
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class JobQueue<T> {
|
||||||
|
private queue: T[] = []
|
||||||
|
private processing = false
|
||||||
|
|
||||||
|
async add(job: T): Promise<void> {
|
||||||
|
this.queue.push(job)
|
||||||
|
|
||||||
|
if (!this.processing) {
|
||||||
|
this.process()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async process(): Promise<void> {
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
while (this.queue.length > 0) {
|
||||||
|
const job = this.queue.shift()!
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.execute(job)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Job failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private async execute(job: T): Promise<void> {
|
||||||
|
// Job execution logic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage for indexing markets
|
||||||
|
interface IndexJob {
|
||||||
|
marketId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexQueue = new JobQueue<IndexJob>()
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const { marketId } = await request.json()
|
||||||
|
|
||||||
|
// Add to queue instead of blocking
|
||||||
|
await indexQueue.add({ marketId })
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, message: 'Job queued' })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 로깅 및 모니터링
|
||||||
|
|
||||||
|
### 구조화된 로깅
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface LogContext {
|
||||||
|
userId?: string
|
||||||
|
requestId?: string
|
||||||
|
method?: string
|
||||||
|
path?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
log(level: 'info' | 'warn' | 'error', message: string, context?: LogContext) {
|
||||||
|
const entry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
...context
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message: string, context?: LogContext) {
|
||||||
|
this.log('info', message, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: string, context?: LogContext) {
|
||||||
|
this.log('warn', message, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string, error: Error, context?: LogContext) {
|
||||||
|
this.log('error', message, {
|
||||||
|
...context,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = new Logger()
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const requestId = crypto.randomUUID()
|
||||||
|
|
||||||
|
logger.info('Fetching markets', {
|
||||||
|
requestId,
|
||||||
|
method: 'GET',
|
||||||
|
path: '/api/markets'
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const markets = await fetchMarkets()
|
||||||
|
return NextResponse.json({ success: true, data: markets })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to fetch markets', error as Error, { requestId })
|
||||||
|
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**기억하세요**: 백엔드 패턴은 확장 가능하고 유지보수 가능한 서버 사이드 애플리케이션을 가능하게 합니다. 복잡도 수준에 맞는 패턴을 선택하세요.
|
||||||
439
docs/ko-KR/skills/clickhouse-io/SKILL.md
Normal file
439
docs/ko-KR/skills/clickhouse-io/SKILL.md
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
---
|
||||||
|
name: clickhouse-io
|
||||||
|
description: 고성능 분석 워크로드를 위한 ClickHouse 데이터베이스 패턴, 쿼리 최적화, 분석 및 데이터 엔지니어링 모범 사례.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# ClickHouse 분석 패턴
|
||||||
|
|
||||||
|
고성능 분석 및 데이터 엔지니어링을 위한 ClickHouse 전용 패턴.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- ClickHouse 테이블 스키마 설계 시 (MergeTree 엔진 선택)
|
||||||
|
- 분석 쿼리 작성 시 (집계, 윈도우 함수, 조인)
|
||||||
|
- 쿼리 성능 최적화 시 (파티션 프루닝, 프로젝션, 구체화된 뷰)
|
||||||
|
- 대량 데이터 수집 시 (배치 삽입, Kafka 통합)
|
||||||
|
- PostgreSQL/MySQL에서 ClickHouse로 분석 마이그레이션 시
|
||||||
|
- 실시간 대시보드 또는 시계열 분석 구현 시
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
ClickHouse는 온라인 분석 처리(OLAP)를 위한 컬럼 지향 데이터베이스 관리 시스템(DBMS)입니다. 대규모 데이터셋에 대한 빠른 분석 쿼리에 최적화되어 있습니다.
|
||||||
|
|
||||||
|
**주요 특징:**
|
||||||
|
- 컬럼 지향 저장소
|
||||||
|
- 데이터 압축
|
||||||
|
- 병렬 쿼리 실행
|
||||||
|
- 분산 쿼리
|
||||||
|
- 실시간 분석
|
||||||
|
|
||||||
|
## 테이블 설계 패턴
|
||||||
|
|
||||||
|
### MergeTree 엔진 (가장 일반적)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE markets_analytics (
|
||||||
|
date Date,
|
||||||
|
market_id String,
|
||||||
|
market_name String,
|
||||||
|
volume UInt64,
|
||||||
|
trades UInt32,
|
||||||
|
unique_traders UInt32,
|
||||||
|
avg_trade_size Float64,
|
||||||
|
created_at DateTime
|
||||||
|
) ENGINE = MergeTree()
|
||||||
|
PARTITION BY toYYYYMM(date)
|
||||||
|
ORDER BY (date, market_id)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
|
```
|
||||||
|
|
||||||
|
### ReplacingMergeTree (중복 제거)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- For data that may have duplicates (e.g., from multiple sources)
|
||||||
|
CREATE TABLE user_events (
|
||||||
|
event_id String,
|
||||||
|
user_id String,
|
||||||
|
event_type String,
|
||||||
|
timestamp DateTime,
|
||||||
|
properties String
|
||||||
|
) ENGINE = ReplacingMergeTree()
|
||||||
|
PARTITION BY toYYYYMM(timestamp)
|
||||||
|
ORDER BY (user_id, event_id, timestamp)
|
||||||
|
PRIMARY KEY (user_id, event_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
### AggregatingMergeTree (사전 집계)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- For maintaining aggregated metrics
|
||||||
|
CREATE TABLE market_stats_hourly (
|
||||||
|
hour DateTime,
|
||||||
|
market_id String,
|
||||||
|
total_volume AggregateFunction(sum, UInt64),
|
||||||
|
total_trades AggregateFunction(count, UInt32),
|
||||||
|
unique_users AggregateFunction(uniq, String)
|
||||||
|
) ENGINE = AggregatingMergeTree()
|
||||||
|
PARTITION BY toYYYYMM(hour)
|
||||||
|
ORDER BY (hour, market_id);
|
||||||
|
|
||||||
|
-- Query aggregated data
|
||||||
|
SELECT
|
||||||
|
hour,
|
||||||
|
market_id,
|
||||||
|
sumMerge(total_volume) AS volume,
|
||||||
|
countMerge(total_trades) AS trades,
|
||||||
|
uniqMerge(unique_users) AS users
|
||||||
|
FROM market_stats_hourly
|
||||||
|
WHERE hour >= toStartOfHour(now() - INTERVAL 24 HOUR)
|
||||||
|
GROUP BY hour, market_id
|
||||||
|
ORDER BY hour DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 쿼리 최적화 패턴
|
||||||
|
|
||||||
|
### 효율적인 필터링
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ✅ GOOD: Use indexed columns first
|
||||||
|
SELECT *
|
||||||
|
FROM markets_analytics
|
||||||
|
WHERE date >= '2025-01-01'
|
||||||
|
AND market_id = 'market-123'
|
||||||
|
AND volume > 1000
|
||||||
|
ORDER BY date DESC
|
||||||
|
LIMIT 100;
|
||||||
|
|
||||||
|
-- ❌ BAD: Filter on non-indexed columns first
|
||||||
|
SELECT *
|
||||||
|
FROM markets_analytics
|
||||||
|
WHERE volume > 1000
|
||||||
|
AND market_name LIKE '%election%'
|
||||||
|
AND date >= '2025-01-01';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 집계
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ✅ GOOD: Use ClickHouse-specific aggregation functions
|
||||||
|
SELECT
|
||||||
|
toStartOfDay(created_at) AS day,
|
||||||
|
market_id,
|
||||||
|
sum(volume) AS total_volume,
|
||||||
|
count() AS total_trades,
|
||||||
|
uniq(trader_id) AS unique_traders,
|
||||||
|
avg(trade_size) AS avg_size
|
||||||
|
FROM trades
|
||||||
|
WHERE created_at >= today() - INTERVAL 7 DAY
|
||||||
|
GROUP BY day, market_id
|
||||||
|
ORDER BY day DESC, total_volume DESC;
|
||||||
|
|
||||||
|
-- ✅ Use quantile for percentiles (more efficient than percentile)
|
||||||
|
SELECT
|
||||||
|
quantile(0.50)(trade_size) AS median,
|
||||||
|
quantile(0.95)(trade_size) AS p95,
|
||||||
|
quantile(0.99)(trade_size) AS p99
|
||||||
|
FROM trades
|
||||||
|
WHERE created_at >= now() - INTERVAL 1 HOUR;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 윈도우 함수
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Calculate running totals
|
||||||
|
SELECT
|
||||||
|
date,
|
||||||
|
market_id,
|
||||||
|
volume,
|
||||||
|
sum(volume) OVER (
|
||||||
|
PARTITION BY market_id
|
||||||
|
ORDER BY date
|
||||||
|
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||||
|
) AS cumulative_volume
|
||||||
|
FROM markets_analytics
|
||||||
|
WHERE date >= today() - INTERVAL 30 DAY
|
||||||
|
ORDER BY market_id, date;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 데이터 삽입 패턴
|
||||||
|
|
||||||
|
### 배치 삽입 (권장)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ClickHouse } from 'clickhouse'
|
||||||
|
|
||||||
|
const clickhouse = new ClickHouse({
|
||||||
|
url: process.env.CLICKHOUSE_URL,
|
||||||
|
port: 8123,
|
||||||
|
basicAuth: {
|
||||||
|
username: process.env.CLICKHOUSE_USER,
|
||||||
|
password: process.env.CLICKHOUSE_PASSWORD
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ Batch insert (efficient)
|
||||||
|
async function bulkInsertTrades(trades: Trade[]) {
|
||||||
|
const values = trades.map(trade => `(
|
||||||
|
'${trade.id}',
|
||||||
|
'${trade.market_id}',
|
||||||
|
'${trade.user_id}',
|
||||||
|
${trade.amount},
|
||||||
|
'${trade.timestamp.toISOString()}'
|
||||||
|
)`).join(',')
|
||||||
|
|
||||||
|
await clickhouse.query(`
|
||||||
|
INSERT INTO trades (id, market_id, user_id, amount, timestamp)
|
||||||
|
VALUES ${values}
|
||||||
|
`).toPromise()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Individual inserts (slow)
|
||||||
|
async function insertTrade(trade: Trade) {
|
||||||
|
// Don't do this in a loop!
|
||||||
|
await clickhouse.query(`
|
||||||
|
INSERT INTO trades VALUES ('${trade.id}', ...)
|
||||||
|
`).toPromise()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 스트리밍 삽입
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// For continuous data ingestion
|
||||||
|
import { createWriteStream } from 'fs'
|
||||||
|
import { pipeline } from 'stream/promises'
|
||||||
|
|
||||||
|
async function streamInserts() {
|
||||||
|
const stream = clickhouse.insert('trades').stream()
|
||||||
|
|
||||||
|
for await (const batch of dataSource) {
|
||||||
|
stream.write(batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
await stream.end()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 구체화된 뷰
|
||||||
|
|
||||||
|
### 실시간 집계
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Create materialized view for hourly stats
|
||||||
|
CREATE MATERIALIZED VIEW market_stats_hourly_mv
|
||||||
|
TO market_stats_hourly
|
||||||
|
AS SELECT
|
||||||
|
toStartOfHour(timestamp) AS hour,
|
||||||
|
market_id,
|
||||||
|
sumState(amount) AS total_volume,
|
||||||
|
countState() AS total_trades,
|
||||||
|
uniqState(user_id) AS unique_users
|
||||||
|
FROM trades
|
||||||
|
GROUP BY hour, market_id;
|
||||||
|
|
||||||
|
-- Query the materialized view
|
||||||
|
SELECT
|
||||||
|
hour,
|
||||||
|
market_id,
|
||||||
|
sumMerge(total_volume) AS volume,
|
||||||
|
countMerge(total_trades) AS trades,
|
||||||
|
uniqMerge(unique_users) AS users
|
||||||
|
FROM market_stats_hourly
|
||||||
|
WHERE hour >= now() - INTERVAL 24 HOUR
|
||||||
|
GROUP BY hour, market_id;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 성능 모니터링
|
||||||
|
|
||||||
|
### 쿼리 성능
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Check slow queries
|
||||||
|
SELECT
|
||||||
|
query_id,
|
||||||
|
user,
|
||||||
|
query,
|
||||||
|
query_duration_ms,
|
||||||
|
read_rows,
|
||||||
|
read_bytes,
|
||||||
|
memory_usage
|
||||||
|
FROM system.query_log
|
||||||
|
WHERE type = 'QueryFinish'
|
||||||
|
AND query_duration_ms > 1000
|
||||||
|
AND event_time >= now() - INTERVAL 1 HOUR
|
||||||
|
ORDER BY query_duration_ms DESC
|
||||||
|
LIMIT 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 테이블 통계
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Check table sizes
|
||||||
|
SELECT
|
||||||
|
database,
|
||||||
|
table,
|
||||||
|
formatReadableSize(sum(bytes)) AS size,
|
||||||
|
sum(rows) AS rows,
|
||||||
|
max(modification_time) AS latest_modification
|
||||||
|
FROM system.parts
|
||||||
|
WHERE active
|
||||||
|
GROUP BY database, table
|
||||||
|
ORDER BY sum(bytes) DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 일반적인 분석 쿼리
|
||||||
|
|
||||||
|
### 시계열 분석
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Daily active users
|
||||||
|
SELECT
|
||||||
|
toDate(timestamp) AS date,
|
||||||
|
uniq(user_id) AS daily_active_users
|
||||||
|
FROM events
|
||||||
|
WHERE timestamp >= today() - INTERVAL 30 DAY
|
||||||
|
GROUP BY date
|
||||||
|
ORDER BY date;
|
||||||
|
|
||||||
|
-- Retention analysis
|
||||||
|
SELECT
|
||||||
|
signup_date,
|
||||||
|
countIf(days_since_signup = 0) AS day_0,
|
||||||
|
countIf(days_since_signup = 1) AS day_1,
|
||||||
|
countIf(days_since_signup = 7) AS day_7,
|
||||||
|
countIf(days_since_signup = 30) AS day_30
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
min(toDate(timestamp)) AS signup_date,
|
||||||
|
toDate(timestamp) AS activity_date,
|
||||||
|
dateDiff('day', signup_date, activity_date) AS days_since_signup
|
||||||
|
FROM events
|
||||||
|
GROUP BY user_id, activity_date
|
||||||
|
)
|
||||||
|
GROUP BY signup_date
|
||||||
|
ORDER BY signup_date DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 퍼널 분석
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Conversion funnel
|
||||||
|
SELECT
|
||||||
|
countIf(step = 'viewed_market') AS viewed,
|
||||||
|
countIf(step = 'clicked_trade') AS clicked,
|
||||||
|
countIf(step = 'completed_trade') AS completed,
|
||||||
|
round(clicked / viewed * 100, 2) AS view_to_click_rate,
|
||||||
|
round(completed / clicked * 100, 2) AS click_to_completion_rate
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
session_id,
|
||||||
|
event_type AS step
|
||||||
|
FROM events
|
||||||
|
WHERE event_date = today()
|
||||||
|
)
|
||||||
|
GROUP BY session_id;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 코호트 분석
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- User cohorts by signup month
|
||||||
|
SELECT
|
||||||
|
toStartOfMonth(signup_date) AS cohort,
|
||||||
|
toStartOfMonth(activity_date) AS month,
|
||||||
|
dateDiff('month', cohort, month) AS months_since_signup,
|
||||||
|
count(DISTINCT user_id) AS active_users
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
min(toDate(timestamp)) OVER (PARTITION BY user_id) AS signup_date,
|
||||||
|
toDate(timestamp) AS activity_date
|
||||||
|
FROM events
|
||||||
|
)
|
||||||
|
GROUP BY cohort, month, months_since_signup
|
||||||
|
ORDER BY cohort, months_since_signup;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 데이터 파이프라인 패턴
|
||||||
|
|
||||||
|
### ETL 패턴
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Extract, Transform, Load
|
||||||
|
async function etlPipeline() {
|
||||||
|
// 1. Extract from source
|
||||||
|
const rawData = await extractFromPostgres()
|
||||||
|
|
||||||
|
// 2. Transform
|
||||||
|
const transformed = rawData.map(row => ({
|
||||||
|
date: new Date(row.created_at).toISOString().split('T')[0],
|
||||||
|
market_id: row.market_slug,
|
||||||
|
volume: parseFloat(row.total_volume),
|
||||||
|
trades: parseInt(row.trade_count)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 3. Load to ClickHouse
|
||||||
|
await bulkInsertToClickHouse(transformed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run periodically
|
||||||
|
setInterval(etlPipeline, 60 * 60 * 1000) // Every hour
|
||||||
|
```
|
||||||
|
|
||||||
|
### 변경 데이터 캡처 (CDC)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Listen to PostgreSQL changes and sync to ClickHouse
|
||||||
|
import { Client } from 'pg'
|
||||||
|
|
||||||
|
const pgClient = new Client({ connectionString: process.env.DATABASE_URL })
|
||||||
|
|
||||||
|
pgClient.query('LISTEN market_updates')
|
||||||
|
|
||||||
|
pgClient.on('notification', async (msg) => {
|
||||||
|
const update = JSON.parse(msg.payload)
|
||||||
|
|
||||||
|
await clickhouse.insert('market_updates', [
|
||||||
|
{
|
||||||
|
market_id: update.id,
|
||||||
|
event_type: update.operation, // INSERT, UPDATE, DELETE
|
||||||
|
timestamp: new Date(),
|
||||||
|
data: JSON.stringify(update.new_data)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 모범 사례
|
||||||
|
|
||||||
|
### 1. 파티셔닝 전략
|
||||||
|
- 시간별 파티셔닝 (보통 월 또는 일)
|
||||||
|
- 파티션이 너무 많은 것 방지 (성능 영향)
|
||||||
|
- 파티션 키에 DATE 타입 사용
|
||||||
|
|
||||||
|
### 2. 정렬 키
|
||||||
|
- 가장 자주 필터링되는 컬럼을 먼저 배치
|
||||||
|
- 카디널리티 고려 (높은 카디널리티 먼저)
|
||||||
|
- 정렬이 압축에 영향을 미침
|
||||||
|
|
||||||
|
### 3. 데이터 타입
|
||||||
|
- 가장 작은 적절한 타입 사용 (UInt32 vs UInt64)
|
||||||
|
- 반복되는 문자열에 LowCardinality 사용
|
||||||
|
- 범주형 데이터에 Enum 사용
|
||||||
|
|
||||||
|
### 4. 피해야 할 것
|
||||||
|
- SELECT * (컬럼을 명시)
|
||||||
|
- FINAL (쿼리 전에 데이터를 병합)
|
||||||
|
- 너무 많은 JOIN (분석을 위해 비정규화)
|
||||||
|
- 작은 빈번한 삽입 (배치 처리)
|
||||||
|
|
||||||
|
### 5. 모니터링
|
||||||
|
- 쿼리 성능 추적
|
||||||
|
- 디스크 사용량 모니터링
|
||||||
|
- 병합 작업 확인
|
||||||
|
- 슬로우 쿼리 로그 검토
|
||||||
|
|
||||||
|
**기억하세요**: ClickHouse는 분석 워크로드에 탁월합니다. 쿼리 패턴에 맞게 테이블을 설계하고, 배치 삽입을 사용하며, 실시간 집계를 위해 구체화된 뷰를 활용하세요.
|
||||||
530
docs/ko-KR/skills/coding-standards/SKILL.md
Normal file
530
docs/ko-KR/skills/coding-standards/SKILL.md
Normal file
@@ -0,0 +1,530 @@
|
|||||||
|
---
|
||||||
|
name: coding-standards
|
||||||
|
description: TypeScript, JavaScript, React, Node.js 개발을 위한 범용 코딩 표준, 모범 사례 및 패턴.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# 코딩 표준 및 모범 사례
|
||||||
|
|
||||||
|
모든 프로젝트에 적용 가능한 범용 코딩 표준.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- 새 프로젝트 또는 모듈을 시작할 때
|
||||||
|
- 코드 품질 및 유지보수성을 검토할 때
|
||||||
|
- 기존 코드를 컨벤션에 맞게 리팩터링할 때
|
||||||
|
- 네이밍, 포맷팅 또는 구조적 일관성을 적용할 때
|
||||||
|
- 린팅, 포맷팅 또는 타입 검사 규칙을 설정할 때
|
||||||
|
- 새 기여자에게 코딩 컨벤션을 안내할 때
|
||||||
|
|
||||||
|
## 코드 품질 원칙
|
||||||
|
|
||||||
|
### 1. 가독성 우선
|
||||||
|
- 코드는 작성보다 읽히는 횟수가 더 많다
|
||||||
|
- 명확한 변수 및 함수 이름 사용
|
||||||
|
- 주석보다 자기 문서화 코드를 선호
|
||||||
|
- 일관된 포맷팅 유지
|
||||||
|
|
||||||
|
### 2. KISS (Keep It Simple, Stupid)
|
||||||
|
- 동작하는 가장 단순한 해결책
|
||||||
|
- 과도한 엔지니어링 지양
|
||||||
|
- 조기 최적화 금지
|
||||||
|
- 이해하기 쉬운 코드 > 영리한 코드
|
||||||
|
|
||||||
|
### 3. DRY (Don't Repeat Yourself)
|
||||||
|
- 공통 로직을 함수로 추출
|
||||||
|
- 재사용 가능한 컴포넌트 생성
|
||||||
|
- 모듈 간 유틸리티 공유
|
||||||
|
- 복사-붙여넣기 프로그래밍 지양
|
||||||
|
|
||||||
|
### 4. YAGNI (You Aren't Gonna Need It)
|
||||||
|
- 필요하기 전에 기능을 만들지 않기
|
||||||
|
- 추측에 의한 일반화 지양
|
||||||
|
- 필요할 때만 복잡성 추가
|
||||||
|
- 단순하게 시작하고 필요할 때 리팩터링
|
||||||
|
|
||||||
|
## TypeScript/JavaScript 표준
|
||||||
|
|
||||||
|
### 변수 네이밍
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Descriptive names
|
||||||
|
const marketSearchQuery = 'election'
|
||||||
|
const isUserAuthenticated = true
|
||||||
|
const totalRevenue = 1000
|
||||||
|
|
||||||
|
// ❌ BAD: Unclear names
|
||||||
|
const q = 'election'
|
||||||
|
const flag = true
|
||||||
|
const x = 1000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 함수 네이밍
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Verb-noun pattern
|
||||||
|
async function fetchMarketData(marketId: string) { }
|
||||||
|
function calculateSimilarity(a: number[], b: number[]) { }
|
||||||
|
function isValidEmail(email: string): boolean { }
|
||||||
|
|
||||||
|
// ❌ BAD: Unclear or noun-only
|
||||||
|
async function market(id: string) { }
|
||||||
|
function similarity(a, b) { }
|
||||||
|
function email(e) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 불변성 패턴 (필수)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ ALWAYS use spread operator
|
||||||
|
const updatedUser = {
|
||||||
|
...user,
|
||||||
|
name: 'New Name'
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedArray = [...items, newItem]
|
||||||
|
|
||||||
|
// ❌ NEVER mutate directly
|
||||||
|
user.name = 'New Name' // BAD
|
||||||
|
items.push(newItem) // BAD
|
||||||
|
```
|
||||||
|
|
||||||
|
### 에러 처리
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Comprehensive error handling
|
||||||
|
async function fetchData(url: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch failed:', error)
|
||||||
|
throw new Error('Failed to fetch data')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: No error handling
|
||||||
|
async function fetchData(url) {
|
||||||
|
const response = await fetch(url)
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async/Await 모범 사례
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Parallel execution when possible
|
||||||
|
const [users, markets, stats] = await Promise.all([
|
||||||
|
fetchUsers(),
|
||||||
|
fetchMarkets(),
|
||||||
|
fetchStats()
|
||||||
|
])
|
||||||
|
|
||||||
|
// ❌ BAD: Sequential when unnecessary
|
||||||
|
const users = await fetchUsers()
|
||||||
|
const markets = await fetchMarkets()
|
||||||
|
const stats = await fetchStats()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 타입 안전성
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Proper types
|
||||||
|
interface Market {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
status: 'active' | 'resolved' | 'closed'
|
||||||
|
created_at: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarket(id: string): Promise<Market> {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Using 'any'
|
||||||
|
function getMarket(id: any): Promise<any> {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## React 모범 사례
|
||||||
|
|
||||||
|
### 컴포넌트 구조
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Functional component with types
|
||||||
|
interface ButtonProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
onClick: () => void
|
||||||
|
disabled?: boolean
|
||||||
|
variant?: 'primary' | 'secondary'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
disabled = false,
|
||||||
|
variant = 'primary'
|
||||||
|
}: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`btn btn-${variant}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: No types, unclear structure
|
||||||
|
export function Button(props) {
|
||||||
|
return <button onClick={props.onClick}>{props.children}</button>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 커스텀 Hook
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Reusable custom hook
|
||||||
|
export function useDebounce<T>(value: T, delay: number): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value)
|
||||||
|
}, delay)
|
||||||
|
|
||||||
|
return () => clearTimeout(handler)
|
||||||
|
}, [value, delay])
|
||||||
|
|
||||||
|
return debouncedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const debouncedQuery = useDebounce(searchQuery, 500)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 상태 관리
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Proper state updates
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
|
// Functional update for state based on previous state
|
||||||
|
setCount(prev => prev + 1)
|
||||||
|
|
||||||
|
// ❌ BAD: Direct state reference
|
||||||
|
setCount(count + 1) // Can be stale in async scenarios
|
||||||
|
```
|
||||||
|
|
||||||
|
### 조건부 렌더링
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Clear conditional rendering
|
||||||
|
{isLoading && <Spinner />}
|
||||||
|
{error && <ErrorMessage error={error} />}
|
||||||
|
{data && <DataDisplay data={data} />}
|
||||||
|
|
||||||
|
// ❌ BAD: Ternary hell
|
||||||
|
{isLoading ? <Spinner /> : error ? <ErrorMessage error={error} /> : data ? <DataDisplay data={data} /> : null}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 설계 표준
|
||||||
|
|
||||||
|
### REST API 컨벤션
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/markets # List all markets
|
||||||
|
GET /api/markets/:id # Get specific market
|
||||||
|
POST /api/markets # Create new market
|
||||||
|
PUT /api/markets/:id # Update market (full)
|
||||||
|
PATCH /api/markets/:id # Update market (partial)
|
||||||
|
DELETE /api/markets/:id # Delete market
|
||||||
|
|
||||||
|
# Query parameters for filtering
|
||||||
|
GET /api/markets?status=active&limit=10&offset=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 응답 형식
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Consistent response structure
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
success: boolean
|
||||||
|
data?: T
|
||||||
|
error?: string
|
||||||
|
meta?: {
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success response
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: markets,
|
||||||
|
meta: { total: 100, page: 1, limit: 10 }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Error response
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid request'
|
||||||
|
}, { status: 400 })
|
||||||
|
```
|
||||||
|
|
||||||
|
### 입력 유효성 검사
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
// ✅ GOOD: Schema validation
|
||||||
|
const CreateMarketSchema = z.object({
|
||||||
|
name: z.string().min(1).max(200),
|
||||||
|
description: z.string().min(1).max(2000),
|
||||||
|
endDate: z.string().datetime(),
|
||||||
|
categories: z.array(z.string()).min(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const body = await request.json()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const validated = CreateMarketSchema.parse(body)
|
||||||
|
// Proceed with validated data
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Validation failed',
|
||||||
|
details: error.errors
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 파일 구성
|
||||||
|
|
||||||
|
### 프로젝트 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── app/ # Next.js App Router
|
||||||
|
│ ├── api/ # API routes
|
||||||
|
│ ├── markets/ # Market pages
|
||||||
|
│ └── (auth)/ # Auth pages (route groups)
|
||||||
|
├── components/ # React components
|
||||||
|
│ ├── ui/ # Generic UI components
|
||||||
|
│ ├── forms/ # Form components
|
||||||
|
│ └── layouts/ # Layout components
|
||||||
|
├── hooks/ # Custom React hooks
|
||||||
|
├── lib/ # Utilities and configs
|
||||||
|
│ ├── api/ # API clients
|
||||||
|
│ ├── utils/ # Helper functions
|
||||||
|
│ └── constants/ # Constants
|
||||||
|
├── types/ # TypeScript types
|
||||||
|
└── styles/ # Global styles
|
||||||
|
```
|
||||||
|
|
||||||
|
### 파일 네이밍
|
||||||
|
|
||||||
|
```
|
||||||
|
components/Button.tsx # PascalCase for components
|
||||||
|
hooks/useAuth.ts # camelCase with 'use' prefix
|
||||||
|
lib/formatDate.ts # camelCase for utilities
|
||||||
|
types/market.types.ts # camelCase with .types suffix
|
||||||
|
```
|
||||||
|
|
||||||
|
## 주석 및 문서화
|
||||||
|
|
||||||
|
### 주석을 작성해야 하는 경우
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Explain WHY, not WHAT
|
||||||
|
// Use exponential backoff to avoid overwhelming the API during outages
|
||||||
|
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000)
|
||||||
|
|
||||||
|
// Deliberately using mutation here for performance with large arrays
|
||||||
|
items.push(newItem)
|
||||||
|
|
||||||
|
// ❌ BAD: Stating the obvious
|
||||||
|
// Increment counter by 1
|
||||||
|
count++
|
||||||
|
|
||||||
|
// Set name to user's name
|
||||||
|
name = user.name
|
||||||
|
```
|
||||||
|
|
||||||
|
### 공개 API를 위한 JSDoc
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Searches markets using semantic similarity.
|
||||||
|
*
|
||||||
|
* @param query - Natural language search query
|
||||||
|
* @param limit - Maximum number of results (default: 10)
|
||||||
|
* @returns Array of markets sorted by similarity score
|
||||||
|
* @throws {Error} If OpenAI API fails or Redis unavailable
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const results = await searchMarkets('election', 5)
|
||||||
|
* console.log(results[0].name) // "Trump vs Biden"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function searchMarkets(
|
||||||
|
query: string,
|
||||||
|
limit: number = 10
|
||||||
|
): Promise<Market[]> {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 성능 모범 사례
|
||||||
|
|
||||||
|
### 메모이제이션
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useMemo, useCallback } from 'react'
|
||||||
|
|
||||||
|
// ✅ GOOD: Memoize expensive computations
|
||||||
|
const sortedMarkets = useMemo(() => {
|
||||||
|
return markets.sort((a, b) => b.volume - a.volume)
|
||||||
|
}, [markets])
|
||||||
|
|
||||||
|
// ✅ GOOD: Memoize callbacks
|
||||||
|
const handleSearch = useCallback((query: string) => {
|
||||||
|
setSearchQuery(query)
|
||||||
|
}, [])
|
||||||
|
```
|
||||||
|
|
||||||
|
### 지연 로딩
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { lazy, Suspense } from 'react'
|
||||||
|
|
||||||
|
// ✅ GOOD: Lazy load heavy components
|
||||||
|
const HeavyChart = lazy(() => import('./HeavyChart'))
|
||||||
|
|
||||||
|
export function Dashboard() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<HeavyChart />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 데이터베이스 쿼리
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Select only needed columns
|
||||||
|
const { data } = await supabase
|
||||||
|
.from('markets')
|
||||||
|
.select('id, name, status')
|
||||||
|
.limit(10)
|
||||||
|
|
||||||
|
// ❌ BAD: Select everything
|
||||||
|
const { data } = await supabase
|
||||||
|
.from('markets')
|
||||||
|
.select('*')
|
||||||
|
```
|
||||||
|
|
||||||
|
## 테스트 표준
|
||||||
|
|
||||||
|
### 테스트 구조 (AAA 패턴)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
test('calculates similarity correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const vector1 = [1, 0, 0]
|
||||||
|
const vector2 = [0, 1, 0]
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const similarity = calculateCosineSimilarity(vector1, vector2)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(similarity).toBe(0)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 테스트 네이밍
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Descriptive test names
|
||||||
|
test('returns empty array when no markets match query', () => { })
|
||||||
|
test('throws error when OpenAI API key is missing', () => { })
|
||||||
|
test('falls back to substring search when Redis unavailable', () => { })
|
||||||
|
|
||||||
|
// ❌ BAD: Vague test names
|
||||||
|
test('works', () => { })
|
||||||
|
test('test search', () => { })
|
||||||
|
```
|
||||||
|
|
||||||
|
## 코드 스멜 감지
|
||||||
|
|
||||||
|
다음 안티패턴을 주의하세요:
|
||||||
|
|
||||||
|
### 1. 긴 함수
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: Function > 50 lines
|
||||||
|
function processMarketData() {
|
||||||
|
// 100 lines of code
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: Split into smaller functions
|
||||||
|
function processMarketData() {
|
||||||
|
const validated = validateData()
|
||||||
|
const transformed = transformData(validated)
|
||||||
|
return saveData(transformed)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 깊은 중첩
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: 5+ levels of nesting
|
||||||
|
if (user) {
|
||||||
|
if (user.isAdmin) {
|
||||||
|
if (market) {
|
||||||
|
if (market.isActive) {
|
||||||
|
if (hasPermission) {
|
||||||
|
// Do something
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: Early returns
|
||||||
|
if (!user) return
|
||||||
|
if (!user.isAdmin) return
|
||||||
|
if (!market) return
|
||||||
|
if (!market.isActive) return
|
||||||
|
if (!hasPermission) return
|
||||||
|
|
||||||
|
// Do something
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 매직 넘버
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: Unexplained numbers
|
||||||
|
if (retryCount > 3) { }
|
||||||
|
setTimeout(callback, 500)
|
||||||
|
|
||||||
|
// ✅ GOOD: Named constants
|
||||||
|
const MAX_RETRIES = 3
|
||||||
|
const DEBOUNCE_DELAY_MS = 500
|
||||||
|
|
||||||
|
if (retryCount > MAX_RETRIES) { }
|
||||||
|
setTimeout(callback, DEBOUNCE_DELAY_MS)
|
||||||
|
```
|
||||||
|
|
||||||
|
**기억하세요**: 코드 품질은 타협할 수 없습니다. 명확하고 유지보수 가능한 코드가 빠른 개발과 자신감 있는 리팩터링을 가능하게 합니다.
|
||||||
363
docs/ko-KR/skills/continuous-learning-v2/SKILL.md
Normal file
363
docs/ko-KR/skills/continuous-learning-v2/SKILL.md
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
---
|
||||||
|
name: continuous-learning-v2
|
||||||
|
description: 훅을 통해 세션을 관찰하고, 신뢰도 점수가 있는 원자적 본능을 생성하며, 이를 스킬/명령어/에이전트로 진화시키는 본능 기반 학습 시스템. v2.1에서는 프로젝트 간 오염을 방지하기 위한 프로젝트 범위 본능이 추가되었습니다.
|
||||||
|
origin: ECC
|
||||||
|
version: 2.1.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# 지속적 학습 v2.1 - 본능 기반 아키텍처
|
||||||
|
|
||||||
|
Claude Code 세션을 원자적 "본능(instinct)" -- 신뢰도 점수가 있는 작은 학습된 행동 -- 을 통해 재사용 가능한 지식으로 변환하는 고급 학습 시스템입니다.
|
||||||
|
|
||||||
|
**v2.1**에서는 **프로젝트 범위 본능**이 추가되었습니다 -- React 패턴은 React 프로젝트에, Python 규칙은 Python 프로젝트에 유지되며, 범용 패턴(예: "항상 입력 유효성 검사")은 전역으로 공유됩니다.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- Claude Code 세션에서 자동 학습 설정 시
|
||||||
|
- 훅을 통한 본능 기반 행동 추출 구성 시
|
||||||
|
- 학습된 행동의 신뢰도 임계값 조정 시
|
||||||
|
- 본능 라이브러리 검토, 내보내기, 가져오기 시
|
||||||
|
- 본능을 완전한 스킬, 명령어 또는 에이전트로 진화 시
|
||||||
|
- 프로젝트 범위 vs 전역 본능 관리 시
|
||||||
|
- 프로젝트에서 전역 범위로 본능 승격 시
|
||||||
|
|
||||||
|
## v2.1의 새로운 기능
|
||||||
|
|
||||||
|
| 기능 | v2.0 | v2.1 |
|
||||||
|
|---------|------|------|
|
||||||
|
| 저장소 | 전역 (~/.claude/homunculus/) | 프로젝트 범위 (projects/<hash>/) |
|
||||||
|
| 범위 | 모든 본능이 어디서나 적용 | 프로젝트 범위 + 전역 |
|
||||||
|
| 감지 | 없음 | git remote URL / 저장소 경로 |
|
||||||
|
| 승격 | 해당 없음 | 2개 이상 프로젝트에서 확인 시 프로젝트 -> 전역 |
|
||||||
|
| 명령어 | 4개 (status/evolve/export/import) | 6개 (+promote/projects) |
|
||||||
|
| 프로젝트 간 | 오염 위험 | 기본적으로 격리 |
|
||||||
|
|
||||||
|
## v2의 새로운 기능 (v1 대비)
|
||||||
|
|
||||||
|
| 기능 | v1 | v2 |
|
||||||
|
|---------|----|----|
|
||||||
|
| 관찰 | Stop 훅 (세션 종료) | PreToolUse/PostToolUse (100% 신뢰성) |
|
||||||
|
| 분석 | 메인 컨텍스트 | 백그라운드 에이전트 (Haiku) |
|
||||||
|
| 세분성 | 전체 스킬 | 원자적 "본능" |
|
||||||
|
| 신뢰도 | 없음 | 0.3-0.9 가중치 |
|
||||||
|
| 진화 | 직접 스킬로 | 본능 -> 클러스터 -> 스킬/명령어/에이전트 |
|
||||||
|
| 공유 | 없음 | 본능 내보내기/가져오기 |
|
||||||
|
|
||||||
|
## 본능 모델
|
||||||
|
|
||||||
|
본능은 작은 학습된 행동입니다:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
id: prefer-functional-style
|
||||||
|
trigger: "when writing new functions"
|
||||||
|
confidence: 0.7
|
||||||
|
domain: "code-style"
|
||||||
|
source: "session-observation"
|
||||||
|
scope: project
|
||||||
|
project_id: "a1b2c3d4e5f6"
|
||||||
|
project_name: "my-react-app"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prefer Functional Style
|
||||||
|
|
||||||
|
## Action
|
||||||
|
Use functional patterns over classes when appropriate.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
- Observed 5 instances of functional pattern preference
|
||||||
|
- User corrected class-based approach to functional on 2025-01-15
|
||||||
|
```
|
||||||
|
|
||||||
|
**속성:**
|
||||||
|
- **원자적** -- 하나의 트리거, 하나의 액션
|
||||||
|
- **신뢰도 가중치** -- 0.3 = 잠정적, 0.9 = 거의 확실
|
||||||
|
- **도메인 태그** -- code-style, testing, git, debugging, workflow 등
|
||||||
|
- **증거 기반** -- 어떤 관찰이 이를 생성했는지 추적
|
||||||
|
- **범위 인식** -- `project` (기본값) 또는 `global`
|
||||||
|
|
||||||
|
## 작동 방식
|
||||||
|
|
||||||
|
```
|
||||||
|
세션 활동 (git 저장소 내)
|
||||||
|
|
|
||||||
|
| 훅이 프롬프트 + 도구 사용을 캡처 (100% 신뢰성)
|
||||||
|
| + 프로젝트 컨텍스트 감지 (git remote / 저장소 경로)
|
||||||
|
v
|
||||||
|
+---------------------------------------------+
|
||||||
|
| projects/<project-hash>/observations.jsonl |
|
||||||
|
| (프롬프트, 도구 호출, 결과, 프로젝트) |
|
||||||
|
+---------------------------------------------+
|
||||||
|
|
|
||||||
|
| 관찰자 에이전트가 읽기 (백그라운드, Haiku)
|
||||||
|
v
|
||||||
|
+---------------------------------------------+
|
||||||
|
| 패턴 감지 |
|
||||||
|
| * 사용자 수정 -> 본능 |
|
||||||
|
| * 에러 해결 -> 본능 |
|
||||||
|
| * 반복 워크플로우 -> 본능 |
|
||||||
|
| * 범위 결정: 프로젝트 또는 전역? |
|
||||||
|
+---------------------------------------------+
|
||||||
|
|
|
||||||
|
| 생성/업데이트
|
||||||
|
v
|
||||||
|
+---------------------------------------------+
|
||||||
|
| projects/<project-hash>/instincts/personal/ |
|
||||||
|
| * prefer-functional.yaml (0.7) [project] |
|
||||||
|
| * use-react-hooks.yaml (0.9) [project] |
|
||||||
|
+---------------------------------------------+
|
||||||
|
| instincts/personal/ (전역) |
|
||||||
|
| * always-validate-input.yaml (0.85) [global]|
|
||||||
|
| * grep-before-edit.yaml (0.6) [global] |
|
||||||
|
+---------------------------------------------+
|
||||||
|
|
|
||||||
|
| /evolve 클러스터링 + /promote
|
||||||
|
v
|
||||||
|
+---------------------------------------------+
|
||||||
|
| projects/<hash>/evolved/ (프로젝트 범위) |
|
||||||
|
| evolved/ (전역) |
|
||||||
|
| * commands/new-feature.md |
|
||||||
|
| * skills/testing-workflow.md |
|
||||||
|
| * agents/refactor-specialist.md |
|
||||||
|
+---------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
## 프로젝트 감지
|
||||||
|
|
||||||
|
시스템이 현재 프로젝트를 자동으로 감지합니다:
|
||||||
|
|
||||||
|
1. **`CLAUDE_PROJECT_DIR` 환경 변수** (최우선 순위)
|
||||||
|
2. **`git remote get-url origin`** -- 이식 가능한 프로젝트 ID를 생성하기 위해 해시됨 (서로 다른 머신에서 같은 저장소는 같은 ID를 가짐)
|
||||||
|
3. **`git rev-parse --show-toplevel`** -- 저장소 경로를 사용한 폴백 (머신별)
|
||||||
|
4. **전역 폴백** -- 프로젝트가 감지되지 않으면 본능은 전역 범위로 이동
|
||||||
|
|
||||||
|
각 프로젝트는 12자 해시 ID를 받습니다 (예: `a1b2c3d4e5f6`). `~/.claude/homunculus/projects.json`의 레지스트리 파일이 ID를 사람이 읽을 수 있는 이름에 매핑합니다.
|
||||||
|
|
||||||
|
## 빠른 시작
|
||||||
|
|
||||||
|
### 1. 관찰 훅 활성화
|
||||||
|
|
||||||
|
`~/.claude/settings.json`에 추가하세요.
|
||||||
|
|
||||||
|
**플러그인으로 설치한 경우** (권장):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [{
|
||||||
|
"matcher": "*",
|
||||||
|
"hooks": [{
|
||||||
|
"type": "command",
|
||||||
|
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh"
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
"PostToolUse": [{
|
||||||
|
"matcher": "*",
|
||||||
|
"hooks": [{
|
||||||
|
"type": "command",
|
||||||
|
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh"
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**수동으로 `~/.claude/skills`에 설치한 경우**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [{
|
||||||
|
"matcher": "*",
|
||||||
|
"hooks": [{
|
||||||
|
"type": "command",
|
||||||
|
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh"
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
"PostToolUse": [{
|
||||||
|
"matcher": "*",
|
||||||
|
"hooks": [{
|
||||||
|
"type": "command",
|
||||||
|
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh"
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 디렉터리 구조 초기화
|
||||||
|
|
||||||
|
시스템은 첫 사용 시 자동으로 디렉터리를 생성하지만, 수동으로도 생성할 수 있습니다:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Global directories
|
||||||
|
mkdir -p ~/.claude/homunculus/{instincts/{personal,inherited},evolved/{agents,skills,commands},projects}
|
||||||
|
|
||||||
|
# Project directories are auto-created when the hook first runs in a git repo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 본능 명령어 사용
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/instinct-status # 학습된 본능 표시 (프로젝트 + 전역)
|
||||||
|
/evolve # 관련 본능을 스킬/명령어로 클러스터링
|
||||||
|
/instinct-export # 본능을 파일로 내보내기
|
||||||
|
/instinct-import # 다른 사람의 본능 가져오기
|
||||||
|
/promote # 프로젝트 본능을 전역 범위로 승격
|
||||||
|
/projects # 모든 알려진 프로젝트와 본능 개수 목록
|
||||||
|
```
|
||||||
|
|
||||||
|
## 명령어
|
||||||
|
|
||||||
|
| 명령어 | 설명 |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/instinct-status` | 모든 본능 (프로젝트 범위 + 전역) 을 신뢰도와 함께 표시 |
|
||||||
|
| `/evolve` | 관련 본능을 스킬/명령어로 클러스터링, 승격 제안 |
|
||||||
|
| `/instinct-export` | 본능 내보내기 (범위/도메인으로 필터링 가능) |
|
||||||
|
| `/instinct-import <file>` | 범위 제어와 함께 본능 가져오기 |
|
||||||
|
| `/promote [id]` | 프로젝트 본능을 전역 범위로 승격 |
|
||||||
|
| `/projects` | 모든 알려진 프로젝트와 본능 개수 목록 |
|
||||||
|
|
||||||
|
## 구성
|
||||||
|
|
||||||
|
백그라운드 관찰자를 제어하려면 `config.json`을 편집하세요:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "2.1",
|
||||||
|
"observer": {
|
||||||
|
"enabled": false,
|
||||||
|
"run_interval_minutes": 5,
|
||||||
|
"min_observations_to_analyze": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 키 | 기본값 | 설명 |
|
||||||
|
|-----|---------|-------------|
|
||||||
|
| `observer.enabled` | `false` | 백그라운드 관찰자 에이전트 활성화 |
|
||||||
|
| `observer.run_interval_minutes` | `5` | 관찰자가 관찰 결과를 분석하는 빈도 |
|
||||||
|
| `observer.min_observations_to_analyze` | `20` | 분석 실행 전 최소 관찰 횟수 |
|
||||||
|
|
||||||
|
기타 동작 (관찰 캡처, 본능 임계값, 프로젝트 범위, 승격 기준)은 `instinct-cli.py`와 `observe.sh`의 코드 기본값으로 구성됩니다.
|
||||||
|
|
||||||
|
## 파일 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.claude/homunculus/
|
||||||
|
+-- identity.json # 프로필, 기술 수준
|
||||||
|
+-- projects.json # 레지스트리: 프로젝트 해시 -> 이름/경로/리모트
|
||||||
|
+-- observations.jsonl # 전역 관찰 결과 (폴백)
|
||||||
|
+-- instincts/
|
||||||
|
| +-- personal/ # 전역 자동 학습된 본능
|
||||||
|
| +-- inherited/ # 전역 가져온 본능
|
||||||
|
+-- evolved/
|
||||||
|
| +-- agents/ # 전역 생성된 에이전트
|
||||||
|
| +-- skills/ # 전역 생성된 스킬
|
||||||
|
| +-- commands/ # 전역 생성된 명령어
|
||||||
|
+-- projects/
|
||||||
|
+-- a1b2c3d4e5f6/ # 프로젝트 해시 (git remote URL에서)
|
||||||
|
| +-- observations.jsonl
|
||||||
|
| +-- observations.archive/
|
||||||
|
| +-- instincts/
|
||||||
|
| | +-- personal/ # 프로젝트별 자동 학습
|
||||||
|
| | +-- inherited/ # 프로젝트별 가져온 것
|
||||||
|
| +-- evolved/
|
||||||
|
| +-- skills/
|
||||||
|
| +-- commands/
|
||||||
|
| +-- agents/
|
||||||
|
+-- f6e5d4c3b2a1/ # 다른 프로젝트
|
||||||
|
+-- ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 범위 결정 가이드
|
||||||
|
|
||||||
|
| 패턴 유형 | 범위 | 예시 |
|
||||||
|
|-------------|-------|---------|
|
||||||
|
| 언어/프레임워크 규칙 | **project** | "React hooks 사용", "Django REST 패턴 따르기" |
|
||||||
|
| 파일 구조 선호도 | **project** | "`__tests__`/에 테스트", "src/components/에 컴포넌트" |
|
||||||
|
| 코드 스타일 | **project** | "함수형 스타일 사용", "dataclasses 선호" |
|
||||||
|
| 에러 처리 전략 | **project** | "에러에 Result 타입 사용" |
|
||||||
|
| 보안 관행 | **global** | "사용자 입력 유효성 검사", "SQL 새니타이징" |
|
||||||
|
| 일반 모범 사례 | **global** | "테스트 먼저 작성", "항상 에러 처리" |
|
||||||
|
| 도구 워크플로우 선호도 | **global** | "편집 전 Grep", "쓰기 전 Read" |
|
||||||
|
| Git 관행 | **global** | "Conventional commits", "작고 집중된 커밋" |
|
||||||
|
|
||||||
|
## 본능 승격 (프로젝트 -> 전역)
|
||||||
|
|
||||||
|
같은 본능이 높은 신뢰도로 여러 프로젝트에 나타나면, 전역 범위로 승격할 후보가 됩니다.
|
||||||
|
|
||||||
|
**자동 승격 기준:**
|
||||||
|
- 2개 이상 프로젝트에서 같은 본능 ID
|
||||||
|
- 평균 신뢰도 >= 0.8
|
||||||
|
|
||||||
|
**승격 방법:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Promote a specific instinct
|
||||||
|
python3 instinct-cli.py promote prefer-explicit-errors
|
||||||
|
|
||||||
|
# Auto-promote all qualifying instincts
|
||||||
|
python3 instinct-cli.py promote
|
||||||
|
|
||||||
|
# Preview without changes
|
||||||
|
python3 instinct-cli.py promote --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
`/evolve` 명령어도 승격 후보를 제안합니다.
|
||||||
|
|
||||||
|
## 신뢰도 점수
|
||||||
|
|
||||||
|
신뢰도는 시간이 지남에 따라 진화합니다:
|
||||||
|
|
||||||
|
| 점수 | 의미 | 동작 |
|
||||||
|
|-------|---------|----------|
|
||||||
|
| 0.3 | 잠정적 | 제안되지만 강제되지 않음 |
|
||||||
|
| 0.5 | 보통 | 관련 시 적용 |
|
||||||
|
| 0.7 | 강함 | 적용이 자동 승인됨 |
|
||||||
|
| 0.9 | 거의 확실 | 핵심 행동 |
|
||||||
|
|
||||||
|
**신뢰도가 증가하는 경우:**
|
||||||
|
- 패턴이 반복적으로 관찰됨
|
||||||
|
- 사용자가 제안된 행동을 수정하지 않음
|
||||||
|
- 다른 소스의 유사한 본능이 동의함
|
||||||
|
|
||||||
|
**신뢰도가 감소하는 경우:**
|
||||||
|
- 사용자가 행동을 명시적으로 수정함
|
||||||
|
- 패턴이 오랜 기간 관찰되지 않음
|
||||||
|
- 모순되는 증거가 나타남
|
||||||
|
|
||||||
|
## 왜 관찰에 스킬이 아닌 훅을 사용하나요?
|
||||||
|
|
||||||
|
> "v1은 관찰에 스킬을 의존했습니다. 스킬은 확률적입니다 -- Claude의 판단에 따라 약 50-80%의 확률로 실행됩니다."
|
||||||
|
|
||||||
|
훅은 **100% 확률로** 결정적으로 실행됩니다. 이는 다음을 의미합니다:
|
||||||
|
- 모든 도구 호출이 관찰됨
|
||||||
|
- 패턴이 누락되지 않음
|
||||||
|
- 학습이 포괄적임
|
||||||
|
|
||||||
|
## 하위 호환성
|
||||||
|
|
||||||
|
v2.1은 v2.0 및 v1과 완전히 호환됩니다:
|
||||||
|
- `~/.claude/homunculus/instincts/`의 기존 전역 본능이 전역 본능으로 계속 작동
|
||||||
|
- v1의 기존 `~/.claude/skills/learned/` 스킬이 계속 작동
|
||||||
|
- Stop 훅이 여전히 실행됨 (하지만 이제 v2에도 데이터를 공급)
|
||||||
|
- 점진적 마이그레이션: 둘 다 병렬로 실행 가능
|
||||||
|
|
||||||
|
## 개인정보 보호
|
||||||
|
|
||||||
|
- 관찰 결과는 사용자의 머신에 **로컬**로 유지
|
||||||
|
- 프로젝트 범위 본능은 프로젝트별로 격리됨
|
||||||
|
- **본능**(패턴)만 내보낼 수 있음 -- 원시 관찰 결과는 아님
|
||||||
|
- 실제 코드나 대화 내용은 공유되지 않음
|
||||||
|
- 내보내기와 승격 대상을 사용자가 제어
|
||||||
|
|
||||||
|
## 관련 자료
|
||||||
|
|
||||||
|
- [Skill Creator](https://skill-creator.app) - 저장소 히스토리에서 본능 생성
|
||||||
|
- Homunculus - v2 본능 기반 아키텍처에 영감을 준 커뮤니티 프로젝트 (원자적 관찰, 신뢰도 점수, 본능 진화 파이프라인)
|
||||||
|
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - 지속적 학습 섹션
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*본능 기반 학습: Claude에게 당신의 패턴을 가르치기, 한 번에 하나의 프로젝트씩.*
|
||||||
119
docs/ko-KR/skills/continuous-learning/SKILL.md
Normal file
119
docs/ko-KR/skills/continuous-learning/SKILL.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
---
|
||||||
|
name: continuous-learning
|
||||||
|
description: Claude Code 세션에서 재사용 가능한 패턴을 자동으로 추출하여 향후 사용을 위한 학습된 스킬로 저장합니다.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# 지속적 학습 스킬
|
||||||
|
|
||||||
|
Claude Code 세션 종료 시 자동으로 평가하여 학습된 스킬로 저장할 수 있는 재사용 가능한 패턴을 추출합니다.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- Claude Code 세션에서 자동 패턴 추출을 설정할 때
|
||||||
|
- 세션 평가를 위한 Stop Hook을 구성할 때
|
||||||
|
- `~/.claude/skills/learned/`에서 학습된 스킬을 검토하거나 큐레이션할 때
|
||||||
|
- 추출 임계값이나 패턴 카테고리를 조정할 때
|
||||||
|
- v1 (이 방식)과 v2 (본능 기반) 접근법을 비교할 때
|
||||||
|
|
||||||
|
## 작동 방식
|
||||||
|
|
||||||
|
이 스킬은 각 세션 종료 시 **Stop Hook**으로 실행됩니다:
|
||||||
|
|
||||||
|
1. **세션 평가**: 세션에 충분한 메시지가 있는지 확인 (기본값: 10개 이상)
|
||||||
|
2. **패턴 감지**: 세션에서 추출 가능한 패턴을 식별
|
||||||
|
3. **스킬 추출**: 유용한 패턴을 `~/.claude/skills/learned/`에 저장
|
||||||
|
|
||||||
|
## 구성
|
||||||
|
|
||||||
|
`config.json`을 편집하여 사용자 지정합니다:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"min_session_length": 10,
|
||||||
|
"extraction_threshold": "medium",
|
||||||
|
"auto_approve": false,
|
||||||
|
"learned_skills_path": "~/.claude/skills/learned/",
|
||||||
|
"patterns_to_detect": [
|
||||||
|
"error_resolution",
|
||||||
|
"user_corrections",
|
||||||
|
"workarounds",
|
||||||
|
"debugging_techniques",
|
||||||
|
"project_specific"
|
||||||
|
],
|
||||||
|
"ignore_patterns": [
|
||||||
|
"simple_typos",
|
||||||
|
"one_time_fixes",
|
||||||
|
"external_api_issues"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 패턴 유형
|
||||||
|
|
||||||
|
| 패턴 | 설명 |
|
||||||
|
|---------|-------------|
|
||||||
|
| `error_resolution` | 특정 에러가 어떻게 해결되었는지 |
|
||||||
|
| `user_corrections` | 사용자 수정으로부터의 패턴 |
|
||||||
|
| `workarounds` | 프레임워크/라이브러리 특이점에 대한 해결책 |
|
||||||
|
| `debugging_techniques` | 효과적인 디버깅 접근법 |
|
||||||
|
| `project_specific` | 프로젝트 고유 컨벤션 |
|
||||||
|
|
||||||
|
## Hook 설정
|
||||||
|
|
||||||
|
`~/.claude/settings.json`에 추가합니다:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"Stop": [{
|
||||||
|
"matcher": "*",
|
||||||
|
"hooks": [{
|
||||||
|
"type": "command",
|
||||||
|
"command": "~/.claude/skills/continuous-learning/evaluate-session.sh"
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stop Hook을 사용하는 이유
|
||||||
|
|
||||||
|
- **경량**: 세션 종료 시 한 번만 실행
|
||||||
|
- **비차단**: 모든 메시지에 지연을 추가하지 않음
|
||||||
|
- **완전한 컨텍스트**: 전체 세션 트랜스크립트에 접근 가능
|
||||||
|
|
||||||
|
## 관련 항목
|
||||||
|
|
||||||
|
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - 지속적 학습 섹션
|
||||||
|
- `/learn` 명령어 - 세션 중 수동 패턴 추출
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 비교 노트 (연구: 2025년 1월)
|
||||||
|
|
||||||
|
### vs Homunculus
|
||||||
|
|
||||||
|
Homunculus v2는 더 정교한 접근법을 취합니다:
|
||||||
|
|
||||||
|
| 기능 | 우리의 접근법 | Homunculus v2 |
|
||||||
|
|---------|--------------|---------------|
|
||||||
|
| 관찰 | Stop Hook (세션 종료 시) | PreToolUse/PostToolUse Hook (100% 신뢰) |
|
||||||
|
| 분석 | 메인 컨텍스트 | 백그라운드 에이전트 (Haiku) |
|
||||||
|
| 세분성 | 완전한 스킬 | 원자적 "본능" |
|
||||||
|
| 신뢰도 | 없음 | 0.3-0.9 가중치 |
|
||||||
|
| 진화 | 스킬로 직접 | 본능 -> 클러스터 -> 스킬/명령어/에이전트 |
|
||||||
|
| 공유 | 없음 | 본능 내보내기/가져오기 |
|
||||||
|
|
||||||
|
**Homunculus의 핵심 통찰:**
|
||||||
|
> "v1은 관찰을 스킬에 의존했습니다. 스킬은 확률적이어서 약 50-80%의 확률로 실행됩니다. v2는 관찰에 Hook(100% 신뢰)을 사용하고 본능을 학습된 행동의 원자 단위로 사용합니다."
|
||||||
|
|
||||||
|
### 잠재적 v2 개선 사항
|
||||||
|
|
||||||
|
1. **본능 기반 학습** - 신뢰도 점수가 있는 더 작고 원자적인 행동
|
||||||
|
2. **백그라운드 관찰자** - 병렬로 분석하는 Haiku 에이전트
|
||||||
|
3. **신뢰도 감쇠** - 반박 시 본능의 신뢰도 감소
|
||||||
|
4. **도메인 태깅** - code-style, testing, git, debugging 등
|
||||||
|
5. **진화 경로** - 관련 본능을 스킬/명령어로 클러스터링
|
||||||
|
|
||||||
|
자세한 사양은 `docs/continuous-learning-v2-spec.md`를 참조하세요.
|
||||||
270
docs/ko-KR/skills/eval-harness/SKILL.md
Normal file
270
docs/ko-KR/skills/eval-harness/SKILL.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
---
|
||||||
|
name: eval-harness
|
||||||
|
description: 평가 주도 개발(EDD) 원칙을 구현하는 Claude Code 세션용 공식 평가 프레임워크
|
||||||
|
origin: ECC
|
||||||
|
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# 평가 하네스 스킬
|
||||||
|
|
||||||
|
Claude Code 세션을 위한 공식 평가 프레임워크로, 평가 주도 개발(EDD) 원칙을 구현합니다.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- AI 지원 워크플로우에 평가 주도 개발(EDD) 설정 시
|
||||||
|
- Claude Code 작업 완료에 대한 합격/불합격 기준 정의 시
|
||||||
|
- pass@k 메트릭으로 에이전트 신뢰성 측정 시
|
||||||
|
- 프롬프트 또는 에이전트 변경에 대한 회귀 테스트 스위트 생성 시
|
||||||
|
- 모델 버전 간 에이전트 성능 벤치마킹 시
|
||||||
|
|
||||||
|
## 철학
|
||||||
|
|
||||||
|
평가 주도 개발은 평가를 "AI 개발의 단위 테스트"로 취급합니다:
|
||||||
|
- 구현 전에 예상 동작 정의
|
||||||
|
- 개발 중 지속적으로 평가 실행
|
||||||
|
- 각 변경 시 회귀 추적
|
||||||
|
- 신뢰성 측정을 위해 pass@k 메트릭 사용
|
||||||
|
|
||||||
|
## 평가 유형
|
||||||
|
|
||||||
|
### 기능 평가
|
||||||
|
Claude가 이전에 할 수 없었던 것을 할 수 있는지 테스트:
|
||||||
|
```markdown
|
||||||
|
[CAPABILITY EVAL: feature-name]
|
||||||
|
Task: Description of what Claude should accomplish
|
||||||
|
Success Criteria:
|
||||||
|
- [ ] Criterion 1
|
||||||
|
- [ ] Criterion 2
|
||||||
|
- [ ] Criterion 3
|
||||||
|
Expected Output: Description of expected result
|
||||||
|
```
|
||||||
|
|
||||||
|
### 회귀 평가
|
||||||
|
변경 사항이 기존 기능을 손상시키지 않는지 확인:
|
||||||
|
```markdown
|
||||||
|
[REGRESSION EVAL: feature-name]
|
||||||
|
Baseline: SHA or checkpoint name
|
||||||
|
Tests:
|
||||||
|
- existing-test-1: PASS/FAIL
|
||||||
|
- existing-test-2: PASS/FAIL
|
||||||
|
- existing-test-3: PASS/FAIL
|
||||||
|
Result: X/Y passed (previously Y/Y)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 채점자 유형
|
||||||
|
|
||||||
|
### 1. 코드 기반 채점자
|
||||||
|
코드를 사용한 결정론적 검사:
|
||||||
|
```bash
|
||||||
|
# Check if file contains expected pattern
|
||||||
|
grep -q "export function handleAuth" src/auth.ts && echo "PASS" || echo "FAIL"
|
||||||
|
|
||||||
|
# Check if tests pass
|
||||||
|
npm test -- --testPathPattern="auth" && echo "PASS" || echo "FAIL"
|
||||||
|
|
||||||
|
# Check if build succeeds
|
||||||
|
npm run build && echo "PASS" || echo "FAIL"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 모델 기반 채점자
|
||||||
|
Claude를 사용하여 개방형 출력 평가:
|
||||||
|
```markdown
|
||||||
|
[MODEL GRADER PROMPT]
|
||||||
|
Evaluate the following code change:
|
||||||
|
1. Does it solve the stated problem?
|
||||||
|
2. Is it well-structured?
|
||||||
|
3. Are edge cases handled?
|
||||||
|
4. Is error handling appropriate?
|
||||||
|
|
||||||
|
Score: 1-5 (1=poor, 5=excellent)
|
||||||
|
Reasoning: [explanation]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 사람 채점자
|
||||||
|
수동 검토 플래그:
|
||||||
|
```markdown
|
||||||
|
[HUMAN REVIEW REQUIRED]
|
||||||
|
Change: Description of what changed
|
||||||
|
Reason: Why human review is needed
|
||||||
|
Risk Level: LOW/MEDIUM/HIGH
|
||||||
|
```
|
||||||
|
|
||||||
|
## 메트릭
|
||||||
|
|
||||||
|
### pass@k
|
||||||
|
"k번 시도 중 최소 한 번 성공"
|
||||||
|
- pass@1: 첫 번째 시도 성공률
|
||||||
|
- pass@3: 3번 시도 내 성공
|
||||||
|
- 일반적인 목표: pass@3 > 90%
|
||||||
|
|
||||||
|
### pass^k
|
||||||
|
"k번 시행 모두 성공"
|
||||||
|
- 신뢰성에 대한 더 높은 기준
|
||||||
|
- pass^3: 3회 연속 성공
|
||||||
|
- 핵심 경로에 사용
|
||||||
|
|
||||||
|
## 평가 워크플로우
|
||||||
|
|
||||||
|
### 1. 정의 (코딩 전)
|
||||||
|
```markdown
|
||||||
|
## EVAL DEFINITION: feature-xyz
|
||||||
|
|
||||||
|
### Capability Evals
|
||||||
|
1. Can create new user account
|
||||||
|
2. Can validate email format
|
||||||
|
3. Can hash password securely
|
||||||
|
|
||||||
|
### Regression Evals
|
||||||
|
1. Existing login still works
|
||||||
|
2. Session management unchanged
|
||||||
|
3. Logout flow intact
|
||||||
|
|
||||||
|
### Success Metrics
|
||||||
|
- pass@3 > 90% for capability evals
|
||||||
|
- pass^3 = 100% for regression evals
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 구현
|
||||||
|
정의된 평가를 통과하기 위한 코드 작성.
|
||||||
|
|
||||||
|
### 3. 평가
|
||||||
|
```bash
|
||||||
|
# Run capability evals
|
||||||
|
[Run each capability eval, record PASS/FAIL]
|
||||||
|
|
||||||
|
# Run regression evals
|
||||||
|
npm test -- --testPathPattern="existing"
|
||||||
|
|
||||||
|
# Generate report
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 보고서
|
||||||
|
```markdown
|
||||||
|
EVAL REPORT: feature-xyz
|
||||||
|
========================
|
||||||
|
|
||||||
|
Capability Evals:
|
||||||
|
create-user: PASS (pass@1)
|
||||||
|
validate-email: PASS (pass@2)
|
||||||
|
hash-password: PASS (pass@1)
|
||||||
|
Overall: 3/3 passed
|
||||||
|
|
||||||
|
Regression Evals:
|
||||||
|
login-flow: PASS
|
||||||
|
session-mgmt: PASS
|
||||||
|
logout-flow: PASS
|
||||||
|
Overall: 3/3 passed
|
||||||
|
|
||||||
|
Metrics:
|
||||||
|
pass@1: 67% (2/3)
|
||||||
|
pass@3: 100% (3/3)
|
||||||
|
|
||||||
|
Status: READY FOR REVIEW
|
||||||
|
```
|
||||||
|
|
||||||
|
## 통합 패턴
|
||||||
|
|
||||||
|
### 구현 전
|
||||||
|
```
|
||||||
|
/eval define feature-name
|
||||||
|
```
|
||||||
|
`.claude/evals/feature-name.md`에 평가 정의 파일 생성
|
||||||
|
|
||||||
|
### 구현 중
|
||||||
|
```
|
||||||
|
/eval check feature-name
|
||||||
|
```
|
||||||
|
현재 평가를 실행하고 상태 보고
|
||||||
|
|
||||||
|
### 구현 후
|
||||||
|
```
|
||||||
|
/eval report feature-name
|
||||||
|
```
|
||||||
|
전체 평가 보고서 생성
|
||||||
|
|
||||||
|
## 평가 저장소
|
||||||
|
|
||||||
|
프로젝트에 평가 저장:
|
||||||
|
```
|
||||||
|
.claude/
|
||||||
|
evals/
|
||||||
|
feature-xyz.md # 평가 정의
|
||||||
|
feature-xyz.log # 평가 실행 이력
|
||||||
|
baseline.json # 회귀 베이스라인
|
||||||
|
```
|
||||||
|
|
||||||
|
## 모범 사례
|
||||||
|
|
||||||
|
1. **코딩 전에 평가 정의** - 성공 기준에 대한 명확한 사고를 강제
|
||||||
|
2. **자주 평가 실행** - 회귀를 조기에 포착
|
||||||
|
3. **시간에 따른 pass@k 추적** - 신뢰성 추세 모니터링
|
||||||
|
4. **가능하면 코드 채점자 사용** - 결정론적 > 확률적
|
||||||
|
5. **보안에는 사람 검토** - 보안 검사를 완전히 자동화하지 말 것
|
||||||
|
6. **평가를 빠르게 유지** - 느린 평가는 실행되지 않음
|
||||||
|
7. **코드와 함께 평가 버전 관리** - 평가는 일급 산출물
|
||||||
|
|
||||||
|
## 예시: 인증 추가
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## EVAL: add-authentication
|
||||||
|
|
||||||
|
### Phase 1: 정의 (10분)
|
||||||
|
Capability Evals:
|
||||||
|
- [ ] User can register with email/password
|
||||||
|
- [ ] User can login with valid credentials
|
||||||
|
- [ ] Invalid credentials rejected with proper error
|
||||||
|
- [ ] Sessions persist across page reloads
|
||||||
|
- [ ] Logout clears session
|
||||||
|
|
||||||
|
Regression Evals:
|
||||||
|
- [ ] Public routes still accessible
|
||||||
|
- [ ] API responses unchanged
|
||||||
|
- [ ] Database schema compatible
|
||||||
|
|
||||||
|
### Phase 2: 구현 (가변)
|
||||||
|
[Write code]
|
||||||
|
|
||||||
|
### Phase 3: 평가
|
||||||
|
Run: /eval check add-authentication
|
||||||
|
|
||||||
|
### Phase 4: 보고서
|
||||||
|
EVAL REPORT: add-authentication
|
||||||
|
==============================
|
||||||
|
Capability: 5/5 passed (pass@3: 100%)
|
||||||
|
Regression: 3/3 passed (pass^3: 100%)
|
||||||
|
Status: SHIP IT
|
||||||
|
```
|
||||||
|
|
||||||
|
## 제품 평가 (v1.8)
|
||||||
|
|
||||||
|
행동 품질을 단위 테스트만으로 포착할 수 없을 때 제품 평가를 사용하세요.
|
||||||
|
|
||||||
|
### 채점자 유형
|
||||||
|
|
||||||
|
1. 코드 채점자 (결정론적 어서션)
|
||||||
|
2. 규칙 채점자 (정규식/스키마 제약 조건)
|
||||||
|
3. 모델 채점자 (LLM 심사위원 루브릭)
|
||||||
|
4. 사람 채점자 (모호한 출력에 대한 수동 판정)
|
||||||
|
|
||||||
|
### pass@k 가이드
|
||||||
|
|
||||||
|
- `pass@1`: 직접 신뢰성
|
||||||
|
- `pass@3`: 제어된 재시도 하에서의 실용적 신뢰성
|
||||||
|
- `pass^3`: 안정성 테스트 (3회 모두 통과해야 함)
|
||||||
|
|
||||||
|
권장 임계값:
|
||||||
|
- 기능 평가: pass@3 >= 0.90
|
||||||
|
- 회귀 평가: 릴리스 핵심 경로에 pass^3 = 1.00
|
||||||
|
|
||||||
|
### 평가 안티패턴
|
||||||
|
|
||||||
|
- 알려진 평가 예시에 프롬프트 과적합
|
||||||
|
- 정상 경로 출력만 측정
|
||||||
|
- 합격률을 쫓으면서 비용과 지연 시간 변동 무시
|
||||||
|
- 릴리스 게이트에 불안정한 채점자 허용
|
||||||
|
|
||||||
|
### 최소 평가 산출물 레이아웃
|
||||||
|
|
||||||
|
- `.claude/evals/<feature>.md` 정의
|
||||||
|
- `.claude/evals/<feature>.log` 실행 이력
|
||||||
|
- `docs/releases/<version>/eval-summary.md` 릴리스 스냅샷
|
||||||
642
docs/ko-KR/skills/frontend-patterns/SKILL.md
Normal file
642
docs/ko-KR/skills/frontend-patterns/SKILL.md
Normal file
@@ -0,0 +1,642 @@
|
|||||||
|
---
|
||||||
|
name: frontend-patterns
|
||||||
|
description: React, Next.js, 상태 관리, 성능 최적화 및 UI 모범 사례를 위한 프론트엔드 개발 패턴.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# 프론트엔드 개발 패턴
|
||||||
|
|
||||||
|
React, Next.js 및 고성능 사용자 인터페이스를 위한 모던 프론트엔드 패턴.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- React 컴포넌트를 구축할 때 (합성, props, 렌더링)
|
||||||
|
- 상태를 관리할 때 (useState, useReducer, Zustand, Context)
|
||||||
|
- 데이터 페칭을 구현할 때 (SWR, React Query, server components)
|
||||||
|
- 성능을 최적화할 때 (메모이제이션, 가상화, 코드 분할)
|
||||||
|
- 폼을 다룰 때 (유효성 검사, 제어 입력, Zod 스키마)
|
||||||
|
- 클라이언트 사이드 라우팅과 네비게이션을 처리할 때
|
||||||
|
- 접근성 있고 반응형인 UI 패턴을 구축할 때
|
||||||
|
|
||||||
|
## 컴포넌트 패턴
|
||||||
|
|
||||||
|
### 상속보다 합성
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD: Component composition
|
||||||
|
interface CardProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
variant?: 'default' | 'outlined'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Card({ children, variant = 'default' }: CardProps) {
|
||||||
|
return <div className={`card card-${variant}`}>{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardHeader({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div className="card-header">{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardBody({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div className="card-body">{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<Card>
|
||||||
|
<CardHeader>Title</CardHeader>
|
||||||
|
<CardBody>Content</CardBody>
|
||||||
|
</Card>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compound Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TabsContextValue {
|
||||||
|
activeTab: string
|
||||||
|
setActiveTab: (tab: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
|
||||||
|
|
||||||
|
export function Tabs({ children, defaultTab }: {
|
||||||
|
children: React.ReactNode
|
||||||
|
defaultTab: string
|
||||||
|
}) {
|
||||||
|
const [activeTab, setActiveTab] = useState(defaultTab)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
||||||
|
{children}
|
||||||
|
</TabsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabList({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div className="tab-list">{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tab({ id, children }: { id: string, children: React.ReactNode }) {
|
||||||
|
const context = useContext(TabsContext)
|
||||||
|
if (!context) throw new Error('Tab must be used within Tabs')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={context.activeTab === id ? 'active' : ''}
|
||||||
|
onClick={() => context.setActiveTab(id)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<Tabs defaultTab="overview">
|
||||||
|
<TabList>
|
||||||
|
<Tab id="overview">Overview</Tab>
|
||||||
|
<Tab id="details">Details</Tab>
|
||||||
|
</TabList>
|
||||||
|
</Tabs>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Render Props 패턴
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DataLoaderProps<T> {
|
||||||
|
url: string
|
||||||
|
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
|
||||||
|
const [data, setData] = useState<T | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<Error | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(url)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(setData)
|
||||||
|
.catch(setError)
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [url])
|
||||||
|
|
||||||
|
return <>{children(data, loading, error)}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<DataLoader<Market[]> url="/api/markets">
|
||||||
|
{(markets, loading, error) => {
|
||||||
|
if (loading) return <Spinner />
|
||||||
|
if (error) return <Error error={error} />
|
||||||
|
return <MarketList markets={markets!} />
|
||||||
|
}}
|
||||||
|
</DataLoader>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 커스텀 Hook 패턴
|
||||||
|
|
||||||
|
### 상태 관리 Hook
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function useToggle(initialValue = false): [boolean, () => void] {
|
||||||
|
const [value, setValue] = useState(initialValue)
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
setValue(v => !v)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return [value, toggle]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const [isOpen, toggleOpen] = useToggle()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 비동기 데이터 페칭 Hook
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface UseQueryOptions<T> {
|
||||||
|
onSuccess?: (data: T) => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useQuery<T>(
|
||||||
|
key: string,
|
||||||
|
fetcher: () => Promise<T>,
|
||||||
|
options?: UseQueryOptions<T>
|
||||||
|
) {
|
||||||
|
const [data, setData] = useState<T | null>(null)
|
||||||
|
const [error, setError] = useState<Error | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const refetch = useCallback(async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fetcher()
|
||||||
|
setData(result)
|
||||||
|
options?.onSuccess?.(result)
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as Error
|
||||||
|
setError(error)
|
||||||
|
options?.onError?.(error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [fetcher, options])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (options?.enabled !== false) {
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
}, [key, refetch, options?.enabled])
|
||||||
|
|
||||||
|
return { data, error, loading, refetch }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const { data: markets, loading, error, refetch } = useQuery(
|
||||||
|
'markets',
|
||||||
|
() => fetch('/api/markets').then(r => r.json()),
|
||||||
|
{
|
||||||
|
onSuccess: data => console.log('Fetched', data.length, 'markets'),
|
||||||
|
onError: err => console.error('Failed:', err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debounce Hook
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function useDebounce<T>(value: T, delay: number): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value)
|
||||||
|
}, delay)
|
||||||
|
|
||||||
|
return () => clearTimeout(handler)
|
||||||
|
}, [value, delay])
|
||||||
|
|
||||||
|
return debouncedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const debouncedQuery = useDebounce(searchQuery, 500)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (debouncedQuery) {
|
||||||
|
performSearch(debouncedQuery)
|
||||||
|
}
|
||||||
|
}, [debouncedQuery])
|
||||||
|
```
|
||||||
|
|
||||||
|
## 상태 관리 패턴
|
||||||
|
|
||||||
|
### Context + Reducer 패턴
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface State {
|
||||||
|
markets: Market[]
|
||||||
|
selectedMarket: Market | null
|
||||||
|
loading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| { type: 'SET_MARKETS'; payload: Market[] }
|
||||||
|
| { type: 'SELECT_MARKET'; payload: Market }
|
||||||
|
| { type: 'SET_LOADING'; payload: boolean }
|
||||||
|
|
||||||
|
function reducer(state: State, action: Action): State {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_MARKETS':
|
||||||
|
return { ...state, markets: action.payload }
|
||||||
|
case 'SELECT_MARKET':
|
||||||
|
return { ...state, selectedMarket: action.payload }
|
||||||
|
case 'SET_LOADING':
|
||||||
|
return { ...state, loading: action.payload }
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MarketContext = createContext<{
|
||||||
|
state: State
|
||||||
|
dispatch: Dispatch<Action>
|
||||||
|
} | undefined>(undefined)
|
||||||
|
|
||||||
|
export function MarketProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
|
markets: [],
|
||||||
|
selectedMarket: null,
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MarketContext.Provider value={{ state, dispatch }}>
|
||||||
|
{children}
|
||||||
|
</MarketContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMarkets() {
|
||||||
|
const context = useContext(MarketContext)
|
||||||
|
if (!context) throw new Error('useMarkets must be used within MarketProvider')
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 성능 최적화
|
||||||
|
|
||||||
|
### 메모이제이션
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ useMemo for expensive computations
|
||||||
|
const sortedMarkets = useMemo(() => {
|
||||||
|
return markets.sort((a, b) => b.volume - a.volume)
|
||||||
|
}, [markets])
|
||||||
|
|
||||||
|
// ✅ useCallback for functions passed to children
|
||||||
|
const handleSearch = useCallback((query: string) => {
|
||||||
|
setSearchQuery(query)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// ✅ React.memo for pure components
|
||||||
|
export const MarketCard = React.memo<MarketCardProps>(({ market }) => {
|
||||||
|
return (
|
||||||
|
<div className="market-card">
|
||||||
|
<h3>{market.name}</h3>
|
||||||
|
<p>{market.description}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 코드 분할 및 지연 로딩
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { lazy, Suspense } from 'react'
|
||||||
|
|
||||||
|
// ✅ Lazy load heavy components
|
||||||
|
const HeavyChart = lazy(() => import('./HeavyChart'))
|
||||||
|
const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))
|
||||||
|
|
||||||
|
export function Dashboard() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Suspense fallback={<ChartSkeleton />}>
|
||||||
|
<HeavyChart data={data} />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<ThreeJsBackground />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 긴 리스트를 위한 가상화
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||||
|
|
||||||
|
export function VirtualMarketList({ markets }: { markets: Market[] }) {
|
||||||
|
const parentRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count: markets.length,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
estimateSize: () => 100, // Estimated row height
|
||||||
|
overscan: 5 // Extra items to render
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: `${virtualizer.getTotalSize()}px`,
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{virtualizer.getVirtualItems().map(virtualRow => (
|
||||||
|
<div
|
||||||
|
key={virtualRow.index}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: `${virtualRow.size}px`,
|
||||||
|
transform: `translateY(${virtualRow.start}px)`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MarketCard market={markets[virtualRow.index]} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 폼 처리 패턴
|
||||||
|
|
||||||
|
### 유효성 검사가 포함된 제어 폼
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface FormData {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
endDate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormErrors {
|
||||||
|
name?: string
|
||||||
|
description?: string
|
||||||
|
endDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateMarketForm() {
|
||||||
|
const [formData, setFormData] = useState<FormData>({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
endDate: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<FormErrors>({})
|
||||||
|
|
||||||
|
const validate = (): boolean => {
|
||||||
|
const newErrors: FormErrors = {}
|
||||||
|
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
newErrors.name = 'Name is required'
|
||||||
|
} else if (formData.name.length > 200) {
|
||||||
|
newErrors.name = 'Name must be under 200 characters'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.description.trim()) {
|
||||||
|
newErrors.description = 'Description is required'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.endDate) {
|
||||||
|
newErrors.endDate = 'End date is required'
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors)
|
||||||
|
return Object.keys(newErrors).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!validate()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createMarket(formData)
|
||||||
|
// Success handling
|
||||||
|
} catch (error) {
|
||||||
|
// Error handling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
value={formData.name}
|
||||||
|
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
placeholder="Market name"
|
||||||
|
/>
|
||||||
|
{errors.name && <span className="error">{errors.name}</span>}
|
||||||
|
|
||||||
|
{/* Other fields */}
|
||||||
|
|
||||||
|
<button type="submit">Create Market</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Boundary 패턴
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ErrorBoundaryState {
|
||||||
|
hasError: boolean
|
||||||
|
error: Error | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends React.Component<
|
||||||
|
{ children: React.ReactNode },
|
||||||
|
ErrorBoundaryState
|
||||||
|
> {
|
||||||
|
state: ErrorBoundaryState = {
|
||||||
|
hasError: false,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||||
|
return { hasError: true, error }
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
|
console.error('Error boundary caught:', error, errorInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div className="error-fallback">
|
||||||
|
<h2>Something went wrong</h2>
|
||||||
|
<p>{this.state.error?.message}</p>
|
||||||
|
<button onClick={() => this.setState({ hasError: false })}>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<ErrorBoundary>
|
||||||
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 애니메이션 패턴
|
||||||
|
|
||||||
|
### Framer Motion 애니메이션
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
|
||||||
|
// ✅ List animations
|
||||||
|
export function AnimatedMarketList({ markets }: { markets: Market[] }) {
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{markets.map(market => (
|
||||||
|
<motion.div
|
||||||
|
key={market.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<MarketCard market={market} />
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Modal animations
|
||||||
|
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
className="modal-overlay"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="modal-content"
|
||||||
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 접근성 패턴
|
||||||
|
|
||||||
|
### 키보드 네비게이션
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function Dropdown({ options, onSelect }: DropdownProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [activeIndex, setActiveIndex] = useState(0)
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault()
|
||||||
|
setActiveIndex(i => Math.min(i + 1, options.length - 1))
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault()
|
||||||
|
setActiveIndex(i => Math.max(i - 1, 0))
|
||||||
|
break
|
||||||
|
case 'Enter':
|
||||||
|
e.preventDefault()
|
||||||
|
onSelect(options[activeIndex])
|
||||||
|
setIsOpen(false)
|
||||||
|
break
|
||||||
|
case 'Escape':
|
||||||
|
setIsOpen(false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
{/* Dropdown implementation */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 포커스 관리
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
||||||
|
const modalRef = useRef<HTMLDivElement>(null)
|
||||||
|
const previousFocusRef = useRef<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
// Save currently focused element
|
||||||
|
previousFocusRef.current = document.activeElement as HTMLElement
|
||||||
|
|
||||||
|
// Focus modal
|
||||||
|
modalRef.current?.focus()
|
||||||
|
} else {
|
||||||
|
// Restore focus when closing
|
||||||
|
previousFocusRef.current?.focus()
|
||||||
|
}
|
||||||
|
}, [isOpen])
|
||||||
|
|
||||||
|
return isOpen ? (
|
||||||
|
<div
|
||||||
|
ref={modalRef}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
tabIndex={-1}
|
||||||
|
onKeyDown={e => e.key === 'Escape' && onClose()}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**기억하세요**: 모던 프론트엔드 패턴은 유지보수 가능하고 고성능인 사용자 인터페이스를 가능하게 합니다. 프로젝트 복잡도에 맞는 패턴을 선택하세요.
|
||||||
674
docs/ko-KR/skills/golang-patterns/SKILL.md
Normal file
674
docs/ko-KR/skills/golang-patterns/SKILL.md
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
---
|
||||||
|
name: golang-patterns
|
||||||
|
description: 견고하고 효율적이며 유지보수 가능한 Go 애플리케이션 구축을 위한 관용적 Go 패턴, 모범 사례 및 규칙.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Go 개발 패턴
|
||||||
|
|
||||||
|
견고하고 효율적이며 유지보수 가능한 애플리케이션 구축을 위한 관용적 Go 패턴과 모범 사례.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- 새로운 Go 코드 작성 시
|
||||||
|
- Go 코드 리뷰 시
|
||||||
|
- 기존 Go 코드 리팩토링 시
|
||||||
|
- Go 패키지/모듈 설계 시
|
||||||
|
|
||||||
|
## 핵심 원칙
|
||||||
|
|
||||||
|
### 1. 단순성과 명확성
|
||||||
|
|
||||||
|
Go는 영리함보다 단순성을 선호합니다. 코드는 명확하고 읽기 쉬워야 합니다.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Good: Clear and direct
|
||||||
|
func GetUser(id string) (*User, error) {
|
||||||
|
user, err := db.FindUser(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get user %s: %w", id, err)
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad: Overly clever
|
||||||
|
func GetUser(id string) (*User, error) {
|
||||||
|
return func() (*User, error) {
|
||||||
|
if u, e := db.FindUser(id); e == nil {
|
||||||
|
return u, nil
|
||||||
|
} else {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 제로 값을 유용하게 만들기
|
||||||
|
|
||||||
|
제로 값이 초기화 없이 즉시 사용 가능하도록 타입을 설계하세요.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Good: Zero value is useful
|
||||||
|
type Counter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
count int // zero value is 0, ready to use
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Counter) Inc() {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.count++
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good: bytes.Buffer works with zero value
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString("hello")
|
||||||
|
|
||||||
|
// Bad: Requires initialization
|
||||||
|
type BadCounter struct {
|
||||||
|
counts map[string]int // nil map will panic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 인터페이스를 받고 구조체를 반환하기
|
||||||
|
|
||||||
|
함수는 인터페이스 매개변수를 받고 구체적 타입을 반환해야 합니다.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Good: Accepts interface, returns concrete type
|
||||||
|
func ProcessData(r io.Reader) (*Result, error) {
|
||||||
|
data, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Result{Data: data}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad: Returns interface (hides implementation details unnecessarily)
|
||||||
|
func ProcessData(r io.Reader) (io.Reader, error) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 에러 처리 패턴
|
||||||
|
|
||||||
|
### 컨텍스트가 있는 에러 래핑
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Good: Wrap errors with context
|
||||||
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load config %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse config %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 커스텀 에러 타입
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Define domain-specific errors
|
||||||
|
type ValidationError struct {
|
||||||
|
Field string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ValidationError) Error() string {
|
||||||
|
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sentinel errors for common cases
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("resource not found")
|
||||||
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
|
ErrInvalidInput = errors.New("invalid input")
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### errors.Is와 errors.As를 사용한 에러 확인
|
||||||
|
|
||||||
|
```go
|
||||||
|
func HandleError(err error) {
|
||||||
|
// Check for specific error
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
log.Println("No records found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for error type
|
||||||
|
var validationErr *ValidationError
|
||||||
|
if errors.As(err, &validationErr) {
|
||||||
|
log.Printf("Validation error on field %s: %s",
|
||||||
|
validationErr.Field, validationErr.Message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown error
|
||||||
|
log.Printf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 에러를 절대 무시하지 말 것
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Bad: Ignoring error with blank identifier
|
||||||
|
result, _ := doSomething()
|
||||||
|
|
||||||
|
// Good: Handle or explicitly document why it's safe to ignore
|
||||||
|
result, err := doSomething()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acceptable: When error truly doesn't matter (rare)
|
||||||
|
_ = writer.Close() // Best-effort cleanup, error logged elsewhere
|
||||||
|
```
|
||||||
|
|
||||||
|
## 동시성 패턴
|
||||||
|
|
||||||
|
### 워커 풀
|
||||||
|
|
||||||
|
```go
|
||||||
|
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for i := 0; i < numWorkers; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for job := range jobs {
|
||||||
|
results <- process(job)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(results)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 취소 및 타임아웃을 위한 Context
|
||||||
|
|
||||||
|
```go
|
||||||
|
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetch %s: %w", url, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 우아한 종료
|
||||||
|
|
||||||
|
```go
|
||||||
|
func GracefulShutdown(server *http.Server) {
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
<-quit
|
||||||
|
log.Println("Shutting down server...")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := server.Shutdown(ctx); err != nil {
|
||||||
|
log.Fatalf("Server forced to shutdown: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Server exited")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 조율된 고루틴을 위한 errgroup
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
|
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
|
||||||
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
|
results := make([][]byte, len(urls))
|
||||||
|
|
||||||
|
for i, url := range urls {
|
||||||
|
i, url := i, url // Capture loop variables
|
||||||
|
g.Go(func() error {
|
||||||
|
data, err := FetchWithTimeout(ctx, url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
results[i] = data
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 고루틴 누수 방지
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Bad: Goroutine leak if context is cancelled
|
||||||
|
func leakyFetch(ctx context.Context, url string) <-chan []byte {
|
||||||
|
ch := make(chan []byte)
|
||||||
|
go func() {
|
||||||
|
data, _ := fetch(url)
|
||||||
|
ch <- data // Blocks forever if no receiver
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good: Properly handles cancellation
|
||||||
|
func safeFetch(ctx context.Context, url string) <-chan []byte {
|
||||||
|
ch := make(chan []byte, 1) // Buffered channel
|
||||||
|
go func() {
|
||||||
|
data, err := fetch(url)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case ch <- data:
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 인터페이스 설계
|
||||||
|
|
||||||
|
### 작고 집중된 인터페이스
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Good: Single-method interfaces
|
||||||
|
type Reader interface {
|
||||||
|
Read(p []byte) (n int, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Writer interface {
|
||||||
|
Write(p []byte) (n int, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Closer interface {
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose interfaces as needed
|
||||||
|
type ReadWriteCloser interface {
|
||||||
|
Reader
|
||||||
|
Writer
|
||||||
|
Closer
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 사용되는 곳에서 인터페이스 정의
|
||||||
|
|
||||||
|
```go
|
||||||
|
// In the consumer package, not the provider
|
||||||
|
package service
|
||||||
|
|
||||||
|
// UserStore defines what this service needs
|
||||||
|
type UserStore interface {
|
||||||
|
GetUser(id string) (*User, error)
|
||||||
|
SaveUser(user *User) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
store UserStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concrete implementation can be in another package
|
||||||
|
// It doesn't need to know about this interface
|
||||||
|
```
|
||||||
|
|
||||||
|
### 타입 어서션을 통한 선택적 동작
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Flusher interface {
|
||||||
|
Flush() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteAndFlush(w io.Writer, data []byte) error {
|
||||||
|
if _, err := w.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush if supported
|
||||||
|
if f, ok := w.(Flusher); ok {
|
||||||
|
return f.Flush()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 패키지 구성
|
||||||
|
|
||||||
|
### 표준 프로젝트 레이아웃
|
||||||
|
|
||||||
|
```text
|
||||||
|
myproject/
|
||||||
|
├── cmd/
|
||||||
|
│ └── myapp/
|
||||||
|
│ └── main.go # Entry point
|
||||||
|
├── internal/
|
||||||
|
│ ├── handler/ # HTTP handlers
|
||||||
|
│ ├── service/ # Business logic
|
||||||
|
│ ├── repository/ # Data access
|
||||||
|
│ └── config/ # Configuration
|
||||||
|
├── pkg/
|
||||||
|
│ └── client/ # Public API client
|
||||||
|
├── api/
|
||||||
|
│ └── v1/ # API definitions (proto, OpenAPI)
|
||||||
|
├── testdata/ # Test fixtures
|
||||||
|
├── go.mod
|
||||||
|
├── go.sum
|
||||||
|
└── Makefile
|
||||||
|
```
|
||||||
|
|
||||||
|
### 패키지 명명
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Good: Short, lowercase, no underscores
|
||||||
|
package http
|
||||||
|
package json
|
||||||
|
package user
|
||||||
|
|
||||||
|
// Bad: Verbose, mixed case, or redundant
|
||||||
|
package httpHandler
|
||||||
|
package json_parser
|
||||||
|
package userService // Redundant 'Service' suffix
|
||||||
|
```
|
||||||
|
|
||||||
|
### 패키지 수준 상태 피하기
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Bad: Global mutable state
|
||||||
|
var db *sql.DB
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good: Dependency injection
|
||||||
|
type Server struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(db *sql.DB) *Server {
|
||||||
|
return &Server{db: db}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 구조체 설계
|
||||||
|
|
||||||
|
### 함수형 옵션 패턴
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Server struct {
|
||||||
|
addr string
|
||||||
|
timeout time.Duration
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*Server)
|
||||||
|
|
||||||
|
func WithTimeout(d time.Duration) Option {
|
||||||
|
return func(s *Server) {
|
||||||
|
s.timeout = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLogger(l *log.Logger) Option {
|
||||||
|
return func(s *Server) {
|
||||||
|
s.logger = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(addr string, opts ...Option) *Server {
|
||||||
|
s := &Server{
|
||||||
|
addr: addr,
|
||||||
|
timeout: 30 * time.Second, // default
|
||||||
|
logger: log.Default(), // default
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
server := NewServer(":8080",
|
||||||
|
WithTimeout(60*time.Second),
|
||||||
|
WithLogger(customLogger),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 합성을 위한 임베딩
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Logger struct {
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Log(msg string) {
|
||||||
|
fmt.Printf("[%s] %s\n", l.prefix, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
*Logger // Embedding - Server gets Log method
|
||||||
|
addr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(addr string) *Server {
|
||||||
|
return &Server{
|
||||||
|
Logger: &Logger{prefix: "SERVER"},
|
||||||
|
addr: addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
s := NewServer(":8080")
|
||||||
|
s.Log("Starting...") // Calls embedded Logger.Log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 메모리 및 성능
|
||||||
|
|
||||||
|
### 크기를 알 때 슬라이스 미리 할당
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Bad: Grows slice multiple times
|
||||||
|
func processItems(items []Item) []Result {
|
||||||
|
var results []Result
|
||||||
|
for _, item := range items {
|
||||||
|
results = append(results, process(item))
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good: Single allocation
|
||||||
|
func processItems(items []Item) []Result {
|
||||||
|
results := make([]Result, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
results = append(results, process(item))
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 빈번한 할당에 sync.Pool 사용
|
||||||
|
|
||||||
|
```go
|
||||||
|
var bufferPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(bytes.Buffer)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProcessRequest(data []byte) []byte {
|
||||||
|
buf := bufferPool.Get().(*bytes.Buffer)
|
||||||
|
defer func() {
|
||||||
|
buf.Reset()
|
||||||
|
bufferPool.Put(buf)
|
||||||
|
}()
|
||||||
|
|
||||||
|
buf.Write(data)
|
||||||
|
// Process...
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 루프에서 문자열 연결 피하기
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Bad: Creates many string allocations
|
||||||
|
func join(parts []string) string {
|
||||||
|
var result string
|
||||||
|
for _, p := range parts {
|
||||||
|
result += p + ","
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good: Single allocation with strings.Builder
|
||||||
|
func join(parts []string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for i, p := range parts {
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteString(",")
|
||||||
|
}
|
||||||
|
sb.WriteString(p)
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Best: Use standard library
|
||||||
|
func join(parts []string) string {
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Go 도구 통합
|
||||||
|
|
||||||
|
### 필수 명령어
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and run
|
||||||
|
go build ./...
|
||||||
|
go run ./cmd/myapp
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
go test ./...
|
||||||
|
go test -race ./...
|
||||||
|
go test -cover ./...
|
||||||
|
|
||||||
|
# Static analysis
|
||||||
|
go vet ./...
|
||||||
|
staticcheck ./...
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
# Module management
|
||||||
|
go mod tidy
|
||||||
|
go mod verify
|
||||||
|
|
||||||
|
# Formatting
|
||||||
|
gofmt -w .
|
||||||
|
goimports -w .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 권장 린터 구성 (.golangci.yml)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- unused
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- misspell
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
errcheck:
|
||||||
|
check-type-assertions: true
|
||||||
|
govet:
|
||||||
|
check-shadowing: true
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-use-default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
## 빠른 참조: Go 관용구
|
||||||
|
|
||||||
|
| 관용구 | 설명 |
|
||||||
|
|-------|-------------|
|
||||||
|
| Accept interfaces, return structs | 함수는 인터페이스 매개변수를 받고 구체적 타입을 반환 |
|
||||||
|
| Errors are values | 에러를 예외가 아닌 일급 값으로 취급 |
|
||||||
|
| Don't communicate by sharing memory | 고루틴 간 조율에 채널 사용 |
|
||||||
|
| Make the zero value useful | 타입이 명시적 초기화 없이 작동해야 함 |
|
||||||
|
| A little copying is better than a little dependency | 불필요한 외부 의존성 피하기 |
|
||||||
|
| Clear is better than clever | 영리함보다 가독성 우선 |
|
||||||
|
| gofmt is no one's favorite but everyone's friend | 항상 gofmt/goimports로 포맷팅 |
|
||||||
|
| Return early | 에러를 먼저 처리하고 정상 경로는 들여쓰기 없이 유지 |
|
||||||
|
|
||||||
|
## 피해야 할 안티패턴
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Bad: Naked returns in long functions
|
||||||
|
func process() (result int, err error) {
|
||||||
|
// ... 50 lines ...
|
||||||
|
return // What is being returned?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad: Using panic for control flow
|
||||||
|
func GetUser(id string) *User {
|
||||||
|
user, err := db.Find(id)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // Don't do this
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad: Passing context in struct
|
||||||
|
type Request struct {
|
||||||
|
ctx context.Context // Context should be first param
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good: Context as first parameter
|
||||||
|
func ProcessRequest(ctx context.Context, id string) error {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad: Mixing value and pointer receivers
|
||||||
|
type Counter struct{ n int }
|
||||||
|
func (c Counter) Value() int { return c.n } // Value receiver
|
||||||
|
func (c *Counter) Increment() { c.n++ } // Pointer receiver
|
||||||
|
// Pick one style and be consistent
|
||||||
|
```
|
||||||
|
|
||||||
|
**기억하세요**: Go 코드는 최고의 의미에서 지루해야 합니다 - 예측 가능하고, 일관적이며, 이해하기 쉽게. 의심스러울 때는 단순하게 유지하세요.
|
||||||
720
docs/ko-KR/skills/golang-testing/SKILL.md
Normal file
720
docs/ko-KR/skills/golang-testing/SKILL.md
Normal file
@@ -0,0 +1,720 @@
|
|||||||
|
---
|
||||||
|
name: golang-testing
|
||||||
|
description: 테이블 주도 테스트, 서브테스트, 벤치마크, 퍼징, 테스트 커버리지를 포함한 Go 테스팅 패턴. 관용적 Go 관행과 함께 TDD 방법론을 따릅니다.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Go 테스팅 패턴
|
||||||
|
|
||||||
|
TDD 방법론을 따르는 신뢰할 수 있고 유지보수 가능한 테스트 작성을 위한 포괄적인 Go 테스팅 패턴.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- 새로운 Go 함수나 메서드 작성 시
|
||||||
|
- 기존 코드에 테스트 커버리지 추가 시
|
||||||
|
- 성능이 중요한 코드에 벤치마크 생성 시
|
||||||
|
- 입력 유효성 검사를 위한 퍼즈 테스트 구현 시
|
||||||
|
- Go 프로젝트에서 TDD 워크플로우 따를 시
|
||||||
|
|
||||||
|
## Go에서의 TDD 워크플로우
|
||||||
|
|
||||||
|
### RED-GREEN-REFACTOR 사이클
|
||||||
|
|
||||||
|
```
|
||||||
|
RED → Write a failing test first
|
||||||
|
GREEN → Write minimal code to pass the test
|
||||||
|
REFACTOR → Improve code while keeping tests green
|
||||||
|
REPEAT → Continue with next requirement
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go에서의 단계별 TDD
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Step 1: Define the interface/signature
|
||||||
|
// calculator.go
|
||||||
|
package calculator
|
||||||
|
|
||||||
|
func Add(a, b int) int {
|
||||||
|
panic("not implemented") // Placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Write failing test (RED)
|
||||||
|
// calculator_test.go
|
||||||
|
package calculator
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestAdd(t *testing.T) {
|
||||||
|
got := Add(2, 3)
|
||||||
|
want := 5
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("Add(2, 3) = %d; want %d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Run test - verify FAIL
|
||||||
|
// $ go test
|
||||||
|
// --- FAIL: TestAdd (0.00s)
|
||||||
|
// panic: not implemented
|
||||||
|
|
||||||
|
// Step 4: Implement minimal code (GREEN)
|
||||||
|
func Add(a, b int) int {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Run test - verify PASS
|
||||||
|
// $ go test
|
||||||
|
// PASS
|
||||||
|
|
||||||
|
// Step 6: Refactor if needed, verify tests still pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## 테이블 주도 테스트
|
||||||
|
|
||||||
|
Go 테스트의 표준 패턴. 최소한의 코드로 포괄적인 커버리지를 가능하게 합니다.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestAdd(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
a, b int
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{"positive numbers", 2, 3, 5},
|
||||||
|
{"negative numbers", -1, -2, -3},
|
||||||
|
{"zero values", 0, 0, 0},
|
||||||
|
{"mixed signs", -1, 1, 0},
|
||||||
|
{"large numbers", 1000000, 2000000, 3000000},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := Add(tt.a, tt.b)
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("Add(%d, %d) = %d; want %d",
|
||||||
|
tt.a, tt.b, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 에러 케이스가 있는 테이블 주도 테스트
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestParseConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want *Config
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid config",
|
||||||
|
input: `{"host": "localhost", "port": 8080}`,
|
||||||
|
want: &Config{Host: "localhost", Port: 8080},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid JSON",
|
||||||
|
input: `{invalid}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty input",
|
||||||
|
input: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "minimal config",
|
||||||
|
input: `{}`,
|
||||||
|
want: &Config{}, // Zero value config
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := ParseConfig(tt.input)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("got %+v; want %+v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 서브테스트 및 서브벤치마크
|
||||||
|
|
||||||
|
### 관련 테스트 구성
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestUser(t *testing.T) {
|
||||||
|
// Setup shared by all subtests
|
||||||
|
db := setupTestDB(t)
|
||||||
|
|
||||||
|
t.Run("Create", func(t *testing.T) {
|
||||||
|
user := &User{Name: "Alice"}
|
||||||
|
err := db.CreateUser(user)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateUser failed: %v", err)
|
||||||
|
}
|
||||||
|
if user.ID == "" {
|
||||||
|
t.Error("expected user ID to be set")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Get", func(t *testing.T) {
|
||||||
|
user, err := db.GetUser("alice-id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetUser failed: %v", err)
|
||||||
|
}
|
||||||
|
if user.Name != "Alice" {
|
||||||
|
t.Errorf("got name %q; want %q", user.Name, "Alice")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Update", func(t *testing.T) {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Delete", func(t *testing.T) {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 병렬 서브테스트
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestParallel(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
}{
|
||||||
|
{"case1", "input1"},
|
||||||
|
{"case2", "input2"},
|
||||||
|
{"case3", "input3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt // Capture range variable
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel() // Run subtests in parallel
|
||||||
|
result := Process(tt.input)
|
||||||
|
// assertions...
|
||||||
|
_ = result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 테스트 헬퍼
|
||||||
|
|
||||||
|
### 헬퍼 함수
|
||||||
|
|
||||||
|
```go
|
||||||
|
func setupTestDB(t *testing.T) *sql.DB {
|
||||||
|
t.Helper() // Marks this as a helper function
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup when test finishes
|
||||||
|
t.Cleanup(func() {
|
||||||
|
db.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
if _, err := db.Exec(schema); err != nil {
|
||||||
|
t.Fatalf("failed to create schema: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertNoError(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEqual[T comparable](t *testing.T, got, want T) {
|
||||||
|
t.Helper()
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %v; want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 임시 파일 및 디렉터리
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestFileProcessing(t *testing.T) {
|
||||||
|
// Create temp directory - automatically cleaned up
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create test file
|
||||||
|
testFile := filepath.Join(tmpDir, "test.txt")
|
||||||
|
err := os.WriteFile(testFile, []byte("test content"), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run test
|
||||||
|
result, err := ProcessFile(testFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessFile failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert...
|
||||||
|
_ = result
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 골든 파일
|
||||||
|
|
||||||
|
`testdata/`에 저장된 예상 출력 파일에 대한 테스트.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var update = flag.Bool("update", false, "update golden files")
|
||||||
|
|
||||||
|
func TestRender(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input Template
|
||||||
|
}{
|
||||||
|
{"simple", Template{Name: "test"}},
|
||||||
|
{"complex", Template{Name: "test", Items: []string{"a", "b"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := Render(tt.input)
|
||||||
|
|
||||||
|
golden := filepath.Join("testdata", tt.name+".golden")
|
||||||
|
|
||||||
|
if *update {
|
||||||
|
// Update golden file: go test -update
|
||||||
|
err := os.WriteFile(golden, got, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to update golden file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
want, err := os.ReadFile(golden)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read golden file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 인터페이스를 사용한 모킹
|
||||||
|
|
||||||
|
### 인터페이스 기반 모킹
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Define interface for dependencies
|
||||||
|
type UserRepository interface {
|
||||||
|
GetUser(id string) (*User, error)
|
||||||
|
SaveUser(user *User) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Production implementation
|
||||||
|
type PostgresUserRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PostgresUserRepository) GetUser(id string) (*User, error) {
|
||||||
|
// Real database query
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock implementation for tests
|
||||||
|
type MockUserRepository struct {
|
||||||
|
GetUserFunc func(id string) (*User, error)
|
||||||
|
SaveUserFunc func(user *User) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUserRepository) GetUser(id string) (*User, error) {
|
||||||
|
return m.GetUserFunc(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUserRepository) SaveUser(user *User) error {
|
||||||
|
return m.SaveUserFunc(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test using mock
|
||||||
|
func TestUserService(t *testing.T) {
|
||||||
|
mock := &MockUserRepository{
|
||||||
|
GetUserFunc: func(id string) (*User, error) {
|
||||||
|
if id == "123" {
|
||||||
|
return &User{ID: "123", Name: "Alice"}, nil
|
||||||
|
}
|
||||||
|
return nil, ErrNotFound
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
service := NewUserService(mock)
|
||||||
|
|
||||||
|
user, err := service.GetUserProfile("123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if user.Name != "Alice" {
|
||||||
|
t.Errorf("got name %q; want %q", user.Name, "Alice")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 벤치마크
|
||||||
|
|
||||||
|
### 기본 벤치마크
|
||||||
|
|
||||||
|
```go
|
||||||
|
func BenchmarkProcess(b *testing.B) {
|
||||||
|
data := generateTestData(1000)
|
||||||
|
b.ResetTimer() // Don't count setup time
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Process(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run: go test -bench=BenchmarkProcess -benchmem
|
||||||
|
// Output: BenchmarkProcess-8 10000 105234 ns/op 4096 B/op 10 allocs/op
|
||||||
|
```
|
||||||
|
|
||||||
|
### 다양한 크기의 벤치마크
|
||||||
|
|
||||||
|
```go
|
||||||
|
func BenchmarkSort(b *testing.B) {
|
||||||
|
sizes := []int{100, 1000, 10000, 100000}
|
||||||
|
|
||||||
|
for _, size := range sizes {
|
||||||
|
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
|
||||||
|
data := generateRandomSlice(size)
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
// Make a copy to avoid sorting already sorted data
|
||||||
|
tmp := make([]int, len(data))
|
||||||
|
copy(tmp, data)
|
||||||
|
sort.Ints(tmp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 메모리 할당 벤치마크
|
||||||
|
|
||||||
|
```go
|
||||||
|
func BenchmarkStringConcat(b *testing.B) {
|
||||||
|
parts := []string{"hello", "world", "foo", "bar", "baz"}
|
||||||
|
|
||||||
|
b.Run("plus", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var s string
|
||||||
|
for _, p := range parts {
|
||||||
|
s += p
|
||||||
|
}
|
||||||
|
_ = s
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("builder", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, p := range parts {
|
||||||
|
sb.WriteString(p)
|
||||||
|
}
|
||||||
|
_ = sb.String()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("join", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = strings.Join(parts, "")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 퍼징 (Go 1.18+)
|
||||||
|
|
||||||
|
### 기본 퍼즈 테스트
|
||||||
|
|
||||||
|
```go
|
||||||
|
func FuzzParseJSON(f *testing.F) {
|
||||||
|
// Add seed corpus
|
||||||
|
f.Add(`{"name": "test"}`)
|
||||||
|
f.Add(`{"count": 123}`)
|
||||||
|
f.Add(`[]`)
|
||||||
|
f.Add(`""`)
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, input string) {
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := json.Unmarshal([]byte(input), &result)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Invalid JSON is expected for random input
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If parsing succeeded, re-encoding should work
|
||||||
|
_, err = json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Marshal failed after successful Unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s
|
||||||
|
```
|
||||||
|
|
||||||
|
### 다중 입력 퍼즈 테스트
|
||||||
|
|
||||||
|
```go
|
||||||
|
func FuzzCompare(f *testing.F) {
|
||||||
|
f.Add("hello", "world")
|
||||||
|
f.Add("", "")
|
||||||
|
f.Add("abc", "abc")
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, a, b string) {
|
||||||
|
result := Compare(a, b)
|
||||||
|
|
||||||
|
// Property: Compare(a, a) should always equal 0
|
||||||
|
if a == b && result != 0 {
|
||||||
|
t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Property: Compare(a, b) and Compare(b, a) should have opposite signs
|
||||||
|
reverse := Compare(b, a)
|
||||||
|
if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) {
|
||||||
|
if result != 0 || reverse != 0 {
|
||||||
|
t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent",
|
||||||
|
a, b, result, b, a, reverse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 테스트 커버리지
|
||||||
|
|
||||||
|
### 커버리지 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic coverage
|
||||||
|
go test -cover ./...
|
||||||
|
|
||||||
|
# Generate coverage profile
|
||||||
|
go test -coverprofile=coverage.out ./...
|
||||||
|
|
||||||
|
# View coverage in browser
|
||||||
|
go tool cover -html=coverage.out
|
||||||
|
|
||||||
|
# View coverage by function
|
||||||
|
go tool cover -func=coverage.out
|
||||||
|
|
||||||
|
# Coverage with race detection
|
||||||
|
go test -race -coverprofile=coverage.out ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 커버리지 목표
|
||||||
|
|
||||||
|
| 코드 유형 | 목표 |
|
||||||
|
|-----------|--------|
|
||||||
|
| 핵심 비즈니스 로직 | 100% |
|
||||||
|
| 공개 API | 90%+ |
|
||||||
|
| 일반 코드 | 80%+ |
|
||||||
|
| 생성된 코드 | 제외 |
|
||||||
|
|
||||||
|
### 생성된 코드를 커버리지에서 제외
|
||||||
|
|
||||||
|
```go
|
||||||
|
//go:generate mockgen -source=interface.go -destination=mock_interface.go
|
||||||
|
|
||||||
|
// In coverage profile, exclude with build tags:
|
||||||
|
// go test -cover -tags=!generate ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP 핸들러 테스팅
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestHealthHandler(t *testing.T) {
|
||||||
|
// Create request
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Call handler
|
||||||
|
HealthHandler(w, req)
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if string(body) != "OK" {
|
||||||
|
t.Errorf("got body %q; want %q", body, "OK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIHandler(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
body string
|
||||||
|
wantStatus int
|
||||||
|
wantBody string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "get user",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: "/users/123",
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantBody: `{"id":"123","name":"Alice"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not found",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: "/users/999",
|
||||||
|
wantStatus: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create user",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: "/users",
|
||||||
|
body: `{"name":"Bob"}`,
|
||||||
|
wantStatus: http.StatusCreated,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := NewAPIHandler()
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var body io.Reader
|
||||||
|
if tt.body != "" {
|
||||||
|
body = strings.NewReader(tt.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(tt.method, tt.path, body)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != tt.wantStatus {
|
||||||
|
t.Errorf("got status %d; want %d", w.Code, tt.wantStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantBody != "" && w.Body.String() != tt.wantBody {
|
||||||
|
t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 테스팅 명령어
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run tests with verbose output
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
go test -run TestAdd ./...
|
||||||
|
|
||||||
|
# Run tests matching pattern
|
||||||
|
go test -run "TestUser/Create" ./...
|
||||||
|
|
||||||
|
# Run tests with race detector
|
||||||
|
go test -race ./...
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
go test -cover -coverprofile=coverage.out ./...
|
||||||
|
|
||||||
|
# Run short tests only
|
||||||
|
go test -short ./...
|
||||||
|
|
||||||
|
# Run tests with timeout
|
||||||
|
go test -timeout 30s ./...
|
||||||
|
|
||||||
|
# Run benchmarks
|
||||||
|
go test -bench=. -benchmem ./...
|
||||||
|
|
||||||
|
# Run fuzzing
|
||||||
|
go test -fuzz=FuzzParse -fuzztime=30s ./...
|
||||||
|
|
||||||
|
# Count test runs (for flaky test detection)
|
||||||
|
go test -count=10 ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 모범 사례
|
||||||
|
|
||||||
|
**해야 할 것:**
|
||||||
|
- 테스트를 먼저 작성 (TDD)
|
||||||
|
- 포괄적인 커버리지를 위해 테이블 주도 테스트 사용
|
||||||
|
- 구현이 아닌 동작을 테스트
|
||||||
|
- 헬퍼 함수에서 `t.Helper()` 사용
|
||||||
|
- 독립적인 테스트에 `t.Parallel()` 사용
|
||||||
|
- `t.Cleanup()`으로 리소스 정리
|
||||||
|
- 시나리오를 설명하는 의미 있는 테스트 이름 사용
|
||||||
|
|
||||||
|
**하지 말아야 할 것:**
|
||||||
|
- 비공개 함수를 직접 테스트 (공개 API를 통해 테스트)
|
||||||
|
- 테스트에서 `time.Sleep()` 사용 (채널이나 조건 사용)
|
||||||
|
- 불안정한 테스트 무시 (수정하거나 제거)
|
||||||
|
- 모든 것을 모킹 (가능하면 통합 테스트 선호)
|
||||||
|
- 에러 경로 테스트 생략
|
||||||
|
|
||||||
|
## CI/CD 통합
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# GitHub Actions example
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -race -coverprofile=coverage.out ./...
|
||||||
|
|
||||||
|
- name: Check coverage
|
||||||
|
run: |
|
||||||
|
go tool cover -func=coverage.out | grep total | awk '{print $3}' | \
|
||||||
|
awk -F'%' '{if ($1 < 80) exit 1}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**기억하세요**: 테스트는 문서입니다. 코드가 어떻게 사용되어야 하는지를 보여줍니다. 명확하게 작성하고 최신 상태로 유지하세요.
|
||||||
211
docs/ko-KR/skills/iterative-retrieval/SKILL.md
Normal file
211
docs/ko-KR/skills/iterative-retrieval/SKILL.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
---
|
||||||
|
name: iterative-retrieval
|
||||||
|
description: 서브에이전트 컨텍스트 문제를 해결하기 위한 점진적 컨텍스트 검색 개선 패턴
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# 반복적 검색 패턴
|
||||||
|
|
||||||
|
서브에이전트가 작업을 시작하기 전까지 필요한 컨텍스트를 알 수 없는 멀티 에이전트 워크플로우의 "컨텍스트 문제"를 해결합니다.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- 사전에 예측할 수 없는 코드베이스 컨텍스트가 필요한 서브에이전트를 생성할 때
|
||||||
|
- 컨텍스트가 점진적으로 개선되는 멀티 에이전트 워크플로우를 구축할 때
|
||||||
|
- 에이전트 작업에서 "컨텍스트 초과" 또는 "컨텍스트 누락" 실패를 겪을 때
|
||||||
|
- 코드 탐색을 위한 RAG 유사 검색 파이프라인을 설계할 때
|
||||||
|
- 에이전트 오케스트레이션에서 토큰 사용량을 최적화할 때
|
||||||
|
|
||||||
|
## 문제
|
||||||
|
|
||||||
|
서브에이전트는 제한된 컨텍스트로 생성됩니다. 다음을 알 수 없습니다:
|
||||||
|
- 관련 코드가 포함된 파일
|
||||||
|
- 코드베이스에 존재하는 패턴
|
||||||
|
- 프로젝트에서 사용하는 용어
|
||||||
|
|
||||||
|
표준 접근법의 실패:
|
||||||
|
- **모든 것을 전송**: 컨텍스트 제한 초과
|
||||||
|
- **아무것도 전송하지 않음**: 에이전트가 중요한 정보를 갖지 못함
|
||||||
|
- **필요한 것을 추측**: 종종 잘못됨
|
||||||
|
|
||||||
|
## 해결책: 반복적 검색
|
||||||
|
|
||||||
|
컨텍스트를 점진적으로 개선하는 4단계 루프:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ DISPATCH │─────▶│ EVALUATE │ │
|
||||||
|
│ └──────────┘ └──────────┘ │
|
||||||
|
│ ▲ │ │
|
||||||
|
│ │ ▼ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ LOOP │◀─────│ REFINE │ │
|
||||||
|
│ └──────────┘ └──────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Max 3 cycles, then proceed │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1단계: DISPATCH
|
||||||
|
|
||||||
|
후보 파일을 수집하기 위한 초기 광범위 쿼리:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Start with high-level intent
|
||||||
|
const initialQuery = {
|
||||||
|
patterns: ['src/**/*.ts', 'lib/**/*.ts'],
|
||||||
|
keywords: ['authentication', 'user', 'session'],
|
||||||
|
excludes: ['*.test.ts', '*.spec.ts']
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dispatch to retrieval agent
|
||||||
|
const candidates = await retrieveFiles(initialQuery);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2단계: EVALUATE
|
||||||
|
|
||||||
|
검색된 콘텐츠의 관련성 평가:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function evaluateRelevance(files, task) {
|
||||||
|
return files.map(file => ({
|
||||||
|
path: file.path,
|
||||||
|
relevance: scoreRelevance(file.content, task),
|
||||||
|
reason: explainRelevance(file.content, task),
|
||||||
|
missingContext: identifyGaps(file.content, task)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
점수 기준:
|
||||||
|
- **높음 (0.8-1.0)**: 대상 기능을 직접 구현
|
||||||
|
- **중간 (0.5-0.7)**: 관련 패턴이나 타입을 포함
|
||||||
|
- **낮음 (0.2-0.4)**: 간접적으로 관련
|
||||||
|
- **없음 (0-0.2)**: 관련 없음, 제외
|
||||||
|
|
||||||
|
### 3단계: REFINE
|
||||||
|
|
||||||
|
평가를 기반으로 검색 기준 업데이트:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function refineQuery(evaluation, previousQuery) {
|
||||||
|
return {
|
||||||
|
// Add new patterns discovered in high-relevance files
|
||||||
|
patterns: [...previousQuery.patterns, ...extractPatterns(evaluation)],
|
||||||
|
|
||||||
|
// Add terminology found in codebase
|
||||||
|
keywords: [...previousQuery.keywords, ...extractKeywords(evaluation)],
|
||||||
|
|
||||||
|
// Exclude confirmed irrelevant paths
|
||||||
|
excludes: [...previousQuery.excludes, ...evaluation
|
||||||
|
.filter(e => e.relevance < 0.2)
|
||||||
|
.map(e => e.path)
|
||||||
|
],
|
||||||
|
|
||||||
|
// Target specific gaps
|
||||||
|
focusAreas: evaluation
|
||||||
|
.flatMap(e => e.missingContext)
|
||||||
|
.filter(unique)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4단계: LOOP
|
||||||
|
|
||||||
|
개선된 기준으로 반복 (최대 3회):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function iterativeRetrieve(task, maxCycles = 3) {
|
||||||
|
let query = createInitialQuery(task);
|
||||||
|
let bestContext = [];
|
||||||
|
|
||||||
|
for (let cycle = 0; cycle < maxCycles; cycle++) {
|
||||||
|
const candidates = await retrieveFiles(query);
|
||||||
|
const evaluation = evaluateRelevance(candidates, task);
|
||||||
|
|
||||||
|
// Check if we have sufficient context
|
||||||
|
const highRelevance = evaluation.filter(e => e.relevance >= 0.7);
|
||||||
|
if (highRelevance.length >= 3 && !hasCriticalGaps(evaluation)) {
|
||||||
|
return highRelevance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refine and continue
|
||||||
|
query = refineQuery(evaluation, query);
|
||||||
|
bestContext = mergeContext(bestContext, highRelevance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestContext;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 실용적인 예시
|
||||||
|
|
||||||
|
### 예시 1: 버그 수정 컨텍스트
|
||||||
|
|
||||||
|
```
|
||||||
|
Task: "Fix the authentication token expiry bug"
|
||||||
|
|
||||||
|
Cycle 1:
|
||||||
|
DISPATCH: Search for "token", "auth", "expiry" in src/**
|
||||||
|
EVALUATE: Found auth.ts (0.9), tokens.ts (0.8), user.ts (0.3)
|
||||||
|
REFINE: Add "refresh", "jwt" keywords; exclude user.ts
|
||||||
|
|
||||||
|
Cycle 2:
|
||||||
|
DISPATCH: Search refined terms
|
||||||
|
EVALUATE: Found session-manager.ts (0.95), jwt-utils.ts (0.85)
|
||||||
|
REFINE: Sufficient context (2 high-relevance files)
|
||||||
|
|
||||||
|
Result: auth.ts, tokens.ts, session-manager.ts, jwt-utils.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 예시 2: 기능 구현
|
||||||
|
|
||||||
|
```
|
||||||
|
Task: "Add rate limiting to API endpoints"
|
||||||
|
|
||||||
|
Cycle 1:
|
||||||
|
DISPATCH: Search "rate", "limit", "api" in routes/**
|
||||||
|
EVALUATE: No matches - codebase uses "throttle" terminology
|
||||||
|
REFINE: Add "throttle", "middleware" keywords
|
||||||
|
|
||||||
|
Cycle 2:
|
||||||
|
DISPATCH: Search refined terms
|
||||||
|
EVALUATE: Found throttle.ts (0.9), middleware/index.ts (0.7)
|
||||||
|
REFINE: Need router patterns
|
||||||
|
|
||||||
|
Cycle 3:
|
||||||
|
DISPATCH: Search "router", "express" patterns
|
||||||
|
EVALUATE: Found router-setup.ts (0.8)
|
||||||
|
REFINE: Sufficient context
|
||||||
|
|
||||||
|
Result: throttle.ts, middleware/index.ts, router-setup.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 에이전트와의 통합
|
||||||
|
|
||||||
|
에이전트 프롬프트에서 사용:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
When retrieving context for this task:
|
||||||
|
1. Start with broad keyword search
|
||||||
|
2. Evaluate each file's relevance (0-1 scale)
|
||||||
|
3. Identify what context is still missing
|
||||||
|
4. Refine search criteria and repeat (max 3 cycles)
|
||||||
|
5. Return files with relevance >= 0.7
|
||||||
|
```
|
||||||
|
|
||||||
|
## 모범 사례
|
||||||
|
|
||||||
|
1. **광범위하게 시작하여 점진적으로 좁히기** - 초기 쿼리를 과도하게 지정하지 않기
|
||||||
|
2. **코드베이스 용어 학습** - 첫 번째 사이클에서 주로 네이밍 컨벤션이 드러남
|
||||||
|
3. **누락된 것 추적** - 명시적 격차 식별이 개선을 주도
|
||||||
|
4. **"충분히 좋은" 수준에서 중단** - 관련성 높은 파일 3개가 보통 수준의 파일 10개보다 나음
|
||||||
|
5. **자신 있게 제외** - 관련성 낮은 파일은 관련성이 높아지지 않음
|
||||||
|
|
||||||
|
## 관련 항목
|
||||||
|
|
||||||
|
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - 서브에이전트 오케스트레이션 섹션
|
||||||
|
- `continuous-learning` 스킬 - 시간이 지남에 따라 개선되는 패턴
|
||||||
|
- `~/.claude/agents/`의 에이전트 정의
|
||||||
147
docs/ko-KR/skills/postgres-patterns/SKILL.md
Normal file
147
docs/ko-KR/skills/postgres-patterns/SKILL.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
name: postgres-patterns
|
||||||
|
description: 쿼리 최적화, 스키마 설계, 인덱싱, 보안을 위한 PostgreSQL 데이터베이스 패턴. Supabase 모범 사례 기반.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# PostgreSQL 패턴
|
||||||
|
|
||||||
|
PostgreSQL 모범 사례 빠른 참조. 자세한 가이드는 `database-reviewer` 에이전트를 사용하세요.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- SQL 쿼리 또는 마이그레이션을 작성할 때
|
||||||
|
- 데이터베이스 스키마를 설계할 때
|
||||||
|
- 느린 쿼리를 문제 해결할 때
|
||||||
|
- Row Level Security를 구현할 때
|
||||||
|
- 커넥션 풀링을 설정할 때
|
||||||
|
|
||||||
|
## 빠른 참조
|
||||||
|
|
||||||
|
### 인덱스 치트 시트
|
||||||
|
|
||||||
|
| 쿼리 패턴 | 인덱스 유형 | 예시 |
|
||||||
|
|--------------|------------|---------|
|
||||||
|
| `WHERE col = value` | B-tree (기본값) | `CREATE INDEX idx ON t (col)` |
|
||||||
|
| `WHERE col > value` | B-tree | `CREATE INDEX idx ON t (col)` |
|
||||||
|
| `WHERE a = x AND b > y` | Composite | `CREATE INDEX idx ON t (a, b)` |
|
||||||
|
| `WHERE jsonb @> '{}'` | GIN | `CREATE INDEX idx ON t USING gin (col)` |
|
||||||
|
| `WHERE tsv @@ query` | GIN | `CREATE INDEX idx ON t USING gin (col)` |
|
||||||
|
| 시계열 범위 | BRIN | `CREATE INDEX idx ON t USING brin (col)` |
|
||||||
|
|
||||||
|
### 데이터 타입 빠른 참조
|
||||||
|
|
||||||
|
| 사용 사례 | 올바른 타입 | 지양 |
|
||||||
|
|----------|-------------|-------|
|
||||||
|
| ID | `bigint` | `int`, random UUID |
|
||||||
|
| 문자열 | `text` | `varchar(255)` |
|
||||||
|
| 타임스탬프 | `timestamptz` | `timestamp` |
|
||||||
|
| 금액 | `numeric(10,2)` | `float` |
|
||||||
|
| 플래그 | `boolean` | `varchar`, `int` |
|
||||||
|
|
||||||
|
### 일반 패턴
|
||||||
|
|
||||||
|
**복합 인덱스 순서:**
|
||||||
|
```sql
|
||||||
|
-- Equality columns first, then range columns
|
||||||
|
CREATE INDEX idx ON orders (status, created_at);
|
||||||
|
-- Works for: WHERE status = 'pending' AND created_at > '2024-01-01'
|
||||||
|
```
|
||||||
|
|
||||||
|
**커버링 인덱스:**
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx ON users (email) INCLUDE (name, created_at);
|
||||||
|
-- Avoids table lookup for SELECT email, name, created_at
|
||||||
|
```
|
||||||
|
|
||||||
|
**부분 인덱스:**
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx ON users (email) WHERE deleted_at IS NULL;
|
||||||
|
-- Smaller index, only includes active users
|
||||||
|
```
|
||||||
|
|
||||||
|
**RLS 정책 (최적화):**
|
||||||
|
```sql
|
||||||
|
CREATE POLICY policy ON orders
|
||||||
|
USING ((SELECT auth.uid()) = user_id); -- Wrap in SELECT!
|
||||||
|
```
|
||||||
|
|
||||||
|
**UPSERT:**
|
||||||
|
```sql
|
||||||
|
INSERT INTO settings (user_id, key, value)
|
||||||
|
VALUES (123, 'theme', 'dark')
|
||||||
|
ON CONFLICT (user_id, key)
|
||||||
|
DO UPDATE SET value = EXCLUDED.value;
|
||||||
|
```
|
||||||
|
|
||||||
|
**커서 페이지네이션:**
|
||||||
|
```sql
|
||||||
|
SELECT * FROM products WHERE id > $last_id ORDER BY id LIMIT 20;
|
||||||
|
-- O(1) vs OFFSET which is O(n)
|
||||||
|
```
|
||||||
|
|
||||||
|
**큐 처리:**
|
||||||
|
```sql
|
||||||
|
UPDATE jobs SET status = 'processing'
|
||||||
|
WHERE id = (
|
||||||
|
SELECT id FROM jobs WHERE status = 'pending'
|
||||||
|
ORDER BY created_at LIMIT 1
|
||||||
|
FOR UPDATE SKIP LOCKED
|
||||||
|
) RETURNING *;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 안티패턴 감지
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Find unindexed foreign keys
|
||||||
|
SELECT conrelid::regclass, a.attname
|
||||||
|
FROM pg_constraint c
|
||||||
|
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
||||||
|
WHERE c.contype = 'f'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_index i
|
||||||
|
WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Find slow queries
|
||||||
|
SELECT query, mean_exec_time, calls
|
||||||
|
FROM pg_stat_statements
|
||||||
|
WHERE mean_exec_time > 100
|
||||||
|
ORDER BY mean_exec_time DESC;
|
||||||
|
|
||||||
|
-- Check table bloat
|
||||||
|
SELECT relname, n_dead_tup, last_vacuum
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
WHERE n_dead_tup > 1000
|
||||||
|
ORDER BY n_dead_tup DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 구성 템플릿
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Connection limits (adjust for RAM)
|
||||||
|
ALTER SYSTEM SET max_connections = 100;
|
||||||
|
ALTER SYSTEM SET work_mem = '8MB';
|
||||||
|
|
||||||
|
-- Timeouts
|
||||||
|
ALTER SYSTEM SET idle_in_transaction_session_timeout = '30s';
|
||||||
|
ALTER SYSTEM SET statement_timeout = '30s';
|
||||||
|
|
||||||
|
-- Monitoring
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
||||||
|
|
||||||
|
-- Security defaults
|
||||||
|
REVOKE ALL ON SCHEMA public FROM public;
|
||||||
|
|
||||||
|
SELECT pg_reload_conf();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 관련 항목
|
||||||
|
|
||||||
|
- 에이전트: `database-reviewer` - 전체 데이터베이스 리뷰 워크플로우
|
||||||
|
- 스킬: `clickhouse-io` - ClickHouse 분석 패턴
|
||||||
|
- 스킬: `backend-patterns` - API 및 백엔드 패턴
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Supabase Agent Skills 기반 (크레딧: Supabase 팀) (MIT License)*
|
||||||
349
docs/ko-KR/skills/project-guidelines-example/SKILL.md
Normal file
349
docs/ko-KR/skills/project-guidelines-example/SKILL.md
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
---
|
||||||
|
name: project-guidelines-example
|
||||||
|
description: "실제 프로덕션 애플리케이션을 기반으로 한 프로젝트별 스킬 템플릿 예시."
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# 프로젝트 가이드라인 스킬 (예시)
|
||||||
|
|
||||||
|
이것은 프로젝트별 스킬의 예시입니다. 자신의 프로젝트에 맞는 템플릿으로 사용하세요.
|
||||||
|
|
||||||
|
실제 프로덕션 애플리케이션을 기반으로 합니다: [Zenith](https://zenith.chat) - AI 기반 고객 발견 플랫폼.
|
||||||
|
|
||||||
|
## 사용 시점
|
||||||
|
|
||||||
|
이 스킬이 설계된 특정 프로젝트에서 작업할 때 참조하세요. 프로젝트 스킬에는 다음이 포함됩니다:
|
||||||
|
- 아키텍처 개요
|
||||||
|
- 파일 구조
|
||||||
|
- 코드 패턴
|
||||||
|
- 테스팅 요구사항
|
||||||
|
- 배포 워크플로우
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 아키텍처 개요
|
||||||
|
|
||||||
|
**기술 스택:**
|
||||||
|
- **Frontend**: Next.js 15 (App Router), TypeScript, React
|
||||||
|
- **Backend**: FastAPI (Python), Pydantic 모델
|
||||||
|
- **Database**: Supabase (PostgreSQL)
|
||||||
|
- **AI**: Claude API (도구 호출 및 구조화된 출력)
|
||||||
|
- **Deployment**: Google Cloud Run
|
||||||
|
- **Testing**: Playwright (E2E), pytest (백엔드), React Testing Library
|
||||||
|
|
||||||
|
**서비스:**
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Frontend │
|
||||||
|
│ Next.js 15 + TypeScript + TailwindCSS │
|
||||||
|
│ Deployed: Vercel / Cloud Run │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Backend │
|
||||||
|
│ FastAPI + Python 3.11 + Pydantic │
|
||||||
|
│ Deployed: Cloud Run │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────┼───────────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||||
|
│ Supabase │ │ Claude │ │ Redis │
|
||||||
|
│ Database │ │ API │ │ Cache │
|
||||||
|
└──────────┘ └──────────┘ └──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 파일 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
project/
|
||||||
|
├── frontend/
|
||||||
|
│ └── src/
|
||||||
|
│ ├── app/ # Next.js app router 페이지
|
||||||
|
│ │ ├── api/ # API 라우트
|
||||||
|
│ │ ├── (auth)/ # 인증 보호 라우트
|
||||||
|
│ │ └── workspace/ # 메인 앱 워크스페이스
|
||||||
|
│ ├── components/ # React 컴포넌트
|
||||||
|
│ │ ├── ui/ # 기본 UI 컴포넌트
|
||||||
|
│ │ ├── forms/ # 폼 컴포넌트
|
||||||
|
│ │ └── layouts/ # 레이아웃 컴포넌트
|
||||||
|
│ ├── hooks/ # 커스텀 React hooks
|
||||||
|
│ ├── lib/ # 유틸리티
|
||||||
|
│ ├── types/ # TypeScript 정의
|
||||||
|
│ └── config/ # 설정
|
||||||
|
│
|
||||||
|
├── backend/
|
||||||
|
│ ├── routers/ # FastAPI 라우트 핸들러
|
||||||
|
│ ├── models.py # Pydantic 모델
|
||||||
|
│ ├── main.py # FastAPI 앱 엔트리
|
||||||
|
│ ├── auth_system.py # 인증
|
||||||
|
│ ├── database.py # 데이터베이스 작업
|
||||||
|
│ ├── services/ # 비즈니스 로직
|
||||||
|
│ └── tests/ # pytest 테스트
|
||||||
|
│
|
||||||
|
├── deploy/ # 배포 설정
|
||||||
|
├── docs/ # 문서
|
||||||
|
└── scripts/ # 유틸리티 스크립트
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 코드 패턴
|
||||||
|
|
||||||
|
### API 응답 형식 (FastAPI)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Generic, TypeVar, Optional
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
class ApiResponse(BaseModel, Generic[T]):
|
||||||
|
success: bool
|
||||||
|
data: Optional[T] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ok(cls, data: T) -> "ApiResponse[T]":
|
||||||
|
return cls(success=True, data=data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fail(cls, error: str) -> "ApiResponse[T]":
|
||||||
|
return cls(success=False, error=error)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend API 호출 (TypeScript)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
success: boolean
|
||||||
|
data?: T
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchApi<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options?: RequestInit
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api${endpoint}`, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options?.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return { success: false, error: `HTTP ${response.status}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: String(error) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude AI 통합 (구조화된 출력)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from anthropic import Anthropic
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class AnalysisResult(BaseModel):
|
||||||
|
summary: str
|
||||||
|
key_points: list[str]
|
||||||
|
confidence: float
|
||||||
|
|
||||||
|
async def analyze_with_claude(content: str) -> AnalysisResult:
|
||||||
|
client = Anthropic()
|
||||||
|
|
||||||
|
response = client.messages.create(
|
||||||
|
model="claude-sonnet-4-5-20250514",
|
||||||
|
max_tokens=1024,
|
||||||
|
messages=[{"role": "user", "content": content}],
|
||||||
|
tools=[{
|
||||||
|
"name": "provide_analysis",
|
||||||
|
"description": "Provide structured analysis",
|
||||||
|
"input_schema": AnalysisResult.model_json_schema()
|
||||||
|
}],
|
||||||
|
tool_choice={"type": "tool", "name": "provide_analysis"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract tool use result
|
||||||
|
tool_use = next(
|
||||||
|
block for block in response.content
|
||||||
|
if block.type == "tool_use"
|
||||||
|
)
|
||||||
|
|
||||||
|
return AnalysisResult(**tool_use.input)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 커스텀 Hooks (React)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useState, useCallback } from 'react'
|
||||||
|
|
||||||
|
interface UseApiState<T> {
|
||||||
|
data: T | null
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApi<T>(
|
||||||
|
fetchFn: () => Promise<ApiResponse<T>>
|
||||||
|
) {
|
||||||
|
const [state, setState] = useState<UseApiState<T>>({
|
||||||
|
data: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const execute = useCallback(async () => {
|
||||||
|
setState(prev => ({ ...prev, loading: true, error: null }))
|
||||||
|
|
||||||
|
const result = await fetchFn()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setState({ data: result.data!, loading: false, error: null })
|
||||||
|
} else {
|
||||||
|
setState({ data: null, loading: false, error: result.error! })
|
||||||
|
}
|
||||||
|
}, [fetchFn])
|
||||||
|
|
||||||
|
return { ...state, execute }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 테스팅 요구사항
|
||||||
|
|
||||||
|
### Backend (pytest)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
poetry run pytest tests/
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
poetry run pytest tests/ --cov=. --cov-report=html
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
poetry run pytest tests/test_auth.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
**테스트 구조:**
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from main import app
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def client():
|
||||||
|
async with AsyncClient(app=app, base_url="http://test") as ac:
|
||||||
|
yield ac
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_health_check(client: AsyncClient):
|
||||||
|
response = await client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == "healthy"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (React Testing Library)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run tests
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
npm run test -- --coverage
|
||||||
|
|
||||||
|
# Run E2E tests
|
||||||
|
npm run test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
**테스트 구조:**
|
||||||
|
```typescript
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
|
import { WorkspacePanel } from './WorkspacePanel'
|
||||||
|
|
||||||
|
describe('WorkspacePanel', () => {
|
||||||
|
it('renders workspace correctly', () => {
|
||||||
|
render(<WorkspacePanel />)
|
||||||
|
expect(screen.getByRole('main')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles session creation', async () => {
|
||||||
|
render(<WorkspacePanel />)
|
||||||
|
fireEvent.click(screen.getByText('New Session'))
|
||||||
|
expect(await screen.findByText('Session created')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 배포 워크플로우
|
||||||
|
|
||||||
|
### 배포 전 체크리스트
|
||||||
|
|
||||||
|
- [ ] 모든 테스트가 로컬에서 통과
|
||||||
|
- [ ] `npm run build` 성공 (frontend)
|
||||||
|
- [ ] `poetry run pytest` 통과 (backend)
|
||||||
|
- [ ] 하드코딩된 시크릿 없음
|
||||||
|
- [ ] 환경 변수 문서화됨
|
||||||
|
- [ ] 데이터베이스 마이그레이션 준비됨
|
||||||
|
|
||||||
|
### 배포 명령어
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and deploy frontend
|
||||||
|
cd frontend && npm run build
|
||||||
|
gcloud run deploy frontend --source .
|
||||||
|
|
||||||
|
# Build and deploy backend
|
||||||
|
cd backend
|
||||||
|
gcloud run deploy backend --source .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 환경 변수
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Frontend (.env.local)
|
||||||
|
NEXT_PUBLIC_API_URL=https://api.example.com
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
|
||||||
|
|
||||||
|
# Backend (.env)
|
||||||
|
DATABASE_URL=postgresql://...
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
SUPABASE_URL=https://xxx.supabase.co
|
||||||
|
SUPABASE_KEY=eyJ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 핵심 규칙
|
||||||
|
|
||||||
|
1. **코드, 주석, 문서에 이모지 없음**
|
||||||
|
2. **불변성** - 객체나 배열을 절대 변형하지 않음
|
||||||
|
3. **TDD** - 구현 전에 테스트 작성
|
||||||
|
4. **80% 커버리지** 최소
|
||||||
|
5. **작은 파일 여러 개** - 200-400줄이 일반적, 800줄 최대
|
||||||
|
6. **프로덕션 코드에 console.log 없음**
|
||||||
|
7. **적절한 에러 처리** (try/catch 사용)
|
||||||
|
8. **입력 유효성 검사** (Pydantic/Zod 사용)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 관련 스킬
|
||||||
|
|
||||||
|
- `coding-standards.md` - 일반 코딩 모범 사례
|
||||||
|
- `backend-patterns.md` - API 및 데이터베이스 패턴
|
||||||
|
- `frontend-patterns.md` - React 및 Next.js 패턴
|
||||||
|
- `tdd-workflow/` - 테스트 주도 개발 방법론
|
||||||
495
docs/ko-KR/skills/security-review/SKILL.md
Normal file
495
docs/ko-KR/skills/security-review/SKILL.md
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
---
|
||||||
|
name: security-review
|
||||||
|
description: 인증 추가, 사용자 입력 처리, 시크릿 관리, API 엔드포인트 생성, 결제/민감한 기능 구현 시 이 스킬을 사용하세요. 포괄적인 보안 체크리스트와 패턴을 제공합니다.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# 보안 리뷰 스킬
|
||||||
|
|
||||||
|
이 스킬은 모든 코드가 보안 모범 사례를 따르고 잠재적 취약점을 식별하도록 보장합니다.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- 인증 또는 권한 부여 구현 시
|
||||||
|
- 사용자 입력 또는 파일 업로드 처리 시
|
||||||
|
- 새로운 API 엔드포인트 생성 시
|
||||||
|
- 시크릿 또는 자격 증명 작업 시
|
||||||
|
- 결제 기능 구현 시
|
||||||
|
- 민감한 데이터 저장 또는 전송 시
|
||||||
|
- 서드파티 API 통합 시
|
||||||
|
|
||||||
|
## 보안 체크리스트
|
||||||
|
|
||||||
|
### 1. 시크릿 관리
|
||||||
|
|
||||||
|
#### 절대 하지 말아야 할 것
|
||||||
|
```typescript
|
||||||
|
const apiKey = "sk-proj-xxxxx" // Hardcoded secret
|
||||||
|
const dbPassword = "password123" // In source code
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 반드시 해야 할 것
|
||||||
|
```typescript
|
||||||
|
const apiKey = process.env.OPENAI_API_KEY
|
||||||
|
const dbUrl = process.env.DATABASE_URL
|
||||||
|
|
||||||
|
// Verify secrets exist
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('OPENAI_API_KEY not configured')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
- [ ] 하드코딩된 API 키, 토큰, 비밀번호 없음
|
||||||
|
- [ ] 모든 시크릿이 환경 변수에 저장됨
|
||||||
|
- [ ] `.env.local`이 .gitignore에 포함됨
|
||||||
|
- [ ] git 히스토리에 시크릿 없음
|
||||||
|
- [ ] 프로덕션 시크릿이 호스팅 플랫폼(Vercel, Railway)에 저장됨
|
||||||
|
|
||||||
|
### 2. 입력 유효성 검사
|
||||||
|
|
||||||
|
#### 항상 사용자 입력을 검증할 것
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
// Define validation schema
|
||||||
|
const CreateUserSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
name: z.string().min(1).max(100),
|
||||||
|
age: z.number().int().min(0).max(150)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validate before processing
|
||||||
|
export async function createUser(input: unknown) {
|
||||||
|
try {
|
||||||
|
const validated = CreateUserSchema.parse(input)
|
||||||
|
return await db.users.create(validated)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return { success: false, errors: error.errors }
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 파일 업로드 유효성 검사
|
||||||
|
```typescript
|
||||||
|
function validateFileUpload(file: File) {
|
||||||
|
// Size check (5MB max)
|
||||||
|
const maxSize = 5 * 1024 * 1024
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
throw new Error('File too large (max 5MB)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type check
|
||||||
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
|
||||||
|
if (!allowedTypes.includes(file.type)) {
|
||||||
|
throw new Error('Invalid file type')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension check
|
||||||
|
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']
|
||||||
|
const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0]
|
||||||
|
if (!extension || !allowedExtensions.includes(extension)) {
|
||||||
|
throw new Error('Invalid file extension')
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
- [ ] 모든 사용자 입력이 스키마로 검증됨
|
||||||
|
- [ ] 파일 업로드가 제한됨 (크기, 타입, 확장자)
|
||||||
|
- [ ] 사용자 입력이 쿼리에 직접 사용되지 않음
|
||||||
|
- [ ] 화이트리스트 검증 사용 (블랙리스트가 아닌)
|
||||||
|
- [ ] 에러 메시지가 민감한 정보를 노출하지 않음
|
||||||
|
|
||||||
|
### 3. SQL Injection 방지
|
||||||
|
|
||||||
|
#### 절대 SQL을 연결하지 말 것
|
||||||
|
```typescript
|
||||||
|
// DANGEROUS - SQL Injection vulnerability
|
||||||
|
const query = `SELECT * FROM users WHERE email = '${userEmail}'`
|
||||||
|
await db.query(query)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 반드시 파라미터화된 쿼리를 사용할 것
|
||||||
|
```typescript
|
||||||
|
// Safe - parameterized query
|
||||||
|
const { data } = await supabase
|
||||||
|
.from('users')
|
||||||
|
.select('*')
|
||||||
|
.eq('email', userEmail)
|
||||||
|
|
||||||
|
// Or with raw SQL
|
||||||
|
await db.query(
|
||||||
|
'SELECT * FROM users WHERE email = $1',
|
||||||
|
[userEmail]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
- [ ] 모든 데이터베이스 쿼리가 파라미터화된 쿼리 사용
|
||||||
|
- [ ] SQL에서 문자열 연결 없음
|
||||||
|
- [ ] ORM/쿼리 빌더가 올바르게 사용됨
|
||||||
|
- [ ] Supabase 쿼리가 적절히 새니타이징됨
|
||||||
|
|
||||||
|
### 4. 인증 및 권한 부여
|
||||||
|
|
||||||
|
#### JWT 토큰 처리
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG: localStorage (vulnerable to XSS)
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
|
||||||
|
// ✅ CORRECT: httpOnly cookies
|
||||||
|
res.setHeader('Set-Cookie',
|
||||||
|
`token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 권한 부여 확인
|
||||||
|
```typescript
|
||||||
|
export async function deleteUser(userId: string, requesterId: string) {
|
||||||
|
// ALWAYS verify authorization first
|
||||||
|
const requester = await db.users.findUnique({
|
||||||
|
where: { id: requesterId }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (requester.role !== 'admin') {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Unauthorized' },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed with deletion
|
||||||
|
await db.users.delete({ where: { id: userId } })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Row Level Security (Supabase)
|
||||||
|
```sql
|
||||||
|
-- Enable RLS on all tables
|
||||||
|
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Users can only view their own data
|
||||||
|
CREATE POLICY "Users view own data"
|
||||||
|
ON users FOR SELECT
|
||||||
|
USING (auth.uid() = id);
|
||||||
|
|
||||||
|
-- Users can only update their own data
|
||||||
|
CREATE POLICY "Users update own data"
|
||||||
|
ON users FOR UPDATE
|
||||||
|
USING (auth.uid() = id);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
- [ ] 토큰이 httpOnly 쿠키에 저장됨 (localStorage가 아닌)
|
||||||
|
- [ ] 민감한 작업 전에 권한 부여 확인
|
||||||
|
- [ ] Supabase에서 Row Level Security 활성화됨
|
||||||
|
- [ ] 역할 기반 접근 제어 구현됨
|
||||||
|
- [ ] 세션 관리가 안전함
|
||||||
|
|
||||||
|
### 5. XSS 방지
|
||||||
|
|
||||||
|
#### HTML 새니타이징
|
||||||
|
```typescript
|
||||||
|
import DOMPurify from 'isomorphic-dompurify'
|
||||||
|
|
||||||
|
// ALWAYS sanitize user-provided HTML
|
||||||
|
function renderUserContent(html: string) {
|
||||||
|
const clean = DOMPurify.sanitize(html, {
|
||||||
|
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
|
||||||
|
ALLOWED_ATTR: []
|
||||||
|
})
|
||||||
|
return <div dangerouslySetInnerHTML={{ __html: clean }} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Content Security Policy
|
||||||
|
```typescript
|
||||||
|
// next.config.js
|
||||||
|
const securityHeaders = [
|
||||||
|
{
|
||||||
|
key: 'Content-Security-Policy',
|
||||||
|
value: `
|
||||||
|
default-src 'self';
|
||||||
|
script-src 'self' 'unsafe-eval' 'unsafe-inline';
|
||||||
|
style-src 'self' 'unsafe-inline';
|
||||||
|
img-src 'self' data: https:;
|
||||||
|
font-src 'self';
|
||||||
|
connect-src 'self' https://api.example.com;
|
||||||
|
`.replace(/\s{2,}/g, ' ').trim()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
- [ ] 사용자 제공 HTML이 새니타이징됨
|
||||||
|
- [ ] CSP 헤더가 구성됨
|
||||||
|
- [ ] 검증되지 않은 동적 콘텐츠 렌더링 없음
|
||||||
|
- [ ] React의 내장 XSS 보호가 사용됨
|
||||||
|
|
||||||
|
### 6. CSRF 보호
|
||||||
|
|
||||||
|
#### CSRF 토큰
|
||||||
|
```typescript
|
||||||
|
import { csrf } from '@/lib/csrf'
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const token = request.headers.get('X-CSRF-Token')
|
||||||
|
|
||||||
|
if (!csrf.verify(token)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Invalid CSRF token' },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process request
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SameSite 쿠키
|
||||||
|
```typescript
|
||||||
|
res.setHeader('Set-Cookie',
|
||||||
|
`session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
- [ ] 상태 변경 작업에 CSRF 토큰 적용
|
||||||
|
- [ ] 모든 쿠키에 SameSite=Strict 설정
|
||||||
|
- [ ] Double-submit 쿠키 패턴 구현
|
||||||
|
|
||||||
|
### 7. 속도 제한
|
||||||
|
|
||||||
|
#### API 속도 제한
|
||||||
|
```typescript
|
||||||
|
import rateLimit from 'express-rate-limit'
|
||||||
|
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100, // 100 requests per window
|
||||||
|
message: 'Too many requests'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply to routes
|
||||||
|
app.use('/api/', limiter)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 비용이 높은 작업
|
||||||
|
```typescript
|
||||||
|
// Aggressive rate limiting for searches
|
||||||
|
const searchLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
max: 10, // 10 requests per minute
|
||||||
|
message: 'Too many search requests'
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/api/search', searchLimiter)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
- [ ] 모든 API 엔드포인트에 속도 제한 적용
|
||||||
|
- [ ] 비용이 높은 작업에 더 엄격한 제한
|
||||||
|
- [ ] IP 기반 속도 제한
|
||||||
|
- [ ] 사용자 기반 속도 제한 (인증된 사용자)
|
||||||
|
|
||||||
|
### 8. 민감한 데이터 노출
|
||||||
|
|
||||||
|
#### 로깅
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG: Logging sensitive data
|
||||||
|
console.log('User login:', { email, password })
|
||||||
|
console.log('Payment:', { cardNumber, cvv })
|
||||||
|
|
||||||
|
// ✅ CORRECT: Redact sensitive data
|
||||||
|
console.log('User login:', { email, userId })
|
||||||
|
console.log('Payment:', { last4: card.last4, userId })
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 에러 메시지
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG: Exposing internal details
|
||||||
|
catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error.message, stack: error.stack },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ CORRECT: Generic error messages
|
||||||
|
catch (error) {
|
||||||
|
console.error('Internal error:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'An error occurred. Please try again.' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
- [ ] 로그에 비밀번호, 토큰, 시크릿 없음
|
||||||
|
- [ ] 사용자에게 표시되는 에러 메시지가 일반적임
|
||||||
|
- [ ] 상세 에러는 서버 로그에만 기록
|
||||||
|
- [ ] 사용자에게 스택 트레이스가 노출되지 않음
|
||||||
|
|
||||||
|
### 9. 블록체인 보안 (Solana)
|
||||||
|
|
||||||
|
#### 지갑 검증
|
||||||
|
```typescript
|
||||||
|
import { verify } from '@solana/web3.js'
|
||||||
|
|
||||||
|
async function verifyWalletOwnership(
|
||||||
|
publicKey: string,
|
||||||
|
signature: string,
|
||||||
|
message: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const isValid = verify(
|
||||||
|
Buffer.from(message),
|
||||||
|
Buffer.from(signature, 'base64'),
|
||||||
|
Buffer.from(publicKey, 'base64')
|
||||||
|
)
|
||||||
|
return isValid
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 트랜잭션 검증
|
||||||
|
```typescript
|
||||||
|
async function verifyTransaction(transaction: Transaction) {
|
||||||
|
// Verify recipient
|
||||||
|
if (transaction.to !== expectedRecipient) {
|
||||||
|
throw new Error('Invalid recipient')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify amount
|
||||||
|
if (transaction.amount > maxAmount) {
|
||||||
|
throw new Error('Amount exceeds limit')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user has sufficient balance
|
||||||
|
const balance = await getBalance(transaction.from)
|
||||||
|
if (balance < transaction.amount) {
|
||||||
|
throw new Error('Insufficient balance')
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
- [ ] 지갑 서명 검증됨
|
||||||
|
- [ ] 트랜잭션 세부 정보 유효성 검사됨
|
||||||
|
- [ ] 트랜잭션 전 잔액 확인
|
||||||
|
- [ ] 블라인드 트랜잭션 서명 없음
|
||||||
|
|
||||||
|
### 10. 의존성 보안
|
||||||
|
|
||||||
|
#### 정기 업데이트
|
||||||
|
```bash
|
||||||
|
# Check for vulnerabilities
|
||||||
|
npm audit
|
||||||
|
|
||||||
|
# Fix automatically fixable issues
|
||||||
|
npm audit fix
|
||||||
|
|
||||||
|
# Update dependencies
|
||||||
|
npm update
|
||||||
|
|
||||||
|
# Check for outdated packages
|
||||||
|
npm outdated
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 잠금 파일
|
||||||
|
```bash
|
||||||
|
# ALWAYS commit lock files
|
||||||
|
git add package-lock.json
|
||||||
|
|
||||||
|
# Use in CI/CD for reproducible builds
|
||||||
|
npm ci # Instead of npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
- [ ] 의존성이 최신 상태
|
||||||
|
- [ ] 알려진 취약점 없음 (npm audit 클린)
|
||||||
|
- [ ] 잠금 파일 커밋됨
|
||||||
|
- [ ] GitHub에서 Dependabot 활성화됨
|
||||||
|
- [ ] 정기적인 보안 업데이트
|
||||||
|
|
||||||
|
## 보안 테스트
|
||||||
|
|
||||||
|
### 자동화된 보안 테스트
|
||||||
|
```typescript
|
||||||
|
// Test authentication
|
||||||
|
test('requires authentication', async () => {
|
||||||
|
const response = await fetch('/api/protected')
|
||||||
|
expect(response.status).toBe(401)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test authorization
|
||||||
|
test('requires admin role', async () => {
|
||||||
|
const response = await fetch('/api/admin', {
|
||||||
|
headers: { Authorization: `Bearer ${userToken}` }
|
||||||
|
})
|
||||||
|
expect(response.status).toBe(403)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test input validation
|
||||||
|
test('rejects invalid input', async () => {
|
||||||
|
const response = await fetch('/api/users', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email: 'not-an-email' })
|
||||||
|
})
|
||||||
|
expect(response.status).toBe(400)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test rate limiting
|
||||||
|
test('enforces rate limits', async () => {
|
||||||
|
const requests = Array(101).fill(null).map(() =>
|
||||||
|
fetch('/api/endpoint')
|
||||||
|
)
|
||||||
|
|
||||||
|
const responses = await Promise.all(requests)
|
||||||
|
const tooManyRequests = responses.filter(r => r.status === 429)
|
||||||
|
|
||||||
|
expect(tooManyRequests.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 배포 전 보안 체크리스트
|
||||||
|
|
||||||
|
모든 프로덕션 배포 전:
|
||||||
|
|
||||||
|
- [ ] **시크릿**: 하드코딩된 시크릿 없음, 모두 환경 변수에 저장
|
||||||
|
- [ ] **입력 유효성 검사**: 모든 사용자 입력 검증됨
|
||||||
|
- [ ] **SQL Injection**: 모든 쿼리 파라미터화됨
|
||||||
|
- [ ] **XSS**: 사용자 콘텐츠 새니타이징됨
|
||||||
|
- [ ] **CSRF**: 보호 활성화됨
|
||||||
|
- [ ] **인증**: 적절한 토큰 처리
|
||||||
|
- [ ] **권한 부여**: 역할 확인 적용됨
|
||||||
|
- [ ] **속도 제한**: 모든 엔드포인트에서 활성화됨
|
||||||
|
- [ ] **HTTPS**: 프로덕션에서 강제 적용
|
||||||
|
- [ ] **보안 헤더**: CSP, X-Frame-Options 구성됨
|
||||||
|
- [ ] **에러 처리**: 에러에 민감한 데이터 없음
|
||||||
|
- [ ] **로깅**: 민감한 데이터가 로그에 없음
|
||||||
|
- [ ] **의존성**: 최신 상태, 취약점 없음
|
||||||
|
- [ ] **Row Level Security**: Supabase에서 활성화됨
|
||||||
|
- [ ] **CORS**: 적절히 구성됨
|
||||||
|
- [ ] **파일 업로드**: 유효성 검사됨 (크기, 타입)
|
||||||
|
- [ ] **지갑 서명**: 검증됨 (블록체인인 경우)
|
||||||
|
|
||||||
|
## 참고 자료
|
||||||
|
|
||||||
|
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||||
|
- [Next.js Security](https://nextjs.org/docs/security)
|
||||||
|
- [Supabase Security](https://supabase.com/docs/guides/auth)
|
||||||
|
- [Web Security Academy](https://portswigger.net/web-security)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**기억하세요**: 보안은 선택 사항이 아닙니다. 하나의 취약점이 전체 플랫폼을 침해할 수 있습니다. 의심스러울 때는 보수적으로 대응하세요.
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
| name | description |
|
||||||
|
|------|-------------|
|
||||||
|
| cloud-infrastructure-security | 클라우드 플랫폼 배포, 인프라 구성, IAM 정책 관리, 로깅/모니터링 설정, CI/CD 파이프라인 구현 시 이 스킬을 사용하세요. 모범 사례에 맞춘 클라우드 보안 체크리스트를 제공합니다. |
|
||||||
|
|
||||||
|
# 클라우드 및 인프라 보안 스킬
|
||||||
|
|
||||||
|
이 스킬은 클라우드 인프라, CI/CD 파이프라인, 배포 구성이 보안 모범 사례를 따르고 업계 표준을 준수하도록 보장합니다.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- 클라우드 플랫폼(AWS, Vercel, Railway, Cloudflare)에 애플리케이션 배포 시
|
||||||
|
- IAM 역할 및 권한 구성 시
|
||||||
|
- CI/CD 파이프라인 설정 시
|
||||||
|
- Infrastructure as Code(Terraform, CloudFormation) 구현 시
|
||||||
|
- 로깅 및 모니터링 구성 시
|
||||||
|
- 클라우드 환경에서 시크릿 관리 시
|
||||||
|
- CDN 및 엣지 보안 설정 시
|
||||||
|
- 재해 복구 및 백업 전략 구현 시
|
||||||
|
|
||||||
|
## 클라우드 보안 체크리스트
|
||||||
|
|
||||||
|
### 1. IAM 및 접근 제어
|
||||||
|
|
||||||
|
#### 최소 권한 원칙
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ✅ CORRECT: Minimal permissions
|
||||||
|
iam_role:
|
||||||
|
permissions:
|
||||||
|
- s3:GetObject # Only read access
|
||||||
|
- s3:ListBucket
|
||||||
|
resources:
|
||||||
|
- arn:aws:s3:::my-bucket/* # Specific bucket only
|
||||||
|
|
||||||
|
# ❌ WRONG: Overly broad permissions
|
||||||
|
iam_role:
|
||||||
|
permissions:
|
||||||
|
- s3:* # All S3 actions
|
||||||
|
resources:
|
||||||
|
- "*" # All resources
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 다중 인증 (MFA)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ALWAYS enable MFA for root/admin accounts
|
||||||
|
aws iam enable-mfa-device \
|
||||||
|
--user-name admin \
|
||||||
|
--serial-number arn:aws:iam::123456789:mfa/admin \
|
||||||
|
--authentication-code1 123456 \
|
||||||
|
--authentication-code2 789012
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
|
||||||
|
- [ ] 프로덕션에서 루트 계정 사용 없음
|
||||||
|
- [ ] 모든 권한 있는 계정에 MFA 활성화됨
|
||||||
|
- [ ] 서비스 계정이 장기 자격 증명이 아닌 역할을 사용
|
||||||
|
- [ ] IAM 정책이 최소 권한을 따름
|
||||||
|
- [ ] 정기적인 접근 검토 수행
|
||||||
|
- [ ] 사용하지 않는 자격 증명 교체 또는 제거
|
||||||
|
|
||||||
|
### 2. 시크릿 관리
|
||||||
|
|
||||||
|
#### 클라우드 시크릿 매니저
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ CORRECT: Use cloud secrets manager
|
||||||
|
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
|
||||||
|
|
||||||
|
const client = new SecretsManager({ region: 'us-east-1' });
|
||||||
|
const secret = await client.getSecretValue({ SecretId: 'prod/api-key' });
|
||||||
|
const apiKey = JSON.parse(secret.SecretString).key;
|
||||||
|
|
||||||
|
// ❌ WRONG: Hardcoded or in environment variables only
|
||||||
|
const apiKey = process.env.API_KEY; // Not rotated, not audited
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 시크릿 교체
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set up automatic rotation for database credentials
|
||||||
|
aws secretsmanager rotate-secret \
|
||||||
|
--secret-id prod/db-password \
|
||||||
|
--rotation-lambda-arn arn:aws:lambda:region:account:function:rotate \
|
||||||
|
--rotation-rules AutomaticallyAfterDays=30
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
|
||||||
|
- [ ] 모든 시크릿이 클라우드 시크릿 매니저에 저장됨 (AWS Secrets Manager, Vercel Secrets)
|
||||||
|
- [ ] 데이터베이스 자격 증명에 대한 자동 교체 활성화됨
|
||||||
|
- [ ] API 키가 최소 분기별로 교체됨
|
||||||
|
- [ ] 코드, 로그, 에러 메시지에 시크릿 없음
|
||||||
|
- [ ] 시크릿 접근에 대한 감사 로깅 활성화됨
|
||||||
|
|
||||||
|
### 3. 네트워크 보안
|
||||||
|
|
||||||
|
#### VPC 및 방화벽 구성
|
||||||
|
|
||||||
|
```terraform
|
||||||
|
# ✅ CORRECT: Restricted security group
|
||||||
|
resource "aws_security_group" "app" {
|
||||||
|
name = "app-sg"
|
||||||
|
|
||||||
|
ingress {
|
||||||
|
from_port = 443
|
||||||
|
to_port = 443
|
||||||
|
protocol = "tcp"
|
||||||
|
cidr_blocks = ["10.0.0.0/16"] # Internal VPC only
|
||||||
|
}
|
||||||
|
|
||||||
|
egress {
|
||||||
|
from_port = 443
|
||||||
|
to_port = 443
|
||||||
|
protocol = "tcp"
|
||||||
|
cidr_blocks = ["0.0.0.0/0"] # Only HTTPS outbound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ❌ WRONG: Open to the internet
|
||||||
|
resource "aws_security_group" "bad" {
|
||||||
|
ingress {
|
||||||
|
from_port = 0
|
||||||
|
to_port = 65535
|
||||||
|
protocol = "tcp"
|
||||||
|
cidr_blocks = ["0.0.0.0/0"] # All ports, all IPs!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
|
||||||
|
- [ ] 데이터베이스가 공개적으로 접근 불가
|
||||||
|
- [ ] SSH/RDP 포트가 VPN/배스천에만 제한됨
|
||||||
|
- [ ] 보안 그룹이 최소 권한을 따름
|
||||||
|
- [ ] 네트워크 ACL이 구성됨
|
||||||
|
- [ ] VPC 플로우 로그가 활성화됨
|
||||||
|
|
||||||
|
### 4. 로깅 및 모니터링
|
||||||
|
|
||||||
|
#### CloudWatch/로깅 구성
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ CORRECT: Comprehensive logging
|
||||||
|
import { CloudWatchLogsClient, CreateLogStreamCommand } from '@aws-sdk/client-cloudwatch-logs';
|
||||||
|
|
||||||
|
const logSecurityEvent = async (event: SecurityEvent) => {
|
||||||
|
await cloudwatch.putLogEvents({
|
||||||
|
logGroupName: '/aws/security/events',
|
||||||
|
logStreamName: 'authentication',
|
||||||
|
logEvents: [{
|
||||||
|
timestamp: Date.now(),
|
||||||
|
message: JSON.stringify({
|
||||||
|
type: event.type,
|
||||||
|
userId: event.userId,
|
||||||
|
ip: event.ip,
|
||||||
|
result: event.result,
|
||||||
|
// Never log sensitive data
|
||||||
|
})
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
|
||||||
|
- [ ] 모든 서비스에 CloudWatch/로깅 활성화됨
|
||||||
|
- [ ] 실패한 인증 시도가 로깅됨
|
||||||
|
- [ ] 관리자 작업이 감사됨
|
||||||
|
- [ ] 로그 보존 기간이 구성됨 (규정 준수를 위해 90일 이상)
|
||||||
|
- [ ] 의심스러운 활동에 대한 알림 구성됨
|
||||||
|
- [ ] 로그가 중앙 집중화되고 변조 방지됨
|
||||||
|
|
||||||
|
### 5. CI/CD 파이프라인 보안
|
||||||
|
|
||||||
|
#### 보안 파이프라인 구성
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ✅ CORRECT: Secure GitHub Actions workflow
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read # Minimal permissions
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Scan for secrets
|
||||||
|
- name: Secret scanning
|
||||||
|
uses: trufflesecurity/trufflehog@main
|
||||||
|
|
||||||
|
# Dependency audit
|
||||||
|
- name: Audit dependencies
|
||||||
|
run: npm audit --audit-level=high
|
||||||
|
|
||||||
|
# Use OIDC, not long-lived tokens
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
|
||||||
|
aws-region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 공급망 보안
|
||||||
|
|
||||||
|
```json
|
||||||
|
// package.json - Use lock files and integrity checks
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"install": "npm ci", // Use ci for reproducible builds
|
||||||
|
"audit": "npm audit --audit-level=moderate",
|
||||||
|
"check": "npm outdated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
|
||||||
|
- [ ] 장기 자격 증명 대신 OIDC 사용
|
||||||
|
- [ ] 파이프라인에서 시크릿 스캐닝
|
||||||
|
- [ ] 의존성 취약점 스캐닝
|
||||||
|
- [ ] 컨테이너 이미지 스캐닝 (해당하는 경우)
|
||||||
|
- [ ] 브랜치 보호 규칙 적용됨
|
||||||
|
- [ ] 병합 전 코드 리뷰 필수
|
||||||
|
- [ ] 서명된 커밋 적용
|
||||||
|
|
||||||
|
### 6. Cloudflare 및 CDN 보안
|
||||||
|
|
||||||
|
#### Cloudflare 보안 구성
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ CORRECT: Cloudflare Workers with security headers
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request): Promise<Response> {
|
||||||
|
const response = await fetch(request);
|
||||||
|
|
||||||
|
// Add security headers
|
||||||
|
const headers = new Headers(response.headers);
|
||||||
|
headers.set('X-Frame-Options', 'DENY');
|
||||||
|
headers.set('X-Content-Type-Options', 'nosniff');
|
||||||
|
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||||
|
headers.set('Permissions-Policy', 'geolocation=(), microphone=()');
|
||||||
|
|
||||||
|
return new Response(response.body, {
|
||||||
|
status: response.status,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### WAF 규칙
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable Cloudflare WAF managed rules
|
||||||
|
# - OWASP Core Ruleset
|
||||||
|
# - Cloudflare Managed Ruleset
|
||||||
|
# - Rate limiting rules
|
||||||
|
# - Bot protection
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
|
||||||
|
- [ ] OWASP 규칙으로 WAF 활성화됨
|
||||||
|
- [ ] 속도 제한 구성됨
|
||||||
|
- [ ] 봇 보호 활성화됨
|
||||||
|
- [ ] DDoS 보호 활성화됨
|
||||||
|
- [ ] 보안 헤더 구성됨
|
||||||
|
- [ ] SSL/TLS 엄격 모드 활성화됨
|
||||||
|
|
||||||
|
### 7. 백업 및 재해 복구
|
||||||
|
|
||||||
|
#### 자동 백업
|
||||||
|
|
||||||
|
```terraform
|
||||||
|
# ✅ CORRECT: Automated RDS backups
|
||||||
|
resource "aws_db_instance" "main" {
|
||||||
|
allocated_storage = 20
|
||||||
|
engine = "postgres"
|
||||||
|
|
||||||
|
backup_retention_period = 30 # 30 days retention
|
||||||
|
backup_window = "03:00-04:00"
|
||||||
|
maintenance_window = "mon:04:00-mon:05:00"
|
||||||
|
|
||||||
|
enabled_cloudwatch_logs_exports = ["postgresql"]
|
||||||
|
|
||||||
|
deletion_protection = true # Prevent accidental deletion
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 확인 단계
|
||||||
|
|
||||||
|
- [ ] 자동 일일 백업 구성됨
|
||||||
|
- [ ] 백업 보존 기간이 규정 준수 요구사항을 충족
|
||||||
|
- [ ] 특정 시점 복구 활성화됨
|
||||||
|
- [ ] 분기별 백업 테스트 수행
|
||||||
|
- [ ] 재해 복구 계획 문서화됨
|
||||||
|
- [ ] RPO 및 RTO가 정의되고 테스트됨
|
||||||
|
|
||||||
|
## 배포 전 클라우드 보안 체크리스트
|
||||||
|
|
||||||
|
모든 프로덕션 클라우드 배포 전:
|
||||||
|
|
||||||
|
- [ ] **IAM**: 루트 계정 미사용, MFA 활성화, 최소 권한 정책
|
||||||
|
- [ ] **시크릿**: 모든 시크릿이 클라우드 시크릿 매니저에 교체와 함께 저장됨
|
||||||
|
- [ ] **네트워크**: 보안 그룹 제한됨, 공개 데이터베이스 없음
|
||||||
|
- [ ] **로깅**: CloudWatch/로깅이 보존 기간과 함께 활성화됨
|
||||||
|
- [ ] **모니터링**: 이상 징후에 대한 알림 구성됨
|
||||||
|
- [ ] **CI/CD**: OIDC 인증, 시크릿 스캐닝, 의존성 감사
|
||||||
|
- [ ] **CDN/WAF**: OWASP 규칙으로 Cloudflare WAF 활성화됨
|
||||||
|
- [ ] **암호화**: 저장 및 전송 중 데이터 암호화
|
||||||
|
- [ ] **백업**: 테스트된 복구와 함께 자동 백업
|
||||||
|
- [ ] **규정 준수**: GDPR/HIPAA 요구사항 충족 (해당하는 경우)
|
||||||
|
- [ ] **문서화**: 인프라 문서화, 런북 작성됨
|
||||||
|
- [ ] **인시던트 대응**: 보안 인시던트 계획 마련
|
||||||
|
|
||||||
|
## 일반적인 클라우드 보안 잘못된 구성
|
||||||
|
|
||||||
|
### S3 버킷 노출
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ❌ WRONG: Public bucket
|
||||||
|
aws s3api put-bucket-acl --bucket my-bucket --acl public-read
|
||||||
|
|
||||||
|
# ✅ CORRECT: Private bucket with specific access
|
||||||
|
aws s3api put-bucket-acl --bucket my-bucket --acl private
|
||||||
|
aws s3api put-bucket-policy --bucket my-bucket --policy file://policy.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### RDS 공개 접근
|
||||||
|
|
||||||
|
```terraform
|
||||||
|
# ❌ WRONG
|
||||||
|
resource "aws_db_instance" "bad" {
|
||||||
|
publicly_accessible = true # NEVER do this!
|
||||||
|
}
|
||||||
|
|
||||||
|
# ✅ CORRECT
|
||||||
|
resource "aws_db_instance" "good" {
|
||||||
|
publicly_accessible = false
|
||||||
|
vpc_security_group_ids = [aws_security_group.db.id]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 참고 자료
|
||||||
|
|
||||||
|
- [AWS Security Best Practices](https://aws.amazon.com/security/best-practices/)
|
||||||
|
- [CIS AWS Foundations Benchmark](https://www.cisecurity.org/benchmark/amazon_web_services)
|
||||||
|
- [Cloudflare Security Documentation](https://developers.cloudflare.com/security/)
|
||||||
|
- [OWASP Cloud Security](https://owasp.org/www-project-cloud-security/)
|
||||||
|
- [Terraform Security Best Practices](https://www.terraform.io/docs/cloud/guides/recommended-practices/)
|
||||||
|
|
||||||
|
**기억하세요**: 클라우드 잘못된 구성은 데이터 유출의 주요 원인입니다. 하나의 노출된 S3 버킷이나 과도하게 허용적인 IAM 정책이 전체 인프라를 침해할 수 있습니다. 항상 최소 권한 원칙과 심층 방어를 따르세요.
|
||||||
103
docs/ko-KR/skills/strategic-compact/SKILL.md
Normal file
103
docs/ko-KR/skills/strategic-compact/SKILL.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
name: strategic-compact
|
||||||
|
description: 임의의 자동 컴팩션 대신 논리적 간격에서 수동 컨텍스트 압축을 제안하여 작업 단계를 통해 컨텍스트를 보존합니다.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# 전략적 컴팩트 스킬
|
||||||
|
|
||||||
|
임의의 자동 컴팩션에 의존하지 않고 워크플로우의 전략적 지점에서 수동 `/compact`를 제안합니다.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- 컨텍스트 제한에 근접하는 긴 세션을 실행할 때 (200K+ 토큰)
|
||||||
|
- 다단계 작업을 수행할 때 (조사 -> 계획 -> 구현 -> 테스트)
|
||||||
|
- 같은 세션 내에서 관련 없는 작업 간 전환할 때
|
||||||
|
- 주요 마일스톤을 완료하고 새 작업을 시작할 때
|
||||||
|
- 응답이 느려지거나 일관성이 떨어질 때 (컨텍스트 압박)
|
||||||
|
|
||||||
|
## 전략적 컴팩션이 필요한 이유
|
||||||
|
|
||||||
|
자동 컴팩션은 임의의 지점에서 실행됩니다:
|
||||||
|
- 종종 작업 중간에 실행되어 중요한 컨텍스트를 잃음
|
||||||
|
- 논리적 작업 경계를 인식하지 못함
|
||||||
|
- 복잡한 다단계 작업을 중단할 수 있음
|
||||||
|
|
||||||
|
논리적 경계에서의 전략적 컴팩션:
|
||||||
|
- **탐색 후, 실행 전** -- 조사 컨텍스트를 압축하고 구현 계획은 유지
|
||||||
|
- **마일스톤 완료 후** -- 다음 단계를 위한 새로운 시작
|
||||||
|
- **주요 컨텍스트 전환 전** -- 다른 작업 시작 전에 탐색 컨텍스트 정리
|
||||||
|
|
||||||
|
## 작동 방식
|
||||||
|
|
||||||
|
`suggest-compact.js` 스크립트는 PreToolUse (Edit/Write)에서 실행되며 다음을 수행합니다:
|
||||||
|
|
||||||
|
1. **도구 호출 추적** -- 세션 내 도구 호출 횟수를 카운트
|
||||||
|
2. **임계값 감지** -- 설정 가능한 임계값에서 제안 (기본값: 50회)
|
||||||
|
3. **주기적 알림** -- 임계값 이후 25회마다 알림
|
||||||
|
|
||||||
|
## Hook 설정
|
||||||
|
|
||||||
|
`~/.claude/settings.json`에 추가합니다:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"hooks": [{ "type": "command", "command": "node ~/.claude/skills/strategic-compact/suggest-compact.js" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Write",
|
||||||
|
"hooks": [{ "type": "command", "command": "node ~/.claude/skills/strategic-compact/suggest-compact.js" }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 구성
|
||||||
|
|
||||||
|
환경 변수:
|
||||||
|
- `COMPACT_THRESHOLD` -- 첫 번째 제안까지의 도구 호출 횟수 (기본값: 50)
|
||||||
|
|
||||||
|
## 컴팩션 결정 가이드
|
||||||
|
|
||||||
|
컴팩션 시기를 결정하기 위해 이 표를 사용하세요:
|
||||||
|
|
||||||
|
| 단계 전환 | 컴팩션? | 이유 |
|
||||||
|
|-----------------|----------|-----|
|
||||||
|
| 조사 -> 계획 | 예 | 조사 컨텍스트는 부피가 크고, 계획이 증류된 결과물 |
|
||||||
|
| 계획 -> 구현 | 예 | 계획은 TodoWrite 또는 파일에 있으므로 코드를 위한 컨텍스트 확보 |
|
||||||
|
| 구현 -> 테스트 | 경우에 따라 | 테스트가 최근 코드를 참조하면 유지; 포커스 전환 시 컴팩션 |
|
||||||
|
| 디버깅 -> 다음 기능 | 예 | 디버그 추적이 관련 없는 작업의 컨텍스트를 오염시킴 |
|
||||||
|
| 구현 중간 | 아니오 | 변수명, 파일 경로, 부분 상태를 잃는 비용이 큼 |
|
||||||
|
| 실패한 접근 후 | 예 | 새 접근을 시도하기 전에 막다른 길의 추론을 정리 |
|
||||||
|
|
||||||
|
## 컴팩션에서 유지되는 것
|
||||||
|
|
||||||
|
무엇이 유지되는지 이해하면 자신 있게 컴팩션할 수 있습니다:
|
||||||
|
|
||||||
|
| 유지됨 | 손실됨 |
|
||||||
|
|----------|------|
|
||||||
|
| CLAUDE.md 지침 | 중간 추론 및 분석 |
|
||||||
|
| TodoWrite 작업 목록 | 이전에 읽은 파일 내용 |
|
||||||
|
| 메모리 파일 (`~/.claude/memory/`) | 다단계 대화 컨텍스트 |
|
||||||
|
| Git 상태 (커밋, 브랜치) | 도구 호출 기록 및 횟수 |
|
||||||
|
| 디스크의 파일 | 구두로 언급된 세밀한 사용자 선호도 |
|
||||||
|
|
||||||
|
## 모범 사례
|
||||||
|
|
||||||
|
1. **계획 후 컴팩션** -- TodoWrite에서 계획이 확정되면 새로 시작하기 위해 컴팩션
|
||||||
|
2. **디버깅 후 컴팩션** -- 계속하기 전에 에러 해결 컨텍스트 정리
|
||||||
|
3. **구현 중간에는 컴팩션하지 않기** -- 관련 변경 사항의 컨텍스트 보존
|
||||||
|
4. **제안을 읽기** -- Hook이 *언제*를 알려주고, *할지* 여부는 당신이 결정
|
||||||
|
5. **컴팩션 전에 기록** -- 컴팩션 전에 중요한 컨텍스트를 파일이나 메모리에 저장
|
||||||
|
6. **요약과 함께 `/compact` 사용** -- 커스텀 메시지 추가: `/compact Focus on implementing auth middleware next`
|
||||||
|
|
||||||
|
## 관련 항목
|
||||||
|
|
||||||
|
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) -- 토큰 최적화 섹션
|
||||||
|
- 메모리 영속성 Hook -- 컴팩션에서 살아남는 상태를 위해
|
||||||
|
- `continuous-learning` 스킬 -- 세션 종료 전 패턴 추출
|
||||||
410
docs/ko-KR/skills/tdd-workflow/SKILL.md
Normal file
410
docs/ko-KR/skills/tdd-workflow/SKILL.md
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
---
|
||||||
|
name: tdd-workflow
|
||||||
|
description: 새 기능 작성, 버그 수정 또는 코드 리팩터링 시 이 스킬을 사용하세요. 단위, 통합, E2E 테스트를 포함한 80% 이상의 커버리지로 테스트 주도 개발을 시행합니다.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# 테스트 주도 개발 워크플로우
|
||||||
|
|
||||||
|
이 스킬은 모든 코드 개발이 포괄적인 테스트 커버리지와 함께 TDD 원칙을 따르도록 보장합니다.
|
||||||
|
|
||||||
|
## 활성화 시점
|
||||||
|
|
||||||
|
- 새 기능이나 기능성을 작성할 때
|
||||||
|
- 버그나 이슈를 수정할 때
|
||||||
|
- 기존 코드를 리팩터링할 때
|
||||||
|
- API 엔드포인트를 추가할 때
|
||||||
|
- 새 컴포넌트를 생성할 때
|
||||||
|
|
||||||
|
## 핵심 원칙
|
||||||
|
|
||||||
|
### 1. 코드보다 테스트가 먼저
|
||||||
|
항상 테스트를 먼저 작성한 후, 테스트를 통과시키는 코드를 구현합니다.
|
||||||
|
|
||||||
|
### 2. 커버리지 요구 사항
|
||||||
|
- 최소 80% 커버리지 (단위 + 통합 + E2E)
|
||||||
|
- 모든 엣지 케이스 커버
|
||||||
|
- 에러 시나리오 테스트
|
||||||
|
- 경계 조건 검증
|
||||||
|
|
||||||
|
### 3. 테스트 유형
|
||||||
|
|
||||||
|
#### 단위 테스트
|
||||||
|
- 개별 함수 및 유틸리티
|
||||||
|
- 컴포넌트 로직
|
||||||
|
- 순수 함수
|
||||||
|
- 헬퍼 및 유틸리티
|
||||||
|
|
||||||
|
#### 통합 테스트
|
||||||
|
- API 엔드포인트
|
||||||
|
- 데이터베이스 작업
|
||||||
|
- 서비스 상호작용
|
||||||
|
- 외부 API 호출
|
||||||
|
|
||||||
|
#### E2E 테스트 (Playwright)
|
||||||
|
- 핵심 사용자 플로우
|
||||||
|
- 완전한 워크플로우
|
||||||
|
- 브라우저 자동화
|
||||||
|
- UI 상호작용
|
||||||
|
|
||||||
|
## TDD 워크플로우 단계
|
||||||
|
|
||||||
|
### 단계 1: 사용자 여정 작성
|
||||||
|
```
|
||||||
|
As a [role], I want to [action], so that [benefit]
|
||||||
|
|
||||||
|
Example:
|
||||||
|
As a user, I want to search for markets semantically,
|
||||||
|
so that I can find relevant markets even without exact keywords.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 단계 2: 테스트 케이스 생성
|
||||||
|
각 사용자 여정에 대해 포괄적인 테스트 케이스를 작성합니다:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('Semantic Search', () => {
|
||||||
|
it('returns relevant markets for query', async () => {
|
||||||
|
// Test implementation
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles empty query gracefully', async () => {
|
||||||
|
// Test edge case
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to substring search when Redis unavailable', async () => {
|
||||||
|
// Test fallback behavior
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sorts results by similarity score', async () => {
|
||||||
|
// Test sorting logic
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 단계 3: 테스트 실행 (실패해야 함)
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
# Tests should fail - we haven't implemented yet
|
||||||
|
```
|
||||||
|
|
||||||
|
### 단계 4: 코드 구현
|
||||||
|
테스트를 통과시키기 위한 최소한의 코드를 작성합니다:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Implementation guided by tests
|
||||||
|
export async function searchMarkets(query: string) {
|
||||||
|
// Implementation here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 단계 5: 테스트 재실행
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
# Tests should now pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### 단계 6: 리팩터링
|
||||||
|
테스트가 통과하는 상태를 유지하면서 코드 품질을 개선합니다:
|
||||||
|
- 중복 제거
|
||||||
|
- 네이밍 개선
|
||||||
|
- 성능 최적화
|
||||||
|
- 가독성 향상
|
||||||
|
|
||||||
|
### 단계 7: 커버리지 확인
|
||||||
|
```bash
|
||||||
|
npm run test:coverage
|
||||||
|
# Verify 80%+ coverage achieved
|
||||||
|
```
|
||||||
|
|
||||||
|
## 테스트 패턴
|
||||||
|
|
||||||
|
### 단위 테스트 패턴 (Jest/Vitest)
|
||||||
|
```typescript
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
|
import { Button } from './Button'
|
||||||
|
|
||||||
|
describe('Button Component', () => {
|
||||||
|
it('renders with correct text', () => {
|
||||||
|
render(<Button>Click me</Button>)
|
||||||
|
expect(screen.getByText('Click me')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls onClick when clicked', () => {
|
||||||
|
const handleClick = jest.fn()
|
||||||
|
render(<Button onClick={handleClick}>Click</Button>)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button'))
|
||||||
|
|
||||||
|
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is disabled when disabled prop is true', () => {
|
||||||
|
render(<Button disabled>Click</Button>)
|
||||||
|
expect(screen.getByRole('button')).toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 통합 테스트 패턴
|
||||||
|
```typescript
|
||||||
|
import { NextRequest } from 'next/server'
|
||||||
|
import { GET } from './route'
|
||||||
|
|
||||||
|
describe('GET /api/markets', () => {
|
||||||
|
it('returns markets successfully', async () => {
|
||||||
|
const request = new NextRequest('http://localhost/api/markets')
|
||||||
|
const response = await GET(request)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(data.success).toBe(true)
|
||||||
|
expect(Array.isArray(data.data)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('validates query parameters', async () => {
|
||||||
|
const request = new NextRequest('http://localhost/api/markets?limit=invalid')
|
||||||
|
const response = await GET(request)
|
||||||
|
|
||||||
|
expect(response.status).toBe(400)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles database errors gracefully', async () => {
|
||||||
|
// Mock database failure
|
||||||
|
const request = new NextRequest('http://localhost/api/markets')
|
||||||
|
// Test error handling
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### E2E 테스트 패턴 (Playwright)
|
||||||
|
```typescript
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
test('user can search and filter markets', async ({ page }) => {
|
||||||
|
// Navigate to markets page
|
||||||
|
await page.goto('/')
|
||||||
|
await page.click('a[href="/markets"]')
|
||||||
|
|
||||||
|
// Verify page loaded
|
||||||
|
await expect(page.locator('h1')).toContainText('Markets')
|
||||||
|
|
||||||
|
// Search for markets
|
||||||
|
await page.fill('input[placeholder="Search markets"]', 'election')
|
||||||
|
|
||||||
|
// Wait for debounce and results
|
||||||
|
await page.waitForTimeout(600)
|
||||||
|
|
||||||
|
// Verify search results displayed
|
||||||
|
const results = page.locator('[data-testid="market-card"]')
|
||||||
|
await expect(results).toHaveCount(5, { timeout: 5000 })
|
||||||
|
|
||||||
|
// Verify results contain search term
|
||||||
|
const firstResult = results.first()
|
||||||
|
await expect(firstResult).toContainText('election', { ignoreCase: true })
|
||||||
|
|
||||||
|
// Filter by status
|
||||||
|
await page.click('button:has-text("Active")')
|
||||||
|
|
||||||
|
// Verify filtered results
|
||||||
|
await expect(results).toHaveCount(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('user can create a new market', async ({ page }) => {
|
||||||
|
// Login first
|
||||||
|
await page.goto('/creator-dashboard')
|
||||||
|
|
||||||
|
// Fill market creation form
|
||||||
|
await page.fill('input[name="name"]', 'Test Market')
|
||||||
|
await page.fill('textarea[name="description"]', 'Test description')
|
||||||
|
await page.fill('input[name="endDate"]', '2025-12-31')
|
||||||
|
|
||||||
|
// Submit form
|
||||||
|
await page.click('button[type="submit"]')
|
||||||
|
|
||||||
|
// Verify success message
|
||||||
|
await expect(page.locator('text=Market created successfully')).toBeVisible()
|
||||||
|
|
||||||
|
// Verify redirect to market page
|
||||||
|
await expect(page).toHaveURL(/\/markets\/test-market/)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 테스트 파일 구성
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/
|
||||||
|
│ ├── Button/
|
||||||
|
│ │ ├── Button.tsx
|
||||||
|
│ │ ├── Button.test.tsx # Unit tests
|
||||||
|
│ │ └── Button.stories.tsx # Storybook
|
||||||
|
│ └── MarketCard/
|
||||||
|
│ ├── MarketCard.tsx
|
||||||
|
│ └── MarketCard.test.tsx
|
||||||
|
├── app/
|
||||||
|
│ └── api/
|
||||||
|
│ └── markets/
|
||||||
|
│ ├── route.ts
|
||||||
|
│ └── route.test.ts # Integration tests
|
||||||
|
└── e2e/
|
||||||
|
├── markets.spec.ts # E2E tests
|
||||||
|
├── trading.spec.ts
|
||||||
|
└── auth.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 외부 서비스 모킹
|
||||||
|
|
||||||
|
### Supabase Mock
|
||||||
|
```typescript
|
||||||
|
jest.mock('@/lib/supabase', () => ({
|
||||||
|
supabase: {
|
||||||
|
from: jest.fn(() => ({
|
||||||
|
select: jest.fn(() => ({
|
||||||
|
eq: jest.fn(() => Promise.resolve({
|
||||||
|
data: [{ id: 1, name: 'Test Market' }],
|
||||||
|
error: null
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis Mock
|
||||||
|
```typescript
|
||||||
|
jest.mock('@/lib/redis', () => ({
|
||||||
|
searchMarketsByVector: jest.fn(() => Promise.resolve([
|
||||||
|
{ slug: 'test-market', similarity_score: 0.95 }
|
||||||
|
])),
|
||||||
|
checkRedisHealth: jest.fn(() => Promise.resolve({ connected: true }))
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenAI Mock
|
||||||
|
```typescript
|
||||||
|
jest.mock('@/lib/openai', () => ({
|
||||||
|
generateEmbedding: jest.fn(() => Promise.resolve(
|
||||||
|
new Array(1536).fill(0.1) // Mock 1536-dim embedding
|
||||||
|
))
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 테스트 커버리지 검증
|
||||||
|
|
||||||
|
### 커버리지 리포트 실행
|
||||||
|
```bash
|
||||||
|
npm run test:coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### 커버리지 임계값
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jest": {
|
||||||
|
"coverageThresholds": {
|
||||||
|
"global": {
|
||||||
|
"branches": 80,
|
||||||
|
"functions": 80,
|
||||||
|
"lines": 80,
|
||||||
|
"statements": 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 흔한 테스트 실수
|
||||||
|
|
||||||
|
### 잘못된 예: 구현 세부사항 테스트
|
||||||
|
```typescript
|
||||||
|
// Don't test internal state
|
||||||
|
expect(component.state.count).toBe(5)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 올바른 예: 사용자에게 보이는 동작 테스트
|
||||||
|
```typescript
|
||||||
|
// Test what users see
|
||||||
|
expect(screen.getByText('Count: 5')).toBeInTheDocument()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 잘못된 예: 취약한 셀렉터
|
||||||
|
```typescript
|
||||||
|
// Breaks easily
|
||||||
|
await page.click('.css-class-xyz')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 올바른 예: 시맨틱 셀렉터
|
||||||
|
```typescript
|
||||||
|
// Resilient to changes
|
||||||
|
await page.click('button:has-text("Submit")')
|
||||||
|
await page.click('[data-testid="submit-button"]')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 잘못된 예: 테스트 격리 없음
|
||||||
|
```typescript
|
||||||
|
// Tests depend on each other
|
||||||
|
test('creates user', () => { /* ... */ })
|
||||||
|
test('updates same user', () => { /* depends on previous test */ })
|
||||||
|
```
|
||||||
|
|
||||||
|
### 올바른 예: 독립적인 테스트
|
||||||
|
```typescript
|
||||||
|
// Each test sets up its own data
|
||||||
|
test('creates user', () => {
|
||||||
|
const user = createTestUser()
|
||||||
|
// Test logic
|
||||||
|
})
|
||||||
|
|
||||||
|
test('updates user', () => {
|
||||||
|
const user = createTestUser()
|
||||||
|
// Update logic
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 지속적 테스트
|
||||||
|
|
||||||
|
### 개발 중 Watch 모드
|
||||||
|
```bash
|
||||||
|
npm test -- --watch
|
||||||
|
# Tests run automatically on file changes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-Commit Hook
|
||||||
|
```bash
|
||||||
|
# Runs before every commit
|
||||||
|
npm test && npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI/CD 통합
|
||||||
|
```yaml
|
||||||
|
# GitHub Actions
|
||||||
|
- name: Run Tests
|
||||||
|
run: npm test -- --coverage
|
||||||
|
- name: Upload Coverage
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 모범 사례
|
||||||
|
|
||||||
|
1. **테스트 먼저 작성** - 항상 TDD
|
||||||
|
2. **테스트당 하나의 Assert** - 단일 동작에 집중
|
||||||
|
3. **설명적인 테스트 이름** - 무엇을 테스트하는지 설명
|
||||||
|
4. **Arrange-Act-Assert** - 명확한 테스트 구조
|
||||||
|
5. **외부 의존성 모킹** - 단위 테스트 격리
|
||||||
|
6. **엣지 케이스 테스트** - null, undefined, 빈 값, 큰 값
|
||||||
|
7. **에러 경로 테스트** - 정상 경로만이 아닌
|
||||||
|
8. **테스트 속도 유지** - 단위 테스트 각 50ms 미만
|
||||||
|
9. **테스트 후 정리** - 부작용 없음
|
||||||
|
10. **커버리지 리포트 검토** - 누락 부분 식별
|
||||||
|
|
||||||
|
## 성공 지표
|
||||||
|
|
||||||
|
- 80% 이상의 코드 커버리지 달성
|
||||||
|
- 모든 테스트 통과 (그린)
|
||||||
|
- 건너뛴 테스트나 비활성화된 테스트 없음
|
||||||
|
- 빠른 테스트 실행 (단위 테스트 30초 미만)
|
||||||
|
- E2E 테스트가 핵심 사용자 플로우를 커버
|
||||||
|
- 테스트가 프로덕션 이전에 버그를 포착
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**기억하세요**: 테스트는 선택 사항이 아닙니다. 테스트는 자신감 있는 리팩터링, 빠른 개발, 그리고 프로덕션 안정성을 가능하게 하는 안전망입니다.
|
||||||
126
docs/ko-KR/skills/verification-loop/SKILL.md
Normal file
126
docs/ko-KR/skills/verification-loop/SKILL.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
name: verification-loop
|
||||||
|
description: "Claude Code 세션을 위한 포괄적인 검증 시스템."
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# 검증 루프 스킬
|
||||||
|
|
||||||
|
Claude Code 세션을 위한 포괄적인 검증 시스템.
|
||||||
|
|
||||||
|
## 사용 시점
|
||||||
|
|
||||||
|
다음 상황에서 이 스킬을 호출하세요:
|
||||||
|
- 기능 또는 주요 코드 변경을 완료한 후
|
||||||
|
- PR을 생성하기 전
|
||||||
|
- 품질 게이트가 통과하는지 확인하고 싶을 때
|
||||||
|
- 리팩터링 후
|
||||||
|
|
||||||
|
## 검증 단계
|
||||||
|
|
||||||
|
### 단계 1: 빌드 검증
|
||||||
|
```bash
|
||||||
|
# Check if project builds
|
||||||
|
npm run build 2>&1 | tail -20
|
||||||
|
# OR
|
||||||
|
pnpm build 2>&1 | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
빌드가 실패하면 계속하기 전에 중단하고 수정합니다.
|
||||||
|
|
||||||
|
### 단계 2: 타입 검사
|
||||||
|
```bash
|
||||||
|
# TypeScript projects
|
||||||
|
npx tsc --noEmit 2>&1 | head -30
|
||||||
|
|
||||||
|
# Python projects
|
||||||
|
pyright . 2>&1 | head -30
|
||||||
|
```
|
||||||
|
|
||||||
|
모든 타입 에러를 보고합니다. 중요한 것은 계속하기 전에 수정합니다.
|
||||||
|
|
||||||
|
### 단계 3: 린트 검사
|
||||||
|
```bash
|
||||||
|
# JavaScript/TypeScript
|
||||||
|
npm run lint 2>&1 | head -30
|
||||||
|
|
||||||
|
# Python
|
||||||
|
ruff check . 2>&1 | head -30
|
||||||
|
```
|
||||||
|
|
||||||
|
### 단계 4: 테스트 스위트
|
||||||
|
```bash
|
||||||
|
# Run tests with coverage
|
||||||
|
npm run test -- --coverage 2>&1 | tail -50
|
||||||
|
|
||||||
|
# Check coverage threshold
|
||||||
|
# Target: 80% minimum
|
||||||
|
```
|
||||||
|
|
||||||
|
보고 항목:
|
||||||
|
- 전체 테스트: X
|
||||||
|
- 통과: X
|
||||||
|
- 실패: X
|
||||||
|
- 커버리지: X%
|
||||||
|
|
||||||
|
### 단계 5: 보안 스캔
|
||||||
|
```bash
|
||||||
|
# Check for secrets
|
||||||
|
grep -rn "sk-" --include="*.ts" --include="*.js" . 2>/dev/null | head -10
|
||||||
|
grep -rn "api_key" --include="*.ts" --include="*.js" . 2>/dev/null | head -10
|
||||||
|
|
||||||
|
# Check for console.log
|
||||||
|
grep -rn "console.log" --include="*.ts" --include="*.tsx" src/ 2>/dev/null | head -10
|
||||||
|
```
|
||||||
|
|
||||||
|
### 단계 6: Diff 리뷰
|
||||||
|
```bash
|
||||||
|
# Show what changed
|
||||||
|
git diff --stat
|
||||||
|
git diff HEAD~1 --name-only
|
||||||
|
```
|
||||||
|
|
||||||
|
각 변경된 파일에서 다음을 검토합니다:
|
||||||
|
- 의도하지 않은 변경
|
||||||
|
- 누락된 에러 처리
|
||||||
|
- 잠재적 엣지 케이스
|
||||||
|
|
||||||
|
## 출력 형식
|
||||||
|
|
||||||
|
모든 단계를 실행한 후 검증 보고서를 생성합니다:
|
||||||
|
|
||||||
|
```
|
||||||
|
VERIFICATION REPORT
|
||||||
|
==================
|
||||||
|
|
||||||
|
Build: [PASS/FAIL]
|
||||||
|
Types: [PASS/FAIL] (X errors)
|
||||||
|
Lint: [PASS/FAIL] (X warnings)
|
||||||
|
Tests: [PASS/FAIL] (X/Y passed, Z% coverage)
|
||||||
|
Security: [PASS/FAIL] (X issues)
|
||||||
|
Diff: [X files changed]
|
||||||
|
|
||||||
|
Overall: [READY/NOT READY] for PR
|
||||||
|
|
||||||
|
Issues to Fix:
|
||||||
|
1. ...
|
||||||
|
2. ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 연속 모드
|
||||||
|
|
||||||
|
긴 세션에서는 15분마다 또는 주요 변경 후에 검증을 실행합니다:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Set a mental checkpoint:
|
||||||
|
- After completing each function
|
||||||
|
- After finishing a component
|
||||||
|
- Before moving to next task
|
||||||
|
|
||||||
|
Run: /verify
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hook과의 통합
|
||||||
|
|
||||||
|
이 스킬은 PostToolUse Hook을 보완하지만 더 깊은 검증을 제공합니다.
|
||||||
|
Hook은 즉시 문제를 포착하고, 이 스킬은 포괄적인 검토를 제공합니다.
|
||||||
Reference in New Issue
Block a user