앞의 세 편에서는 구현에 필요한 경로와 명령, 의존성 규칙, 디자인 값을 정했다. 프로젝트별 경로와 실행 명령을 에이전트 설정으로 분리하기 가 경로와 명령을, 에이전트가 따를 DDD 폴더 구조와 의존성 규칙 정하기 가 금지 import 표를, 에이전트에게 UI 구현을 맡기기 전에 정해야 할 디자인 값 가 여백과 색과 빈 목록 문구를 값으로 적어 뒀다. 이제 그 기준에 따라 구현하면 될 것 같았다.
그런데 이 파일이 서버에서 도는지 브라우저에서 도는지는 세 문서 어디에도 없었다. architecture.md는 무엇이 무엇을 import 할 수 있는지만 정하고 디자인 스펙은 무엇을 그릴지만 정한다.
curvez의 nextjs-implementation 스킬에서는 그 판정을 기록하고 구현이 끝나면 개수를 세어 확인한다.
1. 이 화면을 누가 그리는가
App Router에서 그 판정은 한 줄이다. 파일 맨 위의 "use client". 안 붙이면 서버, 붙이면 클라이언트다.
서버 쪽 코드는 브라우저로 내려가지 않는다. 그래서 데이터베이스를 직접 조회하고 비밀 키를 써도 되지만 클릭도 입력도 받지 못한다. 클라이언트 쪽은 둘 다 받는 대신 그 파일이 브라우저로 내려간다.
앞의 세 문서에는 이 한 줄을 어디에 둘지가 빠져 있었다.
2. 구현 완료를 판단하는 기준
스킬 맨 아래의 완료 기준 목록에 이런 항목이 있다.
page.tsx/layout.tsx최상단"use client"0건 (있으면 이유를decisions에)
다 만든 뒤에 개수를 세는 항목이다. 안 세면 생긴다는 뜻이다.
"use client"는 그 파일 하나에만 적용되는 표시로 보기 쉬운데, 거기서 import 하는 모듈 전체가 클라이언트 번들 경계 안으로 들어간다. 페이지 맨 위에 한 줄 붙이면 그 아래 전 트리가 클라이언트가 된다. 서버에서 끝낼 수 있었던 데이터 조회와 직렬화 못 하는 의존까지 번들로 넘어간다.
리뷰에서 잘 안 잡히는 건 화면이 멀쩡히 나와서다. 타입도 테스트도 통과한다. 경계가 어디로 내려갔는지는 파일 맨 위 한 줄에만 적혀 있다. 그 한 줄은 어느 파일에 있어도 문법적으로 맞다.
3. 코드를 쓰기 전에 세 파일을 이 순서로 읽는다
스킬 1단계는 앞의 세 편이 만들어 둔 파일을 정해진 순서로 읽는 것이다. .curvez/profile.json → .curvez/architecture.md → .curvez/design/. 하나라도 못 읽으면 코드를 쓰지 않는다.
profile.json에서는 웹 소스 경로(paths.web)와 검사 명령(commands의 typecheck·lint·test)을 읽고 stack이 monorepo 면 paths.domain도 읽는다. 여기서 제일 세게 적혀 있는 문장이 경로와 명령을 추측하지 않는다. 폴백을 만들지 않는다. 두 줄이다. 하나라도 없으면 없는 키 이름을 그대로 적고 status: blocked로 돌아간다.
monorepo에서 에이전트마다 폴백이 다르면 두 에이전트가 같은 디렉터리를 소유하게 된다. 이 상태로 병렬 실행하면 나중에 쓴 쪽이 앞선 쪽을 지운다. 사라진 변경은 리뷰에서도 안 잡힌다. 명령을 하드코딩하면 없는 명령을 실행하고 실패를 통과로 착각한다.
architecture.md에서는 ## 금지 import 표와 ## 스택 매핑을 코드를 쓰기 전에 읽는다. 앞의 것이 위반 판정의 유일한 근거다. 뒤의 것은 그 표의 어느 칸인지를 정한다. app/ 아래 파일과 서버 액션, route handler가 대상이다. 파일을 어디에 둘지가 거기서 정해진다.
design/은 index.md → 해당 screens/<screen-id>.md · components/<ComponentName>.md → tokens.md 순으로 읽는다. route(nextjs) 값은 App Router 경로로 그대로 쓴다. 색·간격·타이포는 --<category>-<role>-<variant> 형태의 토큰 이름으로만 쓴다.
4. "use client"를 어디에 두는가
새 파일은 서버 컴포넌트로 시작한다. 상태 훅, 라이프사이클·구독, DOM이벤트 핸들러, 브라우저 전용 API, 클라이언트 전용 Context 중 하나가 실제로 필요해진 순간에만 클라이언트로 내린다. 한 줄로 줄이면 “브라우저에서 뭔가 일어나야 하는가” 다. 데이터 조회·조건 분기·마크업 조립만 하는 파일은 서버로 남는다.
내릴 때도 파일 전체를 내리지 않는다. 그 부분만 작은 컴포넌트로 잘라내고 그 파일 맨 위에 "use client"를 둔다. page.tsx와 layout.tsx에는 붙이지 않는다. 클라이언트 컴포넌트에 넘기는 prop은 직렬화 가능한 값만 넘긴다. 서버로 남겨야 하는 트리는 children prop으로 내려보낸다. 부모가 클라이언트여도 children 자리는 서버에서 그려진 채로 온다.
금지 문장이 하나 명시돼 있다. “나중에 인터랙션이 붙을 것 같아서”는 판정 근거가 아니다. 그 가정은 검증되지 않는다. 한번 내려간 경계는 위로 다시 올라오지 않는다.
판정표 자체는 이 스킬에 없다. agents/curvez-nextjs.md의 ## 판단 기준이 정본이다. 스킬은 그 기준을 언제 적용하고 무엇으로 검증하는지만 정한다. 같은 판정표를 두 곳에 두면 규칙을 바꿀 때 한쪽만 고쳐진다. 그러면 에이전트가 매번 다른 쪽을 따르게 된다.
5. 서버 액션인가 route handler 인가
변경(mutation)을 붙일 때는 서버 액션과 route handler 중 하나를 고른다. 둘 다 서버에서 돌지만 부르는 방법이 다르다. 서버 액션은 브라우저 쪽 코드에서 함수처럼 부르면 Next.js가 그 호출을 서버로 넘긴다. route handler는 /api/... 같은 주소를 만들어 두고 HTTP 요청으로 부른다. 주소가 있으니 우리 앱이 아닌 것도 부를 수 있다.
판정표는 에이전트 정의에 있고 기준은 이렇게 나뉜다.
| 상황 | 선택 |
|---|---|
| 폼 제출·그 앱 UI에서만 부르는 변경 | 서버 액션 |
| 변경 뒤 곧바로 재검증이 필요 | 서버 액션 + revalidatePath / revalidateTag |
| 외부 시스템·웹훅·서드파티가 호출 | route handler |
| 비 HTML 응답 (파일 다운로드, 스트림, 이미지, RSS) | route handler |
| GET 성격의 단순 조회 | 둘 다 아님. 서버 컴포넌트에서 직접 조회 |
표 1. 변경을 서버 액션과 route handler 중 어디에 둘지의 기준
마지막 줄은 자기 자신에게 HTTP를 한 번 더 왕복시키지 않는다는 뜻이다. 서버 액션 쪽에는 조건이 하나 붙는다. 입력을 항상 서버에서 다시 검증한다. 코드에서는 함수처럼 생겼어도 실제로는 주소가 하나 생긴다. 브라우저 화면을 거치지 않고 그 주소로 아무 값이나 보낼 수 있다. 클라이언트에서 이미 검사했다는 것은 근거가 되지 않는다.
데이터 페칭은 그 데이터를 실제로 쓰는 서버 컴포넌트에서 한다. 상위에서 받아 prop으로 길게 내리지 않는다. 두 곳에서 같은 데이터가 필요하면 각자 호출한다. 한 요청 안의 같은 조회는 한 번만 실행되고 결과를 나눠 쓴다.
캐시도 따로 판단해야 한다. 한 번 저장한 데이터가 다음 요청에 그대로 나가기 때문이다. 사용자별·요청별로 달라지는 데이터는 캐시하지 않는다. 모두에게 같고 자주 안 바뀌면 태그를 붙여 캐시한다. 그리고 변경 액션에서 그 태그를 재검증한다.
못 정하겠으면 캐시하지 않는 쪽을 고른다. 안 하면 느려질 뿐이고 나중에 붙일 수 있지만 잘못 캐시하면 사용자에게 다른 사람의 데이터가 보인다. 표로도 판정이 안 될 때는 멈추지 않는다. 서버 쪽·캐시 안 하는 쪽·타입 좁은 쪽을 고르고 decisions에 reversible_at과 함께 남긴다.
6. 도메인이 next/headers를 부르면
도메인 레이어에서는 next/*를 참조하지 않는다. next/navigation, next/headers, next/cache, next/image, next/server 전부 포함이다. 쿠키·헤더·현재 경로는 상위 레이어에서 읽어 인자로 주입한다.
도메인이 next/headers를 부르는 순간 그 코드는 요청이 들어와 있어야만 돈다. 테스트도 재사용도 못 하게 된다.
타입에도 같은 규칙이 붙는다. any와 타입 단언(as) 없이 정의한다. 폼 데이터·API 응답·검색 파라미터 같은 외부 입력은 unknown으로 받는다. 런타임 스키마 검증을 통과시킨 뒤 타입을 얻는다. typecheck 수치로 완료를 판정하는데 any 하나만 있어도 그 수치가 무의미해진다.
7. 위반 0건과 검사 실패를 어떻게 구별하나
화면(route) 하나 또는 컴포넌트 3~5개를 만들 때마다 바로 검증을 돌린다. 20개 파일을 쓰고 나서 typecheck를 처음 돌리면 오류가 서로 얽혀 어느 결정이 원인인지 가리기 어렵다.
무엇을 세는지가 정해져 있다. typecheck·lint·test의 exit 코드와 오류 수, ARCH-NNN 규칙별 위반 건수, any와 타입 단언 개수, page.tsx/layout.tsx 최상단 "use client" 개수, 디자인 스펙의 상태 키 중 미구현 건수. “통과”가 아니라 0 errors, 0 warnings 형태로 출력 수치를 그대로 옮긴다.
여기 함정이 하나 있고 스킬이 그걸 따로 절로 떼어 뒀다. 위반은 grep -E로 찾는데, |가 이 도구의 패턴에서는 “둘 중 아무거나”이고 ## 금지 import 표 안에서는 칸을 나누는 기호라 마크다운 규칙상 \|로 이스케이프돼 있다. 그래서 awk -F' \| '(공백-파이프-공백)로 필드를 끊고 읽어낸 패턴의 \|를 |로 되돌린 다음에야 grep -E가 원래 뜻으로 동작한다. 안 되돌리면 grep이 그걸 역슬래시라는 글자로 읽어 패턴이 전부 어긋나고 위반 0건으로 잘못 나온다.
그래서 검사 전에 규칙 개수를 먼저 찍는다.
grep -cE '^\| ARCH-[0-9]{3} \|' .curvez/architecture.md
검사를 돌려 위반이 0으로 나왔다고 해서 깨끗한 것은 아니다. 이 값이 0이면 표를 못 읽은 것이다. 파싱 실패와 위반 없음은 화면에 둘 다 0으로 보이는데, 앞의 것은 검사를 아예 안 한 상태다. 이 착각은 실행 테스트로 두 번 잡혔다고 적혀 있다. 그래서 파싱 블록은 반드시 references/verification-commands.md의 것을 쓰라고 못 박아 뒀다.
하나라도 실패하면 status: done을 쓰지 않는다. partial로 낮추고 실패한 명령과 출력을 원문 그대로 남긴다.
8. 구현을 멈추고 확인해야 하는 조건
스킬 6단계는 멈추는 조건을 표로 적어 뒀다. 디자인 스펙에 상태 키가 없으면 curvez-designer에게 넘긴다. 이때 state:empty 같은 리터럴 키 이름을 그대로 적는다. 아키텍처 규칙이 구현을 막으면 코드를 쓰기 전에 curvez-architect에게 넘긴다. API 동작이나 버전 제약을 모르면 검색하지 않고 curvez-researcher에게 넘긴다.
여기서 blocked는 실패가 아니라고 명시돼 있다. 앞 문서에 값이 없어서 더 못 간다고 적어 두고 멈추는 상태다. 추측으로 메운 done은 아무도 잡아내지 못한다. 그 뒤 QA와 리뷰어 전부가 잘못된 전제 위에서 돈다.
9. 도입 비용과 한계
| 감수하는 것 | 어떻게 나타나나 |
|---|---|
| 검증 횟수가 늘어난다 | 화면 하나·컴포넌트 3~5개마다 typecheck·lint·test를 다시 돌린다. 한 번에 몰아 돌리는 것보다 총 실행 횟수가 많다 |
| 판정표가 다른 파일에 있다 | 정본은 agents/curvez-nextjs.md 라서, 스킬만 읽어서는 무엇을 서버로 둘지 판정할 수 없다 |
| 앞 단계가 비면 그냥 멈춘다 | paths.web이나 state:empty 하나가 없으면 코드를 쓰지 않고 blocked로 돌아간다. 앞 문서가 부실하면 구현이 시작조차 안 된다 |
| 검사가 문자열 대조다 | ARCH 표와 상태 키를 문자열로 찾는다. 표 형식이 어긋나면 검사 자체가 성립하지 않고, 그걸 규칙 개수로만 겨우 구별한다 |
표 2. 이 규약이 감수하는 것
확인하지 못한 것
references/verification-commands.md의 파싱 블록을 실제architecture.md에 대고 돌려 보지는 못했다. 이스케이프 함정 설명은 문서에 적힌 것을 옮긴 것이다.- 미들웨어를 이 규약에서 어느 레이어로 보는지는 확인하지 못했다.
SKILL.md,references/verification-commands.md, 에이전트 정의 세 파일 어디에도middleware라는 낱말이 없다. - 검증 5개 중 상태 키 대조만 사람 눈에 기댄다. 이걸 자동화하는 방법은 문서에 없다.
이제 경계를 내린 이유는 decisions에 reversible_at과 함께 남는다. 안 내렸어야 할 자리는 개수로 남는다. 다음은 이 개수 세기를 quality-gate가 라운드마다 돌리는 자리다.

답글 남기기