View
Matt Pocock이 본인 .claude 디렉토리를 통째로 GitHub에 공개했다. 그 안의 TDD 스킬 하나가 며칠 동안 개발자 트위터에서 회자됐는데, 좋은 테스트는 Mock이 아니라 실제 연결이라고 못 박은 것이 핵심이었다. 본인 X 계정에서는 "Before: dozens of shit tests, coupled to implementation. After: only the tests required, validating real behavior"라고 결과를 요약했다. 한마디로 "내가 통제하는 코드는 mock 하지 마라"이다.
또 하나의 흔한 TDD 글로 넘기기 쉽지만, AI 코딩 시대에는 얘기가 좀 다르다. Claude Code 같은 LLM이 한 번에 테스트 10개를 갈겨쓰는 환경에서 mock 남용이 만드는 가짜 신뢰는 예전보다 훨씬 위험하다. 이 글에서는 Pocock 스킬이 진짜로 무슨 말을 하는지, mock이 LLM 시대에 왜 더 큰 함정이 되는지, 그리고 한국 개발자가 오늘 당장 적용할 만한 부분까지 다뤄본다.
Matt Pocock이 누구이기에 이것이 화제인가
Matt Pocock은 TypeScript 강의로 유명한 영국 개발자다. Total TypeScript라는 강의 시리즈로 이름값을 쌓았고, X(Twitter) 팔로워 10만+를 끌어모았다. ts 트릭이나 타입 레벨 프로그래밍 영상을 한 번이라도 본 사람이라면 얼굴이 익을 것이다. 단순 인플루언서가 아니라 실제로 OSS에 기여하고 강의도 깊이 있게 만드는 쪽이다.
2026년 2월, Pocock이 본인이 Claude Code를 쓸 때 사용하는 .claude 디렉토리를 통째로 GitHub에 올렸다. 저장소 이름은 mattpocock/skills. 그 안에는 engineering, productivity, personal, misc 카테고리의 스킬이 들어 있는데, 그중 engineering/tdd 폴더가 특히 화제였다. 스킬이라는 것은 결국 Claude Code에게 "이런 식으로 일해라"라고 가르치는 마크다운 파일 묶음인데, Pocock은 이것을 TDD에 한정해 굉장히 빡세게 짰다. tests.md, mocking.md, interface-design.md 같은 파일들이 각자 한 가지 주제만 깊게 다루는 구조다.
다른 스킬은 각 분야 사람들만 보겠지만, TDD는 모든 개발자에게 해당된다. Pocock이 평소에는 TypeScript 타입 마법사 이미지였는데 갑자기 "테스트를 어떻게 짤 것인가"에 대해 말하기 시작하니, 사람들이 "어, 이 사람도 이런 의견이 있었구나" 하면서 들여다본 것이다. 막상 열어 보니 의견이 굉장히 강했다.
Matt Pocock TDD 스킬이 말하는 핵심 한 줄

출처: GitHub (1.1MB)
스킬 안의 SKILL.md Philosophy 섹션 첫 줄이 "Tests should verify behavior through public interfaces, not implementation details"이다. 한국어로 풀면 "테스트는 사용자나 호출자가 실제로 보는 결과를 검증해야지, 내부에서 함수가 어떻게 호출됐는지 같은 구현 디테일을 검증하면 안 된다"이다. 별 것 아닌 것 같지만 실제 코드에 적용하면 결과가 꽤 다르다. 내가 짠 모듈 안에서 다른 함수가 몇 번 호출됐는지 검증하는 테스트, 내부 메서드 시그니처가 바뀌면 깨지는 테스트, 이런 것이 다 "구현 결합" 테스트다.
mocking.md 파일에는 더 강한 룰이 박혀 있다. "Don't mock: Your own classes/modules, Internal collaborators, Anything you control"이다. 풀어 쓰면 자기가 만든 클래스나 모듈, 그리고 내부 collaborator처럼 자기 통제 안에 있는 모든 것에는 mock을 쓰지 말라는 것이다. mock의 적정 자리는 "내가 통제하지 못하는 것", 즉 시스템 경계뿐이다.
Pocock이 직접 정리한 Before/After
Pocock 본인이 X에서 결과를 이렇게 요약했다.
"Before: dozens of shit tests, coupled to implementation. After: only the tests required, validating real behavior."
번역하면 "전에는 구현에 결합된 쓰레기 테스트가 수십 개. 지금은 진짜 동작을 검증하는 필요한 테스트만 남았다"이다. 테스트 갯수가 줄었는데 신뢰도는 올라간 것이다. 양보다 질이라는 흔한 말 같지만, 코드베이스에 박힌 테스트의 절반이 사라지는 것을 보면 좀 다른 얘기처럼 느껴진다.
그래서 Mock이 왜 나쁘다는 것인가

