mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +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