Community라이팅 & 에디팅github.com

meganemura/nukadoko - Gherkin 타입 계약

Gherkin 시나리오가 실행되기 전에 오류를 알 수 있도록 타입 계약을 적용합니다. 어휘는 에이전트가 읽을 수 있으며, 모든 실행은 에이전트가 보고한 것이 아니라 도구가 측정한 영수증을 남깁니다.

nukadoko - Gherkin 타입 계약란 무엇인가요?

nukadoko는 Gherkin 단계에 타입 계약을 도입하여 시나리오가 실행되기 전에 오류를 감지할 수 있게 해주는 AI 에이전트 스킬입니다. 기존 cucumber-js 스위트나 이미 타입화된 단계 DSL에서 점진적으로 마이그레이션할 수 있도록 설계되었습니다. Stage 1에서는 import 경로만 @cucumber/cucumber에서 nukadoko/compat으로 변경하여 기존 스위트가 그대로 실행되도록 합니다. Stage 2에서는 각 단계에 대해 Zod 스키마를 사용한 args/returns 검증, from 의존성 선언, mutates 명시 등의 타입 계약을 추가하여 단계의 정확성과 안전성을 높입니다. 또한 모든 실행은 도구가 측정한 영수증을 생성하여 에이전트가 보고한 것과 달리 신뢰할 수 있는 감사 로그를 제공합니다. 이 스킬은 Cursor, Claude Code, Codex 등 다양한 AI 에이전트 플랫폼에서 사용할 수 있습니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add meganemura/nukadoko

Installed? Explore more 라이팅 & 에디팅 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

nukadoko로 마이그레이션하기

이 가이드의 목적

기존 스위트를 한 번에 다시 작성하지 않고 조금씩 nukadoko로 옮기는 방법입니다. 시작 지점에 따라 두 가지 경우로 나뉩니다:

  • cucumber-js 스위트 — 피처 파일과 글루 코드가 있으며, 보통 Playwright를 구동합니다.
  • 이미 타입화된 단계(typed steps) 형태의 스위트 — cucumber가 아닌 자체 DSL을 사용하지만, nukadoko의 defineStep과 충분히 유사해서 대부분 번역만 하면 됩니다.

어느 쪽이든 마이그레이션은 단계별로 진행되며, 한 번에 모든 것을 바꾸지 않습니다. 이 스킬 문서는 그 이유를 설명하고, 각 시작 지점을 차례로 다룹니다.

아래 내용은 프로젝트가 이미 초기화되었다고 가정합니다. 아직 초기화되지 않았다면(nukadoko.config.ts가 없다면), 먼저 nuka init을 실행하세요.

두 단계, 절대 동시에 하지 않기

한 번에 한 가지만 바꾸고, 절대 동시에 바꾸지 마세요. 두 가지 변경이 함께 적용된 후 단계가 깨지면 어떤 변경이 원인인지 알 수 없습니다. 원인이 하나의 실패에 뒤섞이기 때문입니다. 작업을 단계로 나누면 모든 실패를 정확히 하나의 변경으로 추적할 수 있습니다. 이것이 핵심입니다 — 단계 이름 자체는 부차적입니다.

아래 단계가 현재 상황에 맞지 않는다면, 이 원칙에서 직접 두 단계를 도출하세요. 중요한 것은 추적 가능성을 유지하는 것이지, 특정 단계 이름이 아닙니다.

두 가지 적용 예시가 이어집니다: cucumber-js 스위트는 "import 변경"과 "타입 추가"로 나뉘며(아래 참조), 이미 타입화된 단계 형태의 스위트는 다른 방식으로 나뉩니다(더 아래에서 설명).

cucumber-js 스위트에서 전환하기

이 경우가 위에서 언급한 두 단계의 이름이 붙은 사례입니다.

Stage 1 — 실행 가능하게 만들기

각 글루 파일이 사용하는 import 하나만 변경합니다:

// before: import { Given, When, Then } from "@cucumber/cucumber";
import { Given, When, Then } from "nukadoko/compat";