구현에 결합되는 순간 리팩토링이 불가능해진다
Pocock이 SKILL.md에서 한 말이 직설적이다. "If you rename an internal function and tests fail, those tests were testing implementation, not behavior." 내부 함수 이름만 바꿨는데 테스트가 깨지면, 그 테스트는 동작이 아니라 구현을 검증하던 것이다.
이것이 회사에서 어떻게 나타나는가? 누가 좀 큰 리팩토링을 하려고 보면 테스트 30개가 빨갛게 변한다. 동작은 안 바뀌었는데도 그렇다. 테스트 코드를 같이 30개 고쳐야 하고, 그러다 보면 리팩토링 자체를 포기하게 된다. 코드는 점점 썩어 간다. mock 떡칠한 코드베이스의 전형적인 운명이다.
Mock이 통과해도 진짜 코드 경로는 한 번도 돌지 않은 가짜 신뢰
이것이 더 무서운 부분이다. mock으로 의존성을 다 갈아끼우면 테스트는 초록색이 뜨지만, 진짜 코드 경로는 한 번도 실행되지 않았을 수 있다. 결제 서비스 내부에 if문 분기가 있는데, mock이 그냥 "결제 성공" 객체만 리턴하면 그 분기는 타지 않고 통과돼 버리는 식이다. 프로덕션에서 터지고 나서 "근데 테스트는 통과했는데요?"라고 말하는 상황이 이래서 생긴다. 카카오페이 기술블로그에서도 Mock Test 잘 쓰는 법을 다루면서 비슷한 문제를 지적했다. mock이 거짓 안전망이 되는 것이다.
LLM이 테스트 10개를 갈겨쓸 때 가장 큰 피해가 발생한다
여기가 핵심 포인트다. AI 코딩 도구는 한 번에 테스트 여러 개를 한꺼번에 생성한다. 프롬프트 한 줄에 "테스트 추가해줘"라고 하면 10개짜리 테스트 파일이 한 방에 나온다. 그런데 이것을 통과시키는 가장 쉬운 방법이 mock이다. LLM 입장에서는 mock으로 의존성을 다 갈아끼우면 테스트 짜기가 훨씬 쉽다. 진짜 동작을 분석할 필요도 없고, 그냥 "이 함수가 호출됐는지" 검증하면 된다. LLM이 만든 테스트는 거의 다 mock 떡칠인 경우가 많고, 위에서 말한 "구현 결합 + 가짜 신뢰" 안티패턴이 극단적으로 증폭된다.
Pocock이 TDD 스킬을 만든 진짜 이유가 이것인 듯하다. 사람에게 가르치려는 것이 아니라, Claude Code가 코드를 짤 때 이런 함정에 빠지지 않게 하려고 만든 것이다.
Mock을 써도 되는 자리는 어디인가? 시스템 경계

