전체 내역

·article·2026.09.10·9 min read·조회수352

"완료했습니다"를 믿지 않기로 했습니다, AI 산출물 품질 게이트 세 겹

에이전트가 완료했다고 말하는 것과 실제로 동작하는 것은 다릅니다. 매번 사람이 확인하지 않아도 되도록 typecheck, Playwright e2e, 스크린샷 QA를 어떻게 걸어 두었는지 설정과 함께 공유합니다.

Claude Code 실전· 4 / 5펼치기
  1. 1. Claude Code를 메인 개발 도구로 쓰면서 바뀐 개발 순서
  2. 2. 같은 말을 세 번 하기 싫어서 정리한 CLAUDE.md 작성법
  3. 3. 에이전트에게 디자인과 브라우저를 보여 주기, MCP로 Figma·Playwright·Chrome 연결하기
  4. 4. "완료했습니다"를 믿지 않기로 했습니다, AI 산출물 품질 게이트 세 겹
  5. 5. 깔아 둔 Claude Code 스킬 정리, 그리고 실제로 얼마나 썼는지 세어 봤습니다

에이전트와 일하면서 제일 자주 겪은 장면이 있습니다. "구현을 완료했습니다!"라는 말을 듣고 실행해 보면 타입 에러가 나 있거나, 화면은 뜨는데 버튼이 안 눌리는 겁니다.

처음엔 좀 화가 났습니다. 그런데 생각해 보니 에이전트가 거짓말을 한 건 아니었습니다. 확인할 수단이 없으니 확인하지 않고 끝냈을 뿐입니다. 그래서 "완료"의 기준을 제 눈이 아니라 기계가 판정하게 바꿨습니다. 이 글에서는 그렇게 걸어 둔 세 겹의 품질 게이트를 소개합니다.

1. 첫 번째 게이트: typecheck

가장 싸고 빠른 게이트입니다. TypeScript strict 모드를 켜 두면 에이전트가 자주 하는 실수, 그러니까 없는 prop을 넘기거나 undefined일 수 있는 값을 그냥 쓰거나 API 응답 타입을 추측해서 쓰는 실수가 대부분 걸립니다.

// tsconfig.json (일부)
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true
  }
}
// package.json
{
  "scripts": {
    "typecheck": "tsc --noEmit -p ."
  }
}

1-1. 훅으로 자동으로 돌리기

Claude Code의 훅(hooks)을 쓰면 파일을 고칠 때마다 typecheck를 자동으로 돌릴 수 있습니다. 에이전트가 "한번 실행해 볼게요"라고 하길 기다리지 않아도 됩니다.

// .claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "pnpm -s typecheck" }]
      }
    ]
  }
}

다만 프로젝트가 크면 파일 하나 고칠 때마다 전체 typecheck를 돌리는 게 꽤 무겁습니다. 그래서 작은 프로젝트에서는 훅으로, 큰 프로젝트에서는 CLAUDE.md의 완료 조건에 적어 두는 식으로 나눠서 씁니다.

2. 두 번째 게이트: Playwright e2e

타입이 맞는다고 동작이 맞는 건 아닙니다. 그래서 주요 흐름은 e2e 테스트로 고정해 둡니다. 화면을 만들게 할 때 테스트를 먼저 쓰게 하면, 그 테스트가 곧 요구사항 명세가 됩니다.

// e2e/tasks.spec.ts
import { test, expect } from "@playwright/test";
 
test("상태 필터를 바꾸면 해당 상태의 업무만 보인다", async ({ page }) => {
  await page.goto("/tasks");
  await page.getByRole("radio", { name: "진행 중" }).click();
 
  const rows = page.getByRole("row").filter({ hasNot: page.getByRole("columnheader") });
  await expect(rows.first()).toBeVisible();
  for (const badge of await rows.getByTestId("status-badge").all()) {
    await expect(badge).toHaveText("진행 중");
  }
});
 