확장자는 스위트의 과거가 아니라 discovery가 읽는 방식을 결정합니다: .ts, .mts, .js, .mjs 글루는 모두 같은 방식으로 읽히므로, 항상 일반 JavaScript였던 스위트를 이 단계를 시작하기 위해 TypeScript로 다시 작성할 필요가 없습니다. .cjs 파일은 유일한 예외입니다: nukadoko는 ESM 전용이므로 discovery가 절대 import하지 않습니다. nuka check가 대신 이름을 알려줍니다(step-file-unsupported-extension). .js로 이름을 바꾸는 것은 파일 자체 코드가 이미 ES 모듈 구문인 경우에만 도움이 됩니다. 여전히 require(...)를 호출한다면, 같은 ESM 전용 이유로 import에서 실패하며, nuka check가 step-file-import-failed로 알려줍니다.

그런 다음 nuka check <feature>와 nuka run <feature>를 실행하고 출력을 읽으세요. 지적하는 것을 수정하고 다시 실행하세요. 기존 스위트가 정상(green)이 될 때까지 반복합니다 — 이것이 Stage 1의 완료 조건이며, 그 이상도 아닙니다. 조건은 nuka check와 nuka run이 정상이 되는 것이지, tsc 타입 검사가 깨끗해지는 것이 아닙니다. 둘은 어느 방향으로든 불일치할 수 있습니다.

nuka steps와 nuka describe는 Stage 1 내내 사용할 수 있습니다. 일부 글루 파일이 여전히 import에 실패하더라도: 두 명령 모두 단계 파일을 하나씩 읽기 때문에, import에 실패한 파일은 (--json에서는 import_failures, 그 외에는 stderr로) 이름이 지정되며, 전체 어휘가 비워지지 않습니다. 모든 파일이 깨끗하게 import될 때까지 기다리지 않고 진행하면서 어휘를 읽으세요. nuka run <feature>와 nuka do <step>는 읽을 수 없는 글루 파일에 대해 의도적으로 완전히 거부합니다. 실행을 해야 하기 때문입니다.

시작하기 전에 compat가 지원하지 않는 것들의 목록을 찾지 마세요. 작동하지 않는 것은 import나 첫 번째 nuka run에서 크게 실패합니다 — 실패가 무엇이 깨졌는지 알려주며, 그것이 조치할 사항입니다. 여기에 적힌 목록은 compat의 적용 범위가 확장되는 순간 구식이 됩니다. CLI 자체 출력은 절대 그렇지 않습니다.

Stage 2 — 계약 추가하기

스위트가 실행되면, 일부 단계에 타입을 추가할 가치가 있습니다. 다음 표는 compat 단계와 타입 단계의 차이를 보여줍니다:

compat 단계타입 단계
입력패턴 캡처만, 검사 없음args 스키마로 검증, 각 필드에 .describe() 포함
출력폐기 — 영수증의 result는 nullreturns 스키마로 검증, 영수증에 저장
의존성World에 대한 부작용, 함수 시그니처에 보이지 않음from으로 선언, import에 이름 지정, nuka check가 실행 전 검증, 영수증에 used로 기록
키워드장식적 — Then에 바인딩된 단계도 변경 가능mutates는 nukadoko가 신뢰하는 선언: mutates: true를 선언하면 읽기 전용 환경에서 실행을 거부하고, nuka check는 Then에 바인딩된 경우 경고. 실제 실행된 내용은 영수증의 observed 카운트에 기록됨
단독 실행불가능 (시나리오 밖에서는 World가 비어 있음)nuka do <step>으로 직접 실행 가능, 영수증은 stdout으로 출력됨. from 키는 다른 키와 마찬가지로 --args에서 오거나, 이전 실행 결과를 사용하려면 --use <receipt-id>에서 옴. run 내부의 resultOf fixture 호출은 시나리오가 없어 체인이 없으므로 여전히 아무것도 찾지 못함

