2026. 5. 22. 16:41ㆍ카테고리 없음
AI 코딩 도구를 사용하면 기능 구현 속도는 확실히 빨라집니다. Cursor, Claude Code, GitHub Copilot, Codex 같은 도구에게 요구사항을 설명하면 컴포넌트, API 호출 코드, 유틸 함수, 테스트 코드까지 빠르게 생성해 줍니다.
하지만 실제 프로젝트에 적용해보면 속도만큼이나 불안한 부분도 생깁니다.
- 요구사항을 일부만 이해하고 구현한다.
- 정상 케이스만 처리하고 예외 케이스를 놓친다.
- 기존 코드 컨벤션과 다른 방식으로 작성한다.
- 동작하는 것처럼 보이지만 실제로는 회귀 버그를 만든다.
- 수정 요청을 했더니 관련 없는 파일까지 바꾼다.
그래서 AI 코딩을 실무에 안정적으로 적용하려면 “코드를 먼저 만들고 사람이 확인하는 방식”에서 조금 벗어날 필요가 있습니다. 이때 도움이 되는 접근이 테스트 주도 개발(TDD) 입니다.
이번 글에서는 AI 코딩 에이전트에게 구현을 바로 시키는 대신, 테스트를 먼저 작성하게 하고 그 테스트를 기준으로 구현을 진행하는 흐름을 정리해보겠습니다.
왜 필요한가
AI 에이전트는 문맥을 잘 이해하는 것처럼 보이지만, 실제로는 요구사항을 엄격하게 보장하지 않습니다. 특히 프론트엔드 개발에서는 다음과 같은 문제가 자주 발생합니다.
1. UI는 그럴듯하지만 조건이 빠진다
예를 들어 버튼 컴포넌트에 disabled, loading, variant, size 같은 옵션이 있다고 해보겠습니다. AI는 기본 화면은 잘 만들지만 다음과 같은 조건을 놓칠 수 있습니다.
- loading 상태에서는 클릭 이벤트가 발생하지 않아야 한다.
- disabled 상태에서는 aria 속성이 적절히 들어가야 한다.
- variant가 없을 때 기본값이 적용되어야 한다.
- size 조합에 따라 className이 충돌하지 않아야 한다.
눈으로 보기에는 멀쩡해도 실제 동작 조건은 빠질 수 있습니다.
2. 리팩토링 중 기존 동작이 깨진다
AI에게 “이 컴포넌트 구조를 정리해줘”라고 요청하면 코드는 더 깔끔해질 수 있습니다. 하지만 기존에 의존하던 작은 동작이 사라지는 경우가 있습니다.
예를 들어 기존에는 onChange가 특정 순서로 호출되었는데, 리팩토링 후 호출 타이밍이 바뀌는 식입니다.
테스트가 없다면 이런 문제는 QA나 운영 중에 발견됩니다.
3. 요구사항이 대화 중에 바뀐다
AI와 작업하다 보면 처음 요구사항, 중간 수정 요청, 마지막 예외 조건이 대화 안에 섞입니다. 에이전트는 마지막 요청을 반영하면서 앞에서 말한 조건을 잊을 수 있습니다.
이때 테스트는 요구사항을 코드로 고정하는 역할을 합니다.
핵심 개념
AI와 함께 TDD를 한다고 해서 전통적인 TDD 사이클이 완전히 달라지는 것은 아닙니다. 기본 흐름은 같습니다.
- 실패하는 테스트를 먼저 작성한다.
- 테스트를 통과하는 최소 구현을 작성한다.
- 리팩토링한다.
- 테스트를 다시 실행한다.
다만 AI 에이전트를 사용할 때는 각 단계에서 사람이 해야 할 역할과 AI에게 맡길 역할을 구분하는 것이 중요합니다.
실무 적용 방법
1. 구현 요청 전에 테스트 조건부터 정리하기
AI에게 바로 이렇게 요청하기 쉽습니다.
로그인 폼 컴포넌트 만들어줘.
하지만 이 요청은 너무 넓습니다. 대신 테스트 조건을 먼저 정리하는 방식이 좋습니다.
로그인 폼 컴포넌트를 만들기 전에 테스트 케이스를 먼저 작성해줘.
다음 조건을 포함해줘.
- 이메일과 비밀번호 입력 필드가 렌더링된다.
- 이메일이 비어 있으면 제출할 수 없다.
- 비밀번호가 8자 미만이면 에러 메시지를 보여준다.
- 유효한 값이면 onSubmit이 이메일과 비밀번호를 인자로 호출된다.
- 제출 중에는 버튼이 disabled 상태가 된다.
이렇게 하면 AI는 구현보다 먼저 요구사항을 테스트로 표현하게 됩니다.
2. 테스트 파일만 먼저 생성하게 하기
처음부터 구현 파일까지 한 번에 만들게 하면, AI는 테스트와 구현을 동시에 맞춰버리는 경향이 있습니다. 그러면 테스트가 실제 요구사항 검증이라기보다 “자기가 만든 코드에 맞춘 테스트”가 될 수 있습니다.
그래서 첫 요청은 테스트 파일만 만들게 하는 것이 좋습니다.
아직 구현 코드는 수정하지 말고 LoginForm.test.tsx 파일만 작성해줘.
프로젝트의 기존 테스트 스타일을 참고해서 작성해줘.
이후 테스트가 실패하는 것을 확인한 뒤 구현을 요청합니다.
방금 작성한 테스트를 통과하도록 LoginForm.tsx를 구현해줘.
테스트 파일은 수정하지 마.
여기서 중요한 문장은 “테스트 파일은 수정하지 마” 입니다. AI가 테스트를 통과시키기 위해 테스트 자체를 바꿔버리는 일을 막아야 합니다.
3. 테스트 실행 결과를 기준으로 수정시키기
테스트가 실패하면 실패 로그를 그대로 AI에게 전달합니다.
다음 테스트가 실패했어.
테스트 파일은 수정하지 말고 구현 코드만 수정해줘.
Error: expect(onSubmit).toHaveBeenCalledWith(...)
...
이렇게 하면 AI는 실제 실패 원인을 기준으로 코드를 수정합니다. 단순히 “안 돼 고쳐줘”라고 하는 것보다 훨씬 안정적입니다.
4. 예외 케이스를 추가 테스트로 고정하기
기능이 동작하기 시작하면 바로 끝내지 말고 예외 케이스를 추가하는 것이 좋습니다.
예를 들어 로그인 폼이라면 다음 테스트를 추가할 수 있습니다.
- 앞뒤 공백이 있는 이메일은 trim 처리되는가?
- 엔터 키로 제출할 수 있는가?
- API 에러가 발생하면 에러 메시지를 표시하는가?
- 제출 중 중복 클릭이 막히는가?
AI에게 이렇게 요청할 수 있습니다.
현재 구현에서 놓칠 수 있는 엣지 케이스를 테스트로 추가해줘.
단, 기존 구현 파일은 수정하지 말고 테스트 파일만 수정해줘.
그 다음 다시 구현을 수정하게 합니다.
예시 코드
아래는 React Testing Library 기준의 간단한 예시입니다.
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { LoginForm } from "./LoginForm"
describe("LoginForm", () => {
it("유효한 이메일과 비밀번호를 입력하면 onSubmit을 호출한다", async () => {
const user = userEvent.setup()
const onSubmit = vi.fn()
render(<LoginForm onSubmit={onSubmit} />)
await user.type(screen.getByLabelText("이메일"), "test@example.com")
await user.type(screen.getByLabelText("비밀번호"), "password123")
await user.click(screen.getByRole("button", { name: "로그인" }))
expect(onSubmit).toHaveBeenCalledWith({
email: "test@example.com",
password: "password123",
})
})
it("비밀번호가 8자 미만이면 에러 메시지를 보여준다", async () => {
const user = userEvent.setup()
const onSubmit = vi.fn()
render(<LoginForm onSubmit={onSubmit} />)
await user.type(screen.getByLabelText("이메일"), "test@example.com")
await user.type(screen.getByLabelText("비밀번호"), "1234")
await user.click(screen.getByRole("button", { name: "로그인" }))
expect(screen.getByText("비밀번호는 8자 이상이어야 합니다.")).toBeInTheDocument()
expect(onSubmit).not.toHaveBeenCalled()
})
})
이 테스트는 구현 세부사항보다 사용자 관점의 동작을 검증합니다. AI에게 테스트를 작성시킬 때도 내부 state 이름이나 className보다 실제 사용자가 보는 입력, 버튼, 메시지 중심으로 작성하게 하는 것이 좋습니다.
AI에게 줄 프롬프트 예시
실무에서는 아래와 같은 프롬프트를 템플릿처럼 사용할 수 있습니다.
너는 이 프로젝트의 프론트엔드 개발자야.
먼저 테스트 주도 개발 방식으로 진행해줘.
규칙:
1. 처음에는 테스트 파일만 작성한다.
2. 구현 파일은 수정하지 않는다.
3. 테스트는 사용자 행동 중심으로 작성한다.
4. 정상 케이스와 예외 케이스를 모두 포함한다.
5. 이후 내가 테스트 실행 결과를 주면, 테스트 파일은 수정하지 말고 구현만 수정한다.
기능 요구사항:
- ...
이 프롬프트의 핵심은 AI의 작업 범위를 제한하는 것입니다. AI에게 “잘 만들어줘”라고 하는 것보다 “지금은 테스트만 작성해줘”, “이후에는 구현만 수정해줘”처럼 단계별로 나누는 것이 결과가 더 좋습니다.
주의할 점
테스트도 리뷰해야 한다
AI가 작성한 테스트라고 해서 항상 올바른 것은 아닙니다. 다음 항목은 사람이 꼭 확인해야 합니다.
- 요구사항이 빠지지 않았는가?
- 테스트 이름이 의도를 잘 설명하는가?
- 구현 세부사항에 너무 의존하지 않는가?
- 의미 없는 스냅샷 테스트가 남발되지 않았는가?
- 비동기 테스트에서 await 처리가 적절한가?
테스트를 통과시키기 위해 테스트를 바꾸지 못하게 해야 한다
AI 에이전트는 실패한 테스트를 통과시키는 과정에서 테스트 코드를 수정하려고 할 수 있습니다. 이때는 명확하게 제한해야 합니다.
테스트 파일은 수정하지 마.
실패한 테스트를 기준으로 구현 파일만 수정해줘.
한 번에 너무 큰 기능을 맡기지 않는다
AI에게 큰 기능을 한 번에 맡기면 테스트도 구현도 커지고 검증이 어려워집니다. 기능을 작게 나누고 각 단위마다 테스트를 먼저 만드는 것이 좋습니다.
정리
AI 코딩 에이전트는 빠르게 코드를 만들어주는 도구이지만, 실무에서 중요한 것은 단순한 속도가 아니라 검증 가능한 결과입니다.
테스트를 먼저 작성하게 하면 AI가 이해한 요구사항을 코드로 확인할 수 있고, 구현 이후에도 회귀 버그를 줄일 수 있습니다.
추천하는 흐름은 다음과 같습니다.
- 요구사항을 테스트 케이스로 정리한다.
- AI에게 테스트 파일만 먼저 작성하게 한다.
- 실패하는 테스트를 확인한다.
- 테스트를 수정하지 못하게 하고 구현만 작성하게 한다.
- 실패 로그를 기준으로 반복 수정한다.
- 엣지 케이스를 추가 테스트로 고정한다.
AI 시대의 개발자는 코드를 직접 덜 작성할 수는 있지만, 무엇이 올바른 동작인지 정의하는 역할은 더 중요해지고 있습니다. 테스트는 그 정의를 가장 명확하게 남기는 방법 중 하나입니다.