AI 코드를 신뢰하는 방법: 테스트 전략
테스트는 버그를 잡는 게 아니라 AI를 통제하는 수단이다. 바이브 코딩 시대의 테스트 피라미드와 실전 전략을 다룬다.
AI가 만든 코드를 어떻게 믿을 수 있나?
지난 글들에서 계속 "검증"이 중요하다고 했다. 이번 글에서는 그 검증을 어떻게 하는지, 테스트 전략을 구체적으로 다룬다.
테스트의 목적이 달라졌다
전통적으로 테스트는 "내가 짠 코드가 맞는지" 확인하는 용도였다. 내가 로직을 알고 있으니까, 그 로직이 의도대로 동작하는지 검증.
바이브 코딩에서는 다르다. 내가 코드를 안 짰다. AI가 짰다. 그래서 테스트의 목적이 달라진다.
- 전통: 내 코드가 의도대로 동작하는지 확인
- 바이브: AI 코드가 내 의도를 제대로 구현했는지 확인
미묘한 차이 같지만 중요하다. AI 코드는 "그럴듯해 보이는데 구멍이 있는" 경우가 많다. 테스트는 그 구멍을 찾는 수단이다.
테스트 피라미드 재해석
전통적인 테스트 피라미드:
/\
/ \ E2E (적음)
/____\
/ \ 통합 (중간)
/________\
/ \ 유닛 (많음)
/____________\
유닛 테스트가 많고, 위로 갈수록 적어진다. 이게 교과서적인 답이었다.
바이브 코딩에서는?
/\
/ \ E2E (핵심만)
/____\
/ \ 통합 (많음) ← 강조
/________\
/ \ 유닛 (선택적)
/____________\
통합 테스트가 더 중요해진다. 왜냐면:
- 유닛 테스트: AI가 만든 내부 구현을 테스트해봤자, 내가 그 구현을 모른다
- 통합 테스트: "이 API 호출하면 이 결과 나와야 해"는 명확하다
- E2E: 비용이 크지만, 핵심 흐름은 반드시 커버
유닛 테스트: 선택적으로
AI 코드의 유닛 테스트는 생각보다 가치가 낮다.
// AI가 만든 함수
function calculateDiscount(price: number, userLevel: string) {
if (userLevel === 'VIP') return price * 0.8
if (userLevel === 'GOLD') return price * 0.9
return price
}
// 유닛 테스트
test('VIP는 20% 할인', () => {
expect(calculateDiscount(100, 'VIP')).toBe(80)
})이 테스트가 의미 있으려면, 내가 "VIP는 20% 할인"이라는 스펙을 알아야 한다. 근데 그 스펙을 알면 구현도 내가 확인할 수 있다.
유닛 테스트가 유용한 경우:
- 복잡한 계산 로직
- 경계값 처리가 중요한 경우
- 여러 곳에서 재사용되는 유틸 함수
유닛 테스트가 불필요한 경우:
- 단순한 CRUD
- 프레임워크가 처리하는 로직
- 구현 세부사항이 자주 바뀌는 부분
통합 테스트: 핵심
AI 코드 검증에서 통합 테스트가 핵심인 이유:
- 입출력이 명확하다: "이 요청 보내면 이 응답 와야 해"
- 내부 구현 몰라도 된다: AI가 어떻게 짰든, 결과만 맞으면 됨
- 실제 사용 환경과 비슷하다: 유닛보다 현실적
통합 테스트 예시
// API 통합 테스트
describe('POST /api/login', () => {
test('올바른 정보로 로그인 성공', async () => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email: 'test@example.com', password: 'password123' })
})
const data = await response.json()
expect(response.status).toBe(200)
expect(data.user).toBeDefined()
expect(data.token).toBeDefined()
})
test('존재하지 않는 이메일', async () => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email: 'nobody@example.com', password: 'password123' })
})
const data = await response.json()
expect(response.status).toBe(401)
expect(data.code).toBe('USER_NOT_FOUND')
})
test('비밀번호 틀림', async () => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email: 'test@example.com', password: 'wrong' })
})
const data = await response.json()
expect(response.status).toBe(401)
expect(data.code).toBe('INVALID_PASSWORD')
})
})이 테스트들은 Spec에서 정의한 케이스를 그대로 검증한다. AI 구현이 어떻든, 이 테스트가 통과하면 스펙대로 동작하는 거다.
E2E 테스트: 핵심 흐름만
E2E는 비용이 크다. 느리고, 불안정하고, 유지보수도 힘들다. 그래서 핵심 사용자 흐름만 커버한다.
핵심 흐름 정의
"우리 서비스에서 이게 안 되면 망한다" 싶은 것들:
블로그 예시:
1. 홈페이지 → 포스트 목록 보임
2. 포스트 클릭 → 내용 보임
3. 검색 → 결과 나옴
쇼핑몰 예시:
1. 상품 목록 → 상품 보임
2. 상품 클릭 → 상세 보임
3. 장바구니 담기 → 담김
4. 결제 → 주문 생성
이 흐름들만 E2E로 커버하고, 나머지는 통합 테스트로.
E2E 예시 (Playwright)
test('포스트 조회 흐름', async ({ page }) => {
// 홈페이지
await page.goto('/')
await expect(page.locator('h1')).toContainText('블로그')
// 포스트 목록 확인
const posts = page.locator('[data-testid="post-item"]')
await expect(posts.first()).toBeVisible()
// 첫 번째 포스트 클릭
await posts.first().click()
// 포스트 내용 확인
await expect(page.locator('article')).toBeVisible()
})"깨지면 안 되는 지점" 찾기
커버리지 100%를 목표로 하면 망한다. 대신 "이게 깨지면 진짜 문제" 인 지점을 찾는다.
질문해보기
-
이 기능이 안 되면 사용자가 뭘 못 하나?
- 로그인 안 됨 → 아무것도 못 함 → 반드시 테스트
- 다크모드 안 됨 → 불편함 → 선택적
-
이 버그가 나면 돈을 잃나?
- 결제 금액 계산 오류 → 돈 잃음 → 반드시 테스트
- 날짜 포맷 이상함 → 불편함 → 선택적
-
이 기능이 조용히 깨질 수 있나?
- API 응답 형식 변경 → 조용히 깨짐 → 반드시 테스트
- 버튼 색상 이상함 → 바로 보임 → 선택적
AI에게 테스트 작성 시키기
테스트도 AI에게 시킬 수 있다. 근데 주의할 점이 있다.
잘못된 방법
"이 코드에 대한 테스트 작성해줘"
→ AI가 구현에 맞춘 테스트를 작성함
→ 구현이 틀려도 테스트가 통과할 수 있음
올바른 방법
"이 Spec에 대한 테스트를 작성해줘"
[Spec]
- POST /api/login
- 성공: 200, { user, token }
- 실패(사용자 없음): 401, { code: 'USER_NOT_FOUND' }
- 실패(비밀번호 틀림): 401, { code: 'INVALID_PASSWORD' }
구현 코드는 참조하지 말고, Spec만 보고 테스트를 작성해.
Spec 기반으로 테스트를 작성하면, 구현과 독립적인 테스트가 나온다. 이게 AI 코드를 검증하는 올바른 방법이다.
테스트 자동화 파이프라인
테스트를 작성했으면 자동으로 돌아야 한다.
[로컬]
코드 수정 → 저장 → 관련 테스트 자동 실행
[CI]
PR 생성 → 전체 테스트 실행 → 통과해야 머지
권장 설정
// package.json
{
"scripts": {
"test": "vitest",
"test:watch": "vitest watch",
"test:ci": "vitest run --coverage"
}
}# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run test:ci실패하는 테스트 다루기
AI 코드로 테스트가 실패하면:
-
테스트가 틀린 건가, 코드가 틀린 건가?
- Spec 다시 확인
- 테스트가 Spec을 제대로 반영하는지 확인
-
코드가 틀렸으면
테스트 실패 결과: - 예상: 401, { code: 'USER_NOT_FOUND' } - 실제: 500, { error: 'Internal Server Error' } 존재하지 않는 사용자로 로그인할 때 500이 아니라 401과 USER_NOT_FOUND를 반환하도록 수정해줘. -
테스트가 틀렸으면
- Spec을 수정하거나
- 테스트를 Spec에 맞게 수정
현실적인 테스트 전략
"이상적인" 테스트 전략 말고, 실제로 할 수 있는 걸 정리하면:
MVP/프로토타입 단계
- E2E: 핵심 1~2개 흐름만
- 통합: API 성공/실패 기본 케이스만
- 유닛: 없어도 됨
성장 단계
- E2E: 핵심 흐름 + 돈 관련
- 통합: 모든 API의 주요 케이스
- 유닛: 복잡한 로직만
안정화 단계
- E2E: 주요 사용자 시나리오 전체
- 통합: 모든 케이스
- 유닛: 재사용 로직
마무리
바이브 코딩에서 테스트는 선택이 아니다. AI 코드를 신뢰하려면 테스트가 있어야 한다.
핵심:
- 통합 테스트 중심 - 입출력으로 검증
- Spec 기반 - 구현 말고 Spec을 테스트
- 핵심 흐름 우선 - 커버리지보다 중요도
- 자동화 - 수동으로 하면 안 함
다음 편에서는 1인 개발자가 이 모든 걸 어떻게 운영하는지, 시스템을 다룬다.
바이브 코딩은 자유를 주지만, 책임은 사라지지 않는다.
이 글이 어떠셨나요?
관련 포스트
Spec-First 워크플로우
바이브 코딩의 이상적인 루프: 명세 먼저, 테스트 다음, 코드는 마지막. 실전 워크플로우와 CLAUDE.md 활용법을 다룬다.
2026. 02. 04. 오후 10:00Development바이브 코딩 프로젝트의 6개월 후
바이브 코딩으로 만든 프로젝트의 지속 가능성. 기술 부채 청산, 유지보수, 그리고 진짜 "완성"이란 무엇인가.
2026. 02. 15. 오후 10:00Development1인 개발자의 바이브 코딩 시스템
혼자서 팀처럼 개발하는 법. CLAUDE.md, CI, 셀프 코드리뷰를 활용한 규칙 기반 솔로 개발 시스템을 구축한다.
2026. 02. 11. 오후 10:00뉴스레터 구독
새 글이 올라오면 이메일로 알려드려요.