마지막 행은 위의 "의존성" 행과 별개의 사실이며, 그 결과가 아닙니다: compat 단계는 필요한 것이 아직 채워지지 않은 World에 있고, World가 시그니처의 일부가 아니기 때문에 단독 실행이 불가능합니다 — 먼저 무엇을 설정해야 하는지 검사할 것이 없습니다. 타입 단계의 의존성은 from 항목으로 이름이 지정되고 import에서 볼 수 있으므로, nuka do 호출이 수동으로 제공할 수 있습니다: 일반 키는 --args, 이전 실행 결과에서 가져오려면 --use <receipt-id> — 업스트림 단계의 이름이 명령줄에 나타날 필요가 없습니다. 인용된 영수증이 이미 그 정보를 가지고 있기 때문입니다. 모든 키가 그런 방식으로 도착하는 단계는 --args가 전혀 필요하지 않습니다: --use만으로 완전한 호출입니다. 시나리오 밖에서 여전히 아무것도 찾지 못하는 것은 run 내부의 resultOf fixture로 읽는 의존성입니다: --use를 사용하든 말든, 시나리오가 없으면 걸을 체인이 없습니다.

가장 자주 실행하는 단계부터 한 번에 하나씩 승격하세요 — 전체 스위트를 한 번에 처리하지 마세요. 특정 글루 조각을 어떻게 다시 작성할지는 그 순간에 판단해야 할 문제입니다. nukadoko는 하나의 레시피를 규정하지 않으며, 이 스킬도 그렇게 하지 않습니다.

타입 단계의 run을 작성할 때 가끔 완전히 구조 분해하기 전에 마이그레이션되지 않은 run(ctx, args) 형태를 사용할 수 있습니다. nuka steps --json은 해당 초안이 어떤 fixture를 사용하는지 needs_inferred로 이미 알려줍니다: ctx의 멤버 접근에서 어휘적으로 추측하며, needs: null과 needs_error(실제 계약을 아직 읽을 수 없는 이유 설명)와 함께 제공됩니다. 구조 분해된 시그니처에 무엇을 추가해야 하는지 알기 위해 전체 본문을 수동으로 다시 읽는 수고를 덜지만, 추측일 뿐 계약이 아닙니다: 별칭(const c = ctx)을 놓치고 needs_browser를 절대 이름으로 지정하지 않으므로, 완료된 것으로 간주하지 말고 구조 분해를 마무리하세요.

승격된 단계의 예시

한 가지 예입니다. 이전, compat 글루:

const { Given } = require("@cucumber/cucumber");

Given("a project {string} exists", async function (name) {
  const res = await this.request.post("/projects", { data: { name } });
  this.projectId = (await res.json()).id;
});

이후, 타입 단계 (features/steps/create-project.ts):

import { defineStep } from "nukadoko";
import { z } from "zod";

export default defineStep({
  pattern: "a project {name:string} exists",
  description: "Create a project and return its id",
  args: z.object({
    name: z.string().describe("the project's display name"),
  }),
  returns: z.object({
    id: z.string().describe("the created project's id, for later steps to cite"),
  }),
  mutates: true,
  async run({ request }, args) {
    const res = await request.post("/projects", { data: args });
    return res.json();
  },
});

변경된 점:

  • this.projectId는 사라지고, id는 returns를 통해 반환됩니다. 이후 단계는 from: { projectId: [createProjectStep, "id"] }를 선언하여 키로 읽습니다 — nuka check는 실행 전에 바인딩 순서를 검증하며, 읽기는 해당 이후 단계의 영수증에 나타납니다(resultOf fixture는 키 이름으로 표현할 수 없는 드문 읽기에도 계속 사용 가능).
  • 인수는 스키마 키에 바인딩된 명명된 캡처({name:string} → args.name)이므로, 패턴만으로 어떤 텍스트가 어떤 필드가 되는지 보여줍니다.
  • mutates: true는 이제 코드가 우연히 하는 사실이 아니라 선언입니다 — nuka check는 이 단계가 Then에 바인딩되면 경고하고, 읽기 전용 환경은 실행을 완전히 거부합니다. 영수증은 여전히 실제로 보낸 내용을 검토용으로 기록하지만, 그 카운트가 단계를 거부하지는 않습니다.
  • this.request는 request fixture가 되며, 첫 번째 인수를 구조 분해하여 이름을 지정합니다: 실행기가 실제로 주입하는 것만 이름을 지정할 수 있으며, 암시적인 것은 없고, 실제로 이름이 지정된 것만 빌드됩니다. page나 context를 이름으로 지정하지 않는 단계는 브라우저를 시작하지 않습니다.

