새 프로젝트를 시작할 때마다 Claude에게 내 기술 스택을 다시 설명해야 해서, 직접 만들었습니다
요약
AI 에이전트가 프로젝트 시작 시 반복적인 질문을 던지거나 학습 데이터의 기본값으로 코드를 작성하는 문제를 해결하기 위해 'railhead'를 개발했습니다. 이 도구는 코드베이스의 제약 조건을 통해 에이전트의 환각을 방지하고 과잉 엔지니어링을 막는 스캐폴딩 역할을 합니다.
핵심 포인트
- AI 에이전트의 반복적인 질문과 기술 스택 재설명 문제 해결
- 학습 데이터 기반의 뻔한 디자인과 코드 생성을 방지하는 스캐폴딩 제공
- API 타입 강제 및 단일 클라이언트 사용을 통한 환각(Hallucination) 최소화
- 불필요한 라이브러리나 추상화를 방지하는 과잉 엔지니어링 금지 규칙 적용
fe-rail은 워크플로우 문제를 해결했습니다. 명세(Spec), 빌드(Build), 리뷰(Review), PR. 에이전트(Agent)는 단계를 건너뛰지 않았고, .env 파일을 커밋하지 않았으며, 매 세션마다 제가 똑같은 세 가지 실수를 잡아내야 할 필요도 없어졌습니다. 그 부분에 대해서는 이미 글을 쓴 적이 있습니다.
제가 해결하지 못한 것은 모든 새 프로젝트의 첫 한 시간이었습니다. 빈 저장소(Repo)를 클론(Clone)하고, Claude Code를 열면, 에이전트가 지난번과 똑같은 스무 가지 질문을 던지는 것을 지켜봐야 했습니다. 라우터(Router)를 사용할 것인가 말 것인가? 서버 상태(Server state)는 어디에 저장되는가? 버튼이 로딩 중일 때 어떻게 보여야 하는가? 저는 그것들에 대해 한 번 글로 답을 해주었고, 세 번째 프로젝트쯤 되었을 때는 똑같은 문단을 똑같은 첫 번째 프롬프트(Prompt)에 붙여넣고 있었습니다. 이것은 워크플로우의 문제가 아닙니다. 이것은 스캐폴딩(Scaffolding)의 부재 문제이며, 아무리 프로세스 규율을 지켜도 해결되지 않습니다. PR을 완벽하게 리뷰할 수는 있겠지만, 에이전트에게 하지 말라고 말해준 것이 아무것도 없다면 결국 에이전트가 아무것도 없는 상태에서 스스로 만들어낸 디자인 시스템을 마주하게 될 뿐입니다.
그래서 저는 railhead를 만들었습니다.
제가 실제로 원했던 것
또 다른 SPA 보일러플레이트(Boilerplate)가 아닙니다. 그런 것들은 이미 충분히 많고, 그중 어느 것도 특정한 실패 모드(Failure mode)를 염두에 두고 만들어지지 않았습니다. 즉, AI 에이전트가 나의 결정 대신 자신의 학습 데이터 기본값(Training-data defaults)으로 공백을 채워버리는 문제 말입니다. 그대로 내버려 두면, Claude의 기본 프런트엔드(Frontend)는 다른 모든 AI 생성 프런트엔드와 똑같이 보입니다. 보라색에서 파란색으로 이어지는 그라데이션, 어디에나 쓰이는 Inter 폰트, 동일한 아이콘과 헤딩이 결합된 카드 그리드, 그리고 아무도 요청하지 않은 크림색 배경까지 말이죠. 그것이 딱히 틀린 것은 아닙니다. 다만 제 것이 아닐 뿐이며, 매번 똑같이 "제 것이 아닌" 상태로 나타납니다.
저는 에이전트의 첫 번째 행동이 추측이 아니라 세 개의 파일을 읽는 것인 저장소를 원했습니다. "환각(Hallucination) 최소화"가 시스템 프롬프트(System prompt)에서 반복하는 막연한 느낌이 아니라, 코드베이스가 강제하는 실제 제약 조건(Constraint)이 되는 곳 말입니다. 예를 들어, API 타입(Type)은 단 하나의 생성된 파일로부터 가져와야 하며, 그렇지 않으면 빌드(Build)가 실패하도록 하여, 에이전트가 백엔드(Backend)가 아마도 이런 형태를 반환할 것이라고 생각하는 아무 형태나 가져다 쓰지 못하게 하는 것입니다.
AGENTS.md의 최상단에는 정확히 두 가지 규칙이 있으며, 템플릿의 다른 모든 결정은 이 중 하나로 거슬러 올라갑니다. 첫째: 환각 (Hallucination) 최소화. 단 하나의 API 클라이언트, 단 하나의 생성된 타입 세트만 사용하며, 그럴듯해 보이는 엔드포인트를 임의로 만들어내지 않는 것입니다. 둘째: 과잉 엔지니어링 (Over-engineering) 금지. 모델이 학습 데이터에서 수천 번은 보았을 "베스트 프랙티스 (Best practice)"라 할지라도, 아무도 요청하지 않은 추상화, 라이브러리, 또는 폴더를 추가하지 마십시오. 두 번째 규칙은 제가 거의 건너뛸 뻔했던 규칙이며, 일상적으로 더 중요한 규칙입니다. 엔드포인트를 환각하는 에이전트는 tsc에 의해 적발됩니다. 하지만 아무도 필요로 하지 않는 상태 관리 (State management) 라이브러리를 조용히 추가하는 에이전트는, 그 무엇으로도 잡아낼 수 없는 방식으로 코드베이스를 악화시킵니다.
실제로 포함된 내용
React 19, strict mode가 적용된 TypeScript 6, Vite 8. 라이브러리 모드의 React Router 7: 라우팅만 담당하며, 쿼리 캐시 (Query cache)와 충돌하는 데이터 로더 (Data loaders)는 포함하지 않습니다. TanStack Query가 서버 상태 (Server state)의 모든 바이트를 관리하며, Zustand는 반사적으로 사용하는 것이 아니라 트리 전체에서 진정으로 공유되는 상태에만 나타납니다. 입력 필드가 있는 모든 것에는 React Hook Form과 Zod를 사용합니다. Tailwind v4와 shadcn/ui는 CLI를 통해 추가하여, 컴포넌트가 생성되기 전이 아니라 생성된 후에 제가 직접 제어할 수 있도록 합니다. ESLint와 Prettier 조합 대신 Biome을 사용하는데, 이는 서로 의견이 다를 수 있는 두 개의 도구가 에이전트가 스스로 조정하기에는 너무 많은 도구이기 때문입니다. 유닛 테스트 (Units)를 위한 Vitest, 그리고 실제로 중요한 두 가지 경로(성공 경로와 요청이 실패하는 경로)를 위한 Playwright를 사용합니다.
이 중 어느 것도 생소한 것은 아닙니다. 차이점은 그것을 뒷받침하는 원칙에 있습니다.
첫 실행 모습
$ git clone https://github.com/sh5623/railhead.git
$ cd railhead
$ pnpm install
...
.env 파일을 먼저 건드리지 않고 해당 URL을 열면 의도적으로 빈 흰색 페이지가 나타납니다. src/lib/env.ts는 시작 시 Zod를 사용하여 VITE_API_BASE_URL을 검증하며, 해당 값이 누락되면 오류를 발생시킵니다. 무언가를 클릭하기 전까지는 멀쩡해 보이다가 조용히 깨진 앱을 렌더링하는 대신, 콘솔에 명확하게 오류를 알립니다. 저는 실패 모드가 "작동하다가 갑자기 안 되는 것"이 아니길 원했습니다. 대신 "작동하지 않으며, 그 이유를 단 한 줄로 정확히 알려주는 것"이 되길 원했습니다.
$ cp .env.example .env
$ pnpm dev
이제 홈 라우트(home route)는 제가 언급했던 온보딩 워크스루(onboarding walkthrough)입니다. 단순한 플레이스홀더(placeholder)가 아니라, 곧 전달할 규칙들을 설명하는 실제 페이지입니다. 에이전트(agent)에게 그 뒤에 화면을 추가하라고 요청하면, 에이전트는 빈 파일이나 일반적인 React 앱의 모습에 대한 막연한 기억에서 시작하는 것이 아닙니다. 에이전트는 읽을 수 있는 LoginPage와 HealthBadge, grep할 수 있는 AGENTS.md, 그리고 설정에서 벗어날 경우 명확하게 오류를 발생시킬 것을 알고 있는 pnpm check && pnpm typecheck로부터 시작합니다.
마지막 부분 또한 선택 사항이 아닙니다. Husky는 모든 커밋마다 lint-staged를 실행합니다. CI는 모든 푸시(push)마다 깨끗한 환경에서 타입 체크(typecheck), biome ci, 단위 테스트(unit tests), 빌드(build), 그리고 Playwright까지 전체 시퀀스를 실행합니다. "내 로컬 환경에서는 되는데" 또는 "PR에서는 되는데"와 같은 상황이 조용히 갈라지는 일은 발생할 수 없습니다.
의도된 세 개의 파일
이전 시도에서 제가 계속 실수했던 부분은 이것입니다. 모든 것을 설명하는 긴 README를 작성하곤 했는데, 그러면 에이전트는 이를 대충 훑어보거나 컨벤션(conventions)과 분위기(vibes)를 동일한 범주의 지침으로 취급하곤 했습니다. 그래서 railhead는 도구(tooling) 자체에서 빌려온 규칙에 따라 분리했습니다. 도구가 강제할 수 있는 것이라면, 문서는 이를 반복하지 않는다.
biome.json과 tsconfig.json이 린트(lint), 포맷(format), 임포트 순서(import order), 널 안전성(null-safety)을 담당합니다. pnpm check와 pnpm typecheck를 실행하면 해당 카테고리는 모두 처리됩니다. 별도의 산문(prose)은 필요하지 않습니다. 왜냐하면 산문이야말로 에이전트가 압박을 받는 상황에서 가장 잘 잊어버리는 것이기 때문입니다.
남은 내용은 AGENTS.md에 들어갑니다. 오직 남은 것들만 말이죠. 즉, 단일 API 패턴(하나의 생성된 openapi-fetch 클라이언트, 직접 작성한 fetch 호출 금지, 타입이 일치하지 않을 때 as any 사용 금지: 만약 일치하지 않는다면 스펙이 오래된 것이니 스펙을 수정할 것), 서버 상태(server state)가 존재할 수 있는 위치, 그리고 캐시 무효화(cache invalidation)가 추측에 의존하지 않도록 쿼리 키(query keys)를 구축하는 방법 등이 포함됩니다. CLAUDE.md는 단순히 AGENTS.md를 가리키는 두 줄짜리 스텁(stub)일 뿐입니다. 따라서 동기화해야 할 문서가 두 개의 어긋나는 복사본이 아니라, 정확히 하나가 되도록 합니다.
그다음에는 DESIGN.md와 PRODUCT.md가 있는데, 이는 "보기 좋게 만들어줘"라는 말이 지시가 아니라 단순한 바람에 불과하기 때문에 존재합니다. DESIGN.md에는 실제 토큰(tokens)들이 들어있습니다: OKLCH 색상, 평면에서 오버레이(overlay)까지 이어지는 5단계의 고도(elevation) 사다리, 그리고 브랜드 강조색(brand accent)은 어떤 표면에서도 4~8%만 차지해야 하며 그 이상은 안 된다는 규칙 같은 것들입니다. 또한 금지 목록(bans list)도 포함되어 있는데, 솔직히 말해서 제가 생각했던 것보다 더 즐겁게 작성했습니다: 텍스트 그라데이션 금지, 기본값으로 글래스모피즘(glassmorphism) 사용 금지, 동일한 카드 그리드 금지, 기본 강조색으로 보라색에서 파란색으로 이어지는 그라데이션 금지(저도 직접 출시해 본 적이 있기에 콕 집어 명시했습니다). 만약 당신이 이 중 하나를 사용하려 한다면, 이 파일은 대신 구조를 재조정하라고 알려줄 것입니다. PRODUCT.md는 더 짧습니다: 사용자가 누구인지, 브랜드 보이스(brand voice)를 나타내는 세 단어, 그리고 이 제품이 명시적으로 지향하지 않는 것들의 짧은 목록입니다.
세 개의 파일입니다. 하나의 거대한 파일도 아니고, 여기저기 흩어진 열 개의 파일도 아닙니다. "설정 페이지를 추가해줘"라는 명령을 읽는 에이전트가, 이번 세션 동안 이미 암기하고 있는 API 컨벤션(conventions)을 다시 읽을 필요 없이 디자인 토큰만 가져올 수 있을 만큼 충분한 분리입니다.
토큰은 hex(16진수)가 아닌 OKLCH를 사용합니다. 눈대중으로 확인하는 대신 대비비(contrast ratios)를 논리적으로 추론할 수 있기를 원했기 때문입니다. 또한 elevation ladder(고도 계층)는 shadcn 컴포넌트가 이미 렌더링되는 방식에 직접 매핑되어 있어, 새로운 컴포넌트가 추가될 때 누군가 수동으로 그림자 위치를 결정할 필요 없이 시스템에 자연스럽게 안착합니다. 하지만 제가 가장 먼저 강조하고 싶은 부분은 accent cap(액센트 제한)입니다. 어떤 표면이든 4~8%를 넘지 않도록 설정했습니다. 이것이 제가 계속 언급하는 그 보라색 그라데이션을 금지하는 실제 메커니즘입니다. 단순히 못생겨서 금지하는 것이 아닙니다. "표면 전체를 그라데이션으로 채우는 것"과 "표면의 4%만 액센트로 사용하는 것"은 서로 다른 디자인 언어이며, 제가 의도적으로 결정한 것은 후자뿐이기 때문입니다.
온보딩 페이지가 곧 데모입니다
저는 단순히 이것이 작동한다고 말만 하고 싶지 않았기에, 템플릿에 이를 보여주는 페이지를 포함했습니다. 레포지토리(repo)를 클론(clone)하고 pnpm dev를 실행하면, 홈 라우트(home route)는 빈 캔버스가 아닙니다. 이름의 유혹을 뿌리칠 수 없어서 작은 철도 노선 메타포(metaphor)를 따라 구축한, 템플릿 자체를 스테이션별로 훑어보는 스크롤 방식의 워크스루(walkthrough) 페이지가 나타납니다. 이 페이지는 두 가지 핵심 규칙을 다루고, 표준 패턴(canonical patterns)을 안내하며, 솔직히 말해 "fe-rail 설치는 선택 사항입니다"라는 제목의 섹션도 포함하고 있습니다. 실제로 그렇기 때문입니다. railhead는 무엇으로 빌드할지를 결정하고, fe-rail은 어떤 순서로 빌드할지를 결정합니다. 이 둘은 같은 레일이 아니며, 어느 쪽도 다른 쪽을 필요로 하지 않습니다.
하지만 해당 워크스루의 페이지 중 두 개는 데모용 콘텐츠가 아닙니다. 그것들은 구조를 지탱하는 핵심 요소입니다. src/features/auth와 src/features/health는 "백엔드를 호출하는 단 하나의 방식"과 "라우트를 제한하는 단 하나의 방식"에 대한 표준 구현(canonical implementations)입니다. README에는 이 템플릿에서 실제 프로젝트를 부트스트랩(bootstrap)할 때, 온보딩 투어는 삭제하되 이 두 가지는 유지하라고 명시되어 있습니다. 이것들은 교체해야 할 샘플 코드가 아닙니다. 앱의 나머지 부분이 복제해야 할 패턴입니다.
아직 확신이 서지 않는 부분
어느 부분이 미흡한지에 대해 솔직하게 말씀드리겠습니다. railhead는 아직 초기 단계입니다. 저는 오늘 이 프로젝트를 만들기 시작했으며, 이 글을 쓰는 시점까지 실제 프로덕션 프로젝트로 부트스트랩(bootstrapped)되지는 않았고, 그저 구축한 뒤 수동으로 훑어본 상태입니다. 위의 모든 내용은 실제이며 작동하지만, "내가 테스트할 때 작동한다"와 "다른 사람이 만든 엉망인 첫 번째 피처 브랜치(feature branch)에서도 살아남는다"는 서로 다른 주장이며, 저는 현재 전자에 대해서만 확신할 수 있습니다.
제가 여전히 고민 중인 부분은 shadcn MCP 서버입니다. 에이전트가 컴포넌트의 속성(props)을 추측하는 대신 실제 속성을 찾아볼 수 있도록 의도적으로 리포지토리(repo)에 포함시켰습니다. 이것은 API 레이어뿐만 아니라 UI 레이어에도 적용된 "환각(hallucination) 최소화" 원칙의 핵심입니다. 하지만 이는 새로운 기여자가 가장 먼저 권한 요청 프롬프트를 보게 된다는 의미이기도 하며, 이 트레이드오프(tradeoff)가 이 프로젝트를 클론(clone)하는 모든 팀에게 그만한 가치가 있는지에 대해서는 아직 완전히 확신하지 못하고 있습니다. 현재는 이를 그대로 유지하며 발생하는 마찰을 감수하고 있습니다.
또 다른 미결 과제는 DESIGN.md가 자신의 시스템을 원하는 실제 디자이너를 만났을 때 얼마나 살아남을 수 있느냐 하는 점입니다. 저는 금지 목록(bans list)을 제 개인적인 취향에 따라 작성했는데, 취향은 일반화될 수 없습니다. 저는 특정 값 자체보다는 구조 — 토큰(tokens)에 금지 목록과 각 금지 사유를 더한 형태 — 가 더 잘 일반화될 것이라고 생각하지만, 아직 다른 사람의 프로젝트에서 이 주장을 테스트해보지는 못했습니다.
사용해보기
git clone https://github.com/sh5623/railhead.git
cd railhead
pnpm install
...
fe-rail과 마찬가지로 MIT 라이선스가 적용됩니다. 만약 이미 fe-rail을 사용 중이라면, 두 프로젝트는 함께 사용하도록 설계되었습니다. railhead는 에이전트에게 기술 스택과 규칙을 제공하고, fe-rail은 워크플로우(workflow)를 제공합니다. 어느 쪽도 상대방을 반드시 채택하라고 요구하지 않습니다.
여러분의 기술 스택에서 직접 사용해 보았을 때 무엇이 가장 먼저 깨지는지 알고 싶습니다. 여러분의 팀이 가진 관습 중 AGENTS.md, DESIGN.md, 또는 PRODUCT.md에 아직 포함되지 않은 것은 무엇인가요? 에이전트가 그것을 추론하기를 원하시나요, 아니면 물어보기를 원하시나요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기