From ecc
AI 보조 개발을 위한 회귀 테스트 전략. 데이터베이스 의존성 없는 샌드박스 모드 API 테스트, 자동화된 버그 점검 워크플로, 코드를 작성하고 리뷰하는 동일 모델의 AI 사각지대를 포착하는 패턴.
npx claudepluginhub sam42-lab/everything-claude-code-krThis skill uses the workspace's default tool permissions.
AI가 코드를 작성하고 리뷰하는 AI 보조 개발을 위해 설계된 테스트 패턴입니다. 이 경우 동일한 모델이 코드를 작성하고 검토하므로 자동화된 테스트로만 포착할 수 있는 체계적인 사각지대가 발생합니다.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
AI가 코드를 작성하고 리뷰하는 AI 보조 개발을 위해 설계된 테스트 패턴입니다. 이 경우 동일한 모델이 코드를 작성하고 검토하므로 자동화된 테스트로만 포착할 수 있는 체계적인 사각지대가 발생합니다.
/bug-check 또는 유사한 리뷰 명령을 실행할 때AI가 코드를 작성하고 자신의 작업을 스스로 리뷰할 때, 두 단계 모두에 동일한 가정을 적용하게 됩니다. 이는 다음과 같은 예측 가능한 실패 패턴을 만듭니다.
AI가 수정안 작성 → AI가 수정안 리뷰 → AI가 "정확해 보임"이라고 함 → 버그는 여전히 존재
실제 사례 (운영 환경에서 관측됨):
수정 1: API 응답에 notification_settings 추가
→ SELECT 쿼리에 추가하는 것을 잊음
→ AI가 리뷰했지만 놓침 (동일한 사각지대)
수정 2: SELECT 쿼리에 추가
→ TypeScript 빌드 에러 (생성된 타입에 컬럼이 없음)
→ AI가 수정 1을 리뷰했지만 SELECT 문제를 잡지 못함
수정 3: SELECT *로 변경
→ 프로덕션 경로는 수정되었으나 샌드박스 경로를 잊음
→ AI가 리뷰했지만 또 놓침 (4번째 발생)
수정 4: 첫 실행에서 테스트가 즉시 포착함 PASS:
패턴: 샌드박스/프로덕션 경로의 불일치는 AI가 도입하는 가장 빈번한 회귀(regression) 문제입니다.
AI 친화적인 아키텍처를 가진 대부분의 프로젝트에는 샌드박스/모크 모드가 있습니다. 이것이 DB 없이 빠른 API 테스트를 수행하는 핵심입니다.
// vitest.config.ts
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
environment: "node",
globals: true,
include: ["__tests__/**/*.test.ts"],
setupFiles: ["__tests__/setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "."),
},
},
});
// __tests__/setup.ts
// 샌드박스 모드 강제 - 데이터베이스 필요 없음
process.env.SANDBOX_MODE = "true";
process.env.NEXT_PUBLIC_SUPABASE_URL = "";
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "";
// __tests__/helpers.ts
import { NextRequest } from "next/server";
export function createTestRequest(
url: string,
options?: {
method?: string;
body?: Record<string, unknown>;
headers?: Record<string, string>;
sandboxUserId?: string;
},
): NextRequest {
const { method = "GET", body, headers = {}, sandboxUserId } = options || {};
const fullUrl = url.startsWith("http") ? url : `http://localhost:3000${url}`;
const reqHeaders: Record<string, string> = { ...headers };
if (sandboxUserId) {
reqHeaders["x-sandbox-user-id"] = sandboxUserId;
}
const init: { method: string; headers: Record<string, string>; body?: string } = {
method,
headers: reqHeaders,
};
if (body) {
init.body = JSON.stringify(body);
reqHeaders["content-type"] = "application/json";
}
return new NextRequest(fullUrl, init);
}
export async function parseResponse(response: Response) {
const json = await response.json();
return { status: response.status, json };
}
핵심 원칙: 작동하는 코드가 아니라, 발견된 버그에 대한 테스트를 작성하세요.
// __tests__/api/user/profile.test.ts
import { describe, it, expect } from "vitest";
import { createTestRequest, parseResponse } from "../../helpers";
import { GET, PATCH } from "@/app/api/user/profile/route";
// 계약 정의 - 응답에 반드시 포함되어야 하는 필드들
const REQUIRED_FIELDS = [
"id",
"email",
"full_name",
"phone",
"role",
"created_at",
"avatar_url",
"notification_settings", // ← 버그 발견 후 누락된 것을 확인하여 추가됨
];
describe("GET /api/user/profile", () => {
it("모든 필수 필드를 반환한다", async () => {
const req = createTestRequest("/api/user/profile");
const res = await GET(req);
const { status, json } = await parseResponse(res);
expect(status).toBe(200);
for (const field of REQUIRED_FIELDS) {
expect(json.data).toHaveProperty(field);
}
});
// 회귀 테스트 - 이 정확한 버그가 AI에 의해 4번이나 도입됨
it("notification_settings가 undefined가 아니다 (BUG-R1 regression)", async () => {
const req = createTestRequest("/api/user/profile");
const res = await GET(req);
const { json } = await parseResponse(res);
expect("notification_settings" in json.data).toBe(true);
const ns = json.data.notification_settings;
expect(ns === null || typeof ns === "object").toBe(true);
});
});
가장 흔한 AI 회귀 현상: 프로덕션 경로는 고쳤지만 샌드박스 경로를 잊어버리는 경우(또는 그 반대).
// 샌드박스 응답이 예상된 계약과 일치하는지 테스트
describe("GET /api/user/messages (대화 목록)", () => {
it("샌드박스 모드에서 partner_name을 포함한다", async () => {
const req = createTestRequest("/api/user/messages", {
sandboxUserId: "user-001",
});
const res = await GET(req);
const { json } = await parseResponse(res);
// 프로덕션 경로에는 partner_name이 추가되었지만
// 샌드박스 경로에는 누락되었던 버그를 포착함
if (json.data.length > 0) {
for (const conv of json.data) {
expect("partner_name" in conv).toBe(true);
}
}
});
});
<!-- .claude/commands/bug-check.md -->
# 버그 점검 (Bug Check)
## 1단계: 자동화된 테스트 (필수, 건너뛰기 불가)
코드 리뷰 전에 반드시 이 명령들을 먼저 실행하세요:
npm run test # Vitest 테스트 스위트
npm run build # TypeScript 타입 체크 + 빌드
- 테스트 실패 시 → 최우선 순위 버그로 보고
- 빌드 실패 시 → 타입 에러를 최우선 순위로 보고
- 두 가지 모두 통과한 경우에만 2단계로 진행
## 2단계: 코드 리뷰 (AI 리뷰)
1. 샌드박스 / 프로덕션 경로의 일관성
2. API 응답 형태가 프론트엔드 기대치와 일치하는지 확인
3. SELECT 절의 완전성
4. 롤백을 포함한 에러 처리
5. 낙관적 업데이트(Optimistic update)의 레이스 컨디션
## 3단계: 수정된 각 버그에 대해 회귀 테스트 제안
사용자: "버그 체크해줘" (또는 "/bug-check")
│
├─ 1단계: npm run test
│ ├─ 실패 → 기계적으로 버그 발견 (AI 판단 필요 없음)
│ └─ 통과 → 계속 진행
│
├─ 2단계: npm run build
│ ├─ 실패 → 기계적으로 타입 에러 발견
│ └─ 통과 → 계속 진행
│
├─ 3단계: AI 코드 리뷰 (알려진 사각지대를 염두에 둠)
│ └─ 발견 사항 보고
│
└─ 4단계: 각 수정 사항에 대해 회귀 테스트 작성
└─ 다음 버그 체크 시 수정 사항이 깨지는지 포착
빈도: 가장 흔함 (회귀 사례 4개 중 3개에서 관측됨)
// 실패: AI가 프로덕션 경로에만 필드를 추가함
if (isSandboxMode()) {
return { data: { id, email, name } }; // 새 필드 누락
}
// 프로덕션 경로
return { data: { id, email, name, notification_settings } };
// 통과: 두 경로 모두 동일한 형태를 반환해야 함
if (isSandboxMode()) {
return { data: { id, email, name, notification_settings: null } };
}
return { data: { id, email, name, notification_settings } };
이를 잡기 위한 테스트:
it("샌드박스와 프로덕션이 동일한 필드를 반환한다", async () => {
// 테스트 환경에서는 샌드박스 모드가 강제됨
const res = await GET(createTestRequest("/api/user/profile"));
const { json } = await parseResponse(res);
for (const field of REQUIRED_FIELDS) {
expect(json.data).toHaveProperty(field);
}
});
빈도: 새 컬럼을 추가할 때 Supabase/Prisma에서 흔히 발생
// 실패: 응답에는 새 컬럼을 추가했지만 SELECT에는 추가하지 않음
const { data } = await supabase
.from("users")
.select("id, email, name") // 여기에 notification_settings가 없음
.single();
return { data: { ...data, notification_settings: data.notification_settings } };
// → notification_settings는 항상 undefined임
// 통과: SELECT *를 사용하거나 새 컬럼을 명시적으로 포함
const { data } = await supabase
.from("users")
.select("*")
.single();
빈도: 보통 - 기존 컴포넌트에 에러 처리를 추가할 때 발생
// 실패: 에러 상태는 설정했지만 이전 데이터가 지워지지 않음
catch (err) {
setError("로딩 실패");
// reservations에는 여전히 이전 탭의 데이터가 표시됨!
}
// 통과: 에러 시 관련 상태 초기화
catch (err) {
setReservations([]); // 오래된 데이터 제거
setError("로딩 실패");
}
// 실패: 실패 시 롤백 없음
const handleRemove = async (id: string) => {
setItems(prev => prev.filter(i => i.id !== id));
await fetch(`/api/items/${id}`, { method: "DELETE" });
// API 실패 시, UI에서는 사라졌지만 DB에는 여전히 존재함
};
// 통과: 이전 상태를 캡처하고 실패 시 롤백
const handleRemove = async (id: string) => {
const prevItems = [...items];
setItems(prev => prev.filter(i => i.id !== id));
try {
const res = await fetch(`/api/items/${id}`, { method: "DELETE" });
if (!res.ok) throw new Error("API 에러");
} catch {
setItems(prevItems); // 롤백
alert("삭제에 실패했습니다");
}
};
100% 커버리지를 목표로 하지 마세요. 대신:
/api/user/profile에서 버그 발견 → profile API용 테스트 작성
/api/user/messages에서 버그 발견 → messages API용 테스트 작성
/api/user/favorites에서 버그 발견 → favorites API용 테스트 작성
/api/user/notifications에 버그 없음 → (아직) 테스트 작성 안 함
이 방식이 AI 개발에서 효과적인 이유:
| AI 회귀 패턴 | 테스트 전략 | 우선순위 |
|---|---|---|
| 샌드박스/프로덕션 불일치 | 샌드박스 모드에서 동일한 응답 형태 확인 | 높음 |
| SELECT 절 누락 | 응답의 모든 필수 필드 확인 | 높음 |
| 에러 상태 누수 | 에러 발생 시 상태 정리 확인 | 중간 |
| 롤백 누락 | API 실패 시 상태 복구 확인 | 중간 |
| Null을 가리는 타입 캐스팅 | 필드가 undefined가 아님을 확인 | 중간 |
권장 사항:
금지 사항: