
앞의 세 편에서는 경로, 의존성 규칙, 디자인 값을 정했다. 프로젝트별 경로와 실행 명령을 에이전트 설정으로 분리하기 가 경로와 명령을, 에이전트가 따를 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.js import 제한
도메인 레이어에서는 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가 라운드마다 이 검사를 실행한다.