test("업무를 일시정지하면 목록과 상세에 함께 반영된다", async ({ page }) => {
  await page.goto("/tasks");
  await page.getByRole("row", { name: /주간 보고/ }).click();
  await page.getByRole("button", { name: "일시정지" }).click();
  await expect(page.getByRole("status")).toContainText("일시정지했습니다");
  await page.goBack();
  await expect(page.getByRole("row", { name: /주간 보고/ }).getByTestId("status-badge")).toHaveText("일시정지");
});

선택자는 getByRole, getByText 위주로 씁니다. 에이전트가 클래스 이름을 바꿔도 테스트가 안 깨지고, 접근성 이름이 빠진 버튼은 테스트를 쓰는 단계에서 바로 드러납니다. 덤으로 접근성까지 챙겨지는 셈입니다.

2-1. 모션도 테스트할 수 있습니다

진입 모션 순서처럼 "보기에 맞는지"도 어느 정도는 테스트로 고정할 수 있습니다. 요소가 화면에 나타나는 순서를 기록해서 비교하면 됩니다.

test("진입 모션은 상단바, 타이틀, 카드, 테이블 순서다", async ({ page }) => {
  await page.goto("/dashboard");
  const order = await page.evaluate(async () => {
    const ids = ["topbar", "title", "cards", "table"];
    const seen: string[] = [];
    const start = performance.now();
    while (seen.length < ids.length && performance.now() - start < 3000) {
      for (const id of ids) {
        const el = document.querySelector(`[data-motion="${id}"]`);
        if (el && !seen.includes(id) && Number(getComputedStyle(el).opacity) > 0.5) seen.push(id);
      }
      await new Promise(requestAnimationFrame);
    }
    return seen;
  });
  expect(order).toEqual(["topbar", "title", "cards", "table"]);
});
 
test("동작 줄이기 설정에서는 바로 보인다", async ({ browser }) => {
  const page = await browser.newPage({ reducedMotion: "reduce" });
  await page.goto("/dashboard");
  await expect(page.locator('[data-motion="table"]')).toHaveCSS("opacity", "1");
});

3. 세 번째 게이트: 스크린샷 QA

마지막은 눈으로 보는 게이트입니다. 그렇다고 제가 매번 브라우저를 여는 건 아니고, 에이전트에게 정해진 조건으로 스크린샷을 찍어서 보여 달라고 합니다.

## UI 변경 완료 조건 (CLAUDE.md)
- 1440px, 390px 두 폭에서 라이트/다크 스크린샷 4장
- 390px에서 document.documentElement.scrollWidth === innerWidth (가로 스크롤 없음)
- 콘솔 에러 0건

자주 깨지는 화면은 Playwright 시각 비교로 고정해 둡니다.

test("대시보드 시각 회귀", async ({ page }) => {
  await page.goto("/dashboard");
  await page.emulateMedia({ reducedMotion: "reduce" }); // 모션이 끝난 상태로 비교
  await expect(page).toHaveScreenshot("dashboard.png", { maxDiffPixelRatio: 0.01 });
});

4. 게이트를 걸고 나서 달라진 것

"다 된 거 맞아요?"라고 묻는 일이 없어졌습니다. 게이트 결과를 보면 되니까요.

그리고 에이전트가 스스로 고치기 시작했습니다. typecheck 결과가 대화에 들어오면, 제가 지적하기도 전에 에러를 읽고 고칩니다. 확인 수단을 주니까 확인을 하더라고요.

게이트에서 반복해서 걸리는 패턴은 CLAUDE.md에 규칙으로 추가합니다. 문서와 코드를 같이 고치는 습관이 여기서 생겼습니다.

마치며

사실 이 게이트들은 AI 때문에 새로 생긴 게 아닙니다. 사람 팀원과 일할 때도 똑같이 필요한 것들입니다. 저는 그동안 "나중에 하지" 하고 미뤄 왔는데, AI와 일하면서 비로소 제대로 갖추게 됐습니다.

아직 게이트가 하나도 없다면 typecheck부터 걸어 보시길 권합니다. 설정 몇 줄이면 되고, 그것만으로도 "완료했습니다"의 무게가 꽤 달라집니다.

Comments (0)