출처: docker.com
Pocock 룰에서 mock이 허용되는 자리는 시스템 경계뿐이다. 시스템 경계가 무엇인가? 내가 통제할 수 없는 영역이다. 외부 결제 API(Stripe, Toss), 이메일 발송(SendGrid), 외부 webhook, 푸시 알림 서비스 같은 것이다. 이런 것은 테스트마다 진짜로 호출하면 돈도 들고 속도도 느리며, 진짜 사람에게 메일이 가서 사고가 난다.
다만 DB는 좀 미묘하다. 옛날에는 DB도 무조건 mock 했지만, 요즘은 Testcontainers 같은 도구로 진짜 DB를 띄워 놓고 테스트하는 것이 표준이 됐다. Pocock도 가급적 진짜 DB를 쓰라고 한다. 트랜잭션, 인덱스, constraint 같은 것은 mock으로는 절대 잡지 못한다. 여기에 한 마디 더 붙이면, 인메모리 SQLite 같은 다른 종류의 DB로 mock하는 것도 위험하다. 프로덕션이 PostgreSQL인데 테스트는 SQLite라면 똑같지 않다. 같은 DB의 도커 컨테이너가 정답이다.
Date.now(), Math.random(), 파일 읽기/쓰기도 시스템 경계다. 이 자리에서는 mock 혹은 stub을 허용한다. 안 그러면 테스트가 시간에 따라 결과가 바뀌는 flaky test가 된다. Pocock의 시스템 경계 정의를 표로 정리하면 이런 모양이다.
| 카테고리 | 예시 | mock 가능? |
| 외부 서비스 | Stripe, SendGrid, Slack API | 가능 |
| 외부 DB(이상적) | Testcontainers PostgreSQL | mock 안 함 |
| 시간/난수 | Date, Math.random | 가능 |
| 파일 시스템 | fs.readFile | 가능 |
| 내가 만든 클래스 | UserService, OrderRepository | **금지** |
| 내부 헬퍼 함수 | calculateTax, formatDate | **금지** |
실제 연결 테스트는 어떤 코드인가
mock으로 떡칠된 나쁜 테스트는 보통 이렇게 생겼다.
test('주문 결제 처리', () => {
const paymentService = { charge: vi.fn() };
const orderService = new OrderService(paymentService);
orderService.placeOrder({ amount: 1000 });
expect(paymentService.charge).toHaveBeenCalledWith(1000);
});
이 테스트가 검증하는 것은 "OrderService가 paymentService.charge를 1000으로 호출했음"이다. 즉, 내부 호출 시퀀스를 검증하는 것이다. orderService를 리팩토링해서 charge 대신 chargeAsync로 바꾸면, 동작이 똑같아도 테스트가 깨진다.
Pocock 식 좋은 테스트는 이렇게 생겼다.
test('주문 결제 처리', async () => {
const orderService = new OrderService(realPaymentGatewayStub);
const result = await orderService.placeOrder({ amount: 1000 });
expect(result.status).toBe('paid');
expect(result.amount).toBe(1000);
});
여기에서는 외부 결제 게이트웨이만 stub으로 두고, OrderService 자체는 진짜 인스턴스다. 검증하는 것도 "결과가 paid로 나왔는가"라는 외부에서 보이는 동작이다. 내부에서 charge를 호출하든 chargeAsync를 호출하든, 결과만 같으면 통과한다. 리팩토링을 해도 잘 깨지지 않는다.
이상하게 들리지만, 통합 스타일 테스트가 단위 테스트보다 중복이 적은 경우가 많다. 단위 테스트는 모듈마다 mock을 깔고 테스트하니까 같은 동작을 여러 곳에서 검증하게 된다. 반면 통합 테스트는 "사용자가 주문하면 결제가 된다" 한 줄로 여러 컴포넌트가 같이 검증된다. 우아한형제들 기술블로그의 프론트엔드 통합 테스트 글도 비슷한 결론에 도달했다.
인터페이스 설계만 잘 해도 mock이 거의 필요 없어진다
Pocock의 interface-design.md에 박힌 룰 중 하나가 "Accept dependencies, don't create them"이다. 의존성을 함수나 클래스 안에서 직접 만들지 말고 인자로 받으라는 것이다. 이것이 곧 의존성 주입(Dependency Injection)이다.
// Bad: 안에서 만들면 테스트할 때 mock 강제됨
class OrderService {
private payment = new StripePaymentGateway();
}
// Good: 받으면 테스트에서 진짜 객체든 stub이든 자유롭게 넣을 수 있음
class OrderService {
constructor(private payment: PaymentGateway) {}
}
DI를 적용하면 시스템 경계만 stub으로 갈아끼우고 나머지는 다 진짜 객체로 통합 테스트가 가능해진다.
또 다른 룰은 "Return results, don't produce side effects"이다. 함수가 안에서 DB에 쓰고 메일 보내고 다 하지 말고, 가능한 한 값만 리턴하고 부수효과는 바깥에서 처리하라는 것이다. 순수 함수는 mock이 필요 없다. 입력 넣고 출력만 검증하면 끝이다. 마지막은 좁은 인터페이스 설계다. "이 모듈이 무슨 기능을 하는가"를 SDK처럼 명확하게 정의해 두면, 테스트할 때 그 인터페이스만 검증하면 된다. 내부 구현은 알 필요가 없다. 이것은 Martin Fowler의 Mocks Aren't Stubs 글에서 말한 Classicist 진영의 입장과 잘 맞는다.
TDD 트레이서 불릿: 한 번에 하나씩 RED, GREEN, REFACTOR