이것이 여기서 유일하게 작업된 예시입니다. compat와 타입 간의 모든 차이를 나열한 카탈로그가 아닙니다 — 다른 패턴이 자주 나타나서 예시가 필요하다면, 이 단일 사례에서 즉석으로 만들지 말고 별도로 추가해야 합니다.

전역 After 훅이 정리(cleanup)를 담당하던 경우

cucumber-js 스위트는 종종 전역 After 훅에 의존하여 단계가 생성한 테넌트, 시드된 행, 임시 파일 등을 정리합니다. 이는 해당 단계가 승격된 후 재검토할 가치가 있습니다: 리소스를 nukadoko.config.ts의 자체 fixtures 아래 fixture로 선언하고, 정리 코드를 훅에 남겨두지 말고 리소스를 빌드한 동일한 함수에서 await use(...) 바로 뒤에 작성하세요. 훅은 태그만으로 어떤 시나리오가 실제로 정리가 필요한지 추측해야 합니다. defineFixtures(nukadoko 패키지에서 제공)는 fixture를 완전히 타입화된 상태로 유지합니다. 모든 타입 단계는 page나 request와 같은 방식으로 이름을 구조 분해하여 접근할 수 있습니다. 이는 추가 사항이며 필수가 아닙니다: 아직 어떤 타입 단계도 소유하지 않은 것을 정리하는 After 훅은 여전히 Stage 1에서와 똑같이 유효합니다.

타입화된 단계 형태의 DSL에서 전환하기

피처 파일이나 cucumber 글루가 없다면, 위의 Stage 1에 있는 compat 문은 관련이 없습니다 — 바로 건너뛰세요.

이것이 가능한 이유는 defineStep에서 pattern이 선택 사항이기 때문입니다: 패턴 없이도 단계를 정의할 수 있으며, 여전히 CLI 전용 어휘의 완전한 조각으로 nuka do로 실행하고 nuka describe로 검사할 수 있습니다. 이는 위의 동일한 원칙에서 생성된 다른 두 단계입니다: 먼저 각 단계를 타입화된 defineStep으로 옮기고, 나중에 피처 파일이 필요하게 되면 Gherkin pattern으로 묶습니다.

소스 DSL이 이미 description, args, returns, mutates, run 함수와 같은 것을 가지고 있다면, defineStep으로의 번역은 직접적입니다 — 각각에 해당하는 defineStep 대응 항목이 있습니다.

하지 말아야 할 것

  • Stage 1과 Stage 2를 동시에 하지 마세요. import 전환과 타입 추가를 같은 변경에 섞는 것은 정확히 실패 원인을 알 수 없게 만듭니다 — 위의 "두 단계, 절대 동시에"를 참조하세요.
  • 마이그레이션 중간에 스위트가 실행되지 않게 하지 마세요. compat 단계와 타입 단계는 같은 피처 파일에 공존할 수 있습니다. 일부 단계가 승격되고 다른 단계가 아직 compat인 스위트는 계속 통과해야 합니다. "완전히 타입화됨"을 "실행 가능"의 전제 조건으로 만들지 마세요.
  • 타입화된 대체 단계가 작성되고 통과하기 전에 compat 글루를 삭제하지 마세요. 새 단계를 작성하고 nuka do로 실행한 후에만 이전 단계를 제거하세요 — 절대 반대 순서로 하지 마세요.
  • nuka check나 nuka run이 요청하지 않은 수정을 추측하지 마세요. 그들의 출력이 무엇이 잘못되었는지에 대한 증거입니다. 그들이 지적하지 않은 것을 변경하는 것은 직감에 의한 변경일 뿐, 실제로 깨진 것에 기반한 변경이 아닙니다.

관련 스킬