출처: dev.to
Pocock이 강조하는 또 한 가지가 트레이서 불릿(tracer bullet) 방식이다. 테스트 한 개만 빨갛게 만들고, 그 한 개만 통과시키고, 그 다음 한 개로 넘어가는 식이다. 한 번에 10개를 빨갛게 만들면 어디부터 손대야 할지 길을 잃는다. LLM에게 시킬 때도 똑같다.
스킬에 박혀 있는 강력한 룰이 "Never refactor while RED. Get to GREEN first."이다. 빨간 상태에서 리팩토링하면 안 된다. 일단 통과시키고, 통과된 상태에서 코드를 정리하라는 것이다. Kent Beck의 원래 TDD 룰 그대로인데, LLM이 자주 어기는 룰이기도 하다. AI는 자꾸 "이왕 바꾸는 김에 이것도..." 하면서 RED 상태에서 코드를 다 갈아엎으려 한다.
수직 슬라이스(vertical slice)는 한 기능을 UI에서 DB까지 얇게 한 번 관통하는 구현이다. 트레이서 불릿이 그 슬라이스 하나하나다. "주문 생성"이라는 큰 기능을 한 번에 다 짜지 말고, 일단 주문 생성 후 ID 반환만 통과시킨 다음 amount 저장으로 넘어가고, 그 다음에 주문 상태 pending 저장 식으로 한 줄씩 박는 것이다. 각 단계마다 통합 테스트 하나만 추가된다.
한국 개발자가 오늘 당장 적용하는 방법
기존 코드베이스에 mock으로 떡칠된 테스트가 수백 개 있다면, 한 번에 다 갈아엎는 것은 비현실적이다. 새로 짜는 코드부터 Pocock 룰을 적용해 보고, 리팩토링하다 깨지는 mock 테스트는 살리지 말고 통합 테스트로 대체하는 식이 현실적이다. 핵심 비즈니스 로직 위주로 통합 테스트 비율을 늘리고, 외부 의존이 진짜 외부 의존인지 한 번 더 확인해 보면 어느 정도 정리된다.
JS/TS 생태계에서는 Vitest나 Jest를 쓰면 된다. Testcontainers 노드 클라이언트도 잘 돼 있다. 한국어 자료는 카카오페이, 우아한형제들, 토스 기술블로그에 단편적으로 있는데, Pocock 룰의 프레임으로 다시 읽으면 새롭게 보일 것이다.
Claude Code를 쓰는 사람이라면 더 쉽다. mattpocock/skills 저장소를 클론해서 본인 .claude/skills/ 폴더에 복사하면 된다. 그 다음부터 Claude Code가 테스트를 짤 때 자동으로 이 룰을 따른다. Pocock 본인이 정리한 가이드에 셋업 방법이 자세히 나와 있다.
Mock은 도구이지 종교가 아니다
Matt Pocock TDD 스킬의 핵심은 mock을 무조건 쓰지 말라는 것이 아니다. mock을 어디에 쓸 것인가가 중요하다는 것이다. 좋은 테스트는 public 인터페이스로 외부에서 보이는 동작을 검증하고, 내가 통제하는 코드에는 mock을 쓰지 않으며, mock은 시스템 경계인 외부 API나 시간, 난수 영역에서만 허용된다. 거기에 RED 상태에서 리팩토링하지 말고 트레이서 불릿으로 한 줄씩 박으라는 룰, 그리고 인터페이스 설계만 잘 하면(DI, 순수 함수) mock이 거의 필요 없어진다는 룰까지 묶이면 끝이다.
AI 시대에 이것이 더 중요해진 이유는 LLM이 mock으로 가짜 통과를 만들기가 너무 쉽기 때문이다. Claude Code 같은 도구를 쓸수록 사람이 더 빡세게 룰을 박아 둬야 한다.
다만 mock 자체가 악은 아니다. 외부 결제 시스템을 진짜로 호출하는 테스트는 짜면 안 된다. 그러나 자기가 짠 OrderService를 mock하는 테스트는 거의 항상 잘못 짠 것이다. 다음에 본인 PR을 열 때, 새로 추가된 mock을 한 번씩만 들여다보고 자문해 보았다. "이 mock, 진짜 시스템 경계인가? 아니면 그냥 내가 게을러서 갈아끼운 것인가?" 답이 후자라면 그 mock은 빼고 통합 테스트로 다시 짜는 것이 맞다.
'개발 방법론' 카테고리의 다른 글
| 비관적 락 vs 낙관적 락, 언제 어느 것을 써야 하는가 (0) | 2026.04.30 |
|---|---|
| 개발자가 PRD 잘 쓰는 법 정리 (작성하지 않으면 왜 실패하는가) (0) | 2026.04.30 |
| 화이트박스 vs 블랙박스 테스트, 차이는 무엇이며 언제 어떤 것을 사용해야 하는가 (0) | 2026.04.28 |
| Gartner TIME 모델로 레거시 앱을 어떻게 처리할지 결정하는 방법 (유지·투자·이전·폐기) (0) | 2026.04.23 |
