3편까지 로컬에서 AI Factory 파이프라인이 꽤 안정적으로 돌아가게 되었습니다. 그래서 이번에는 Railway에 올려서 서버에서 자동으로 앱을 만들도록 하려고 했는데요.

예상치 못한 곳에서 삽질이 시작되었습니다. exit code null. 이 정체불명의 에러와 하룻밤을 보냈습니다..

그리고 Railway 삽질을 하면서 기존 파이프라인의 근본적인 품질 문제들을 다시 들여다보게 되었고, 꽤 의미있는 개선을 하게 되었습니다!

바로 본론으로 들어가겠습니다!!


Railway 배포: “로컬에선 됐는데…”

Railway는 GitHub 레포를 연결하면 push할 때마다 자동으로 빌드/배포해주는 서비스입니다. AI Factory의 대시보드(web)를 Railway에 올리고, 거기서 파이프라인을 실행하면 서버가 알아서 앱을 만들어주는 구조로 가려고 했습니다.

문제는 Railway 컨테이너가 빈 서버라는 것입니다.

로컬에서는 git config에 이름/이메일이 설정되어 있고, Claude Code CLI도 설치되어 있고, GitHub 인증도 되어있습니다. Railway에서는 이 모든 것이 없습니다.

그래서 파이프라인 시작 시 자동으로 환경을 세팅하는 setupRailwayEnvironment() 함수를 만들었습니다.

function setupRailwayEnvironment(): void {
  const isRailway = !!process.env.RAILWAY_ENVIRONMENT;
  if (!isRailway) return;

  // Git user config (commit에 필요)
  spawnSync("git", ["config", "--global", "user.name", "AI Factory Bot"]);
  spawnSync("git", ["config", "--global", "user.email", "bot@ai-factory.dev"]);

  // Claude Code CLI 설치
  // ...
}

여기서 첫 번째 문제가 터졌습니다.


첫 번째 삽질: git credential 쓰기 실패

Railway 로그에 이런 메시지가 떴습니다.

Failed to write git credentials

처음에는 git credential helper에 GITHUB_TOKEN을 저장하는 방식을 썼는데, Railway 컨테이너의 홈 디렉토리 권한 문제로 .git-credentials 파일을 쓸 수 없었습니다.

해결: credential 파일 대신 git URL rewrite 방식으로 변경했습니다.

// 기존: credential 파일 쓰기 (실패)
fs.writeFileSync("~/.git-credentials", `https://owner:token@github.com\n`);

// 변경: URL rewrite (성공)
spawnSync("git", ["config", "--global",
  `url.https://${owner}:${token}@github.com/.insteadOf`,
  "https://github.com/"
]);

이렇게 하면 git이 https://github.com/ 요청을 할 때 자동으로 토큰이 붙은 URL로 변환됩니다.


두 번째 삽질: exit code null의 정체

credential 문제를 해결하고 다시 돌렸더니 이번에는 이런 에러가 났습니다.

stage scaffold failed (non-critical) - exitCode: null
[push scaffold] failed: exit code null
Pipeline crash

exit code null. 0이 아니라 null입니다. 이건 프로세스가 정상 종료된 게 아니라 시그널로 종료되었거나, spawn 자체가 실패했다는 의미입니다.

원인을 추적하기 위해 safeGit() 함수에 디버깅 로그를 추가하고, git init 직후 테스트 커밋도 추가했습니다.

결과적으로 원인은 두 가지였습니다.

  1. HOME 환경변수 미설정: Railway 컨테이너에서 HOME이 없을 수 있어서 git global config이 안 먹힘
  2. safe.directory 문제: Railway에서 파일 소유권이 다르면 git이 “unsafe repository” 에러를 던짐
if (!process.env.HOME) {
  process.env.HOME = "/root";
}
spawnSync("git", ["config", "--global", "--add", "safe.directory", "*"]);

이 두 줄을 추가하니 exit code null이 사라졌습니다!! 로컬에서 “당연히 되는 것”이 서버에서는 안 될 수 있다는 것을 또 한번 체감했습니다..


세 번째 삽질: 크래시해도 상태가 “coding…”

파이프라인이 에러로 중단되었는데 대시보드에서는 여전히 “coding…” 상태로 표시됩니다. 새로고침해도 안 바뀝니다.

Railway에서는 파이프라인이 별도 자식 프로세스로 실행되는데, 이 프로세스가 크래시하면 메모리의 상태가 업데이트되지 않습니다.

해결: 대시보드의 상태 확인 API에서 DB의 최근 이벤트를 자동으로 확인해서, 크래시 이벤트가 있으면 자동으로 “failed” 상태로 전환하는 로직을 추가했습니다.


Work Packets이 2개만 생성되는 문제

Railway 환경 문제를 해결하고 드디어 파이프라인이 돌아가기 시작했는데.. Spec Agent가 Work Packets을 2개만 만들고 끝나버렸습니다. 최소 6개는 나와야 하는데요.

원인: GPT-5.2에 maxTokens: 12000으로 설정했는데, JSON 구조화 출력이 토큰을 많이 소비하다 보니 토큰이 부족해서 2개만 만들고 잘려버린 것이었습니다.

12000 → 16000으로 올려봤지만 여전히 부족해서, 결국 32000으로 대폭 올렸더니 17개 패킷이 생성되었습니다!!

[spec] 17 work packets generated
[spec] === Done in 223.2s · $0.1300 ===

비용은 $0.13으로, maxTokens를 올려도 실제 사용 토큰만 과금되기 때문에 비용 차이가 거의 없었습니다. 넉넉하게 잡는 것이 답이었습니다.


비슷한 도구들은 어떻게 하고 있을까?

Railway 삽질을 하면서 “나 말고도 이런 자동화 파이프라인을 만든 사람들이 있을까?”라는 궁금증이 생겨서 시장을 조사해봤습니다.

자율성 낮음 ←─────────────────→ 자율성 높음

Cursor/Windsurf     AI Factory        Lovable/Bolt      Devin
(개발자 보조)      (파이프라인 자동화)  (노코드 생성)     (완전 자율)

비슷한 목표를 가진 프로젝트들이 여럿 있었습니다. Lovable, Bolt은 브라우저에서 바로 앱을 생성하는 방식이고, Devin은 완전 자율형, MetaGPT는 AI 소프트웨어 회사를 시뮬레이션하는 프레임워크였습니다. 그리고 MoAI-ADK라는 프로젝트가 특히 눈에 띄었는데, Claude Code 내부에서 27개 에이전트를 오케스트레이션하는 방식이었습니다.

재밌는 발견은, AI Factory가 이미 이런 도구들의 핵심 매커니즘(git worktree 격리, 에이전트 역할 분리, 파이프라인 오케스트레이션)을 상당 부분 갖추고 있었다는 것입니다. 차이는 세부 구현의 정교함에 있었습니다.

이 조사가 정말 도움이 되었던 이유는, 제가 파이프라인을 돌리면서 반복적으로 느꼈던 문제들(import 충돌, 에러 방치, UI 불일관성)을 이미 다른 도구들이 각자의 방식으로 해결하고 있었기 때문입니다. 이런 사례들을 참고하면서 AI Factory를 고도화하는 방향이 더 명확해졌습니다!


품질 개선 Phase 1: 다른 도구들을 참고하며 고도화

파이프라인을 계속 돌리면서 반복적으로 느꼈던 근본 문제 세 가지가 있었습니다. 앞서 조사한 MoAI-ADK, Devin, v0(Vercel) 같은 도구들의 접근 방식을 참고하면서, 이 문제들을 하나씩 해결해보기로 했습니다.

문제 1: 패킷 간 import 충돌

패킷 0001이 src/lib/db.ts를 만들고, 패킷 0002가 이 파일을 import합니다. 그런데 패킷 0002가 코딩될 때 import 경로를 다르게 추측하면 빌드가 깨집니다.

MoAI-ADK를 분석해보니 코딩 전에 모든 파일을 빈 스텁으로 먼저 생성해서 import 경로를 확정하는 방식을 쓰고 있었습니다. 이걸 참고해서 AI Factory에도 적용했습니다.

// API route 스텁
export default function handler(req, res) {
  res.status(501).json({ error: "Not implemented" });
}

// React 컴포넌트 스텁
export default function Dashboard() {
  return null;
}

Work Packets의 files 필드에서 모든 파일 경로를 추출하고, 파일 유형에 맞는 스텁을 자동 생성하는 generateFileStubs() 함수를 구현했습니다. 코딩 에이전트가 작업을 시작하기 전에 import 경로가 이미 확정되어 있으니까 패킷 간 충돌이 크게 줄어듭니다.

문제 2: 코딩 에이전트가 에러를 방치함

기존에는 Claude Code가 코딩을 끝내면 “끝났습니다”라고 하고, 외부에서 typecheck/test를 돌렸습니다. 실패하면 새 세션으로 재시도하는데, 이전 세션의 컨텍스트가 유실되어 같은 실수를 반복하는 경우가 많았습니다.

Devin의 사례를 보니 코딩 에이전트가 스스로 실행 → 에러 확인 → 수정 → 반복하는 자기 검증 루프 방식을 쓰고 있었습니다. 이걸 참고해서 CLAUDE.md의 AFTER 섹션을 대폭 강화했습니다.

## AFTER writing code (MANDATORY — self-verification loop)
You MUST run these checks and fix any failures YOURSELF before finishing.
Do NOT finish with failing checks. Repeat fix→check up to 3 times.

1. pnpm typecheck — if errors, fix them and re-run
2. npx vitest run — if failures, read the error, fix source code, re-run
3. npx next build — if fails, fix and re-run

CRITICAL: Do NOT give up on failing tests.
Common test fixes:
- "Cannot find module" → check import path
- "UNIQUE constraint" → use unique test data (Date.now())
- "no such table" → ensure DB migration runs in test setup

Only finish when ALL 3 checks pass.

핵심은 “포기하지 마라”흔한 에러 패턴 힌트를 같이 주는 것입니다. 기존에는 Claude Code가 테스트 실패를 보고 “이건 어렵네요, 스킵하겠습니다”라고 했던 경우가 있었는데, 이 지시를 넣으니 자기가 만든 에러를 자기가 끝까지 고치려 시도합니다.

문제 3: UI 품질이 들쭉날쭉

“대시보드를 만들어라”라고 하면 AI마다 해석이 다릅니다. 어떤 세션에서는 멋진 카드 레이아웃이 나오고, 어떤 세션에서는 <table> 하나만 나옵니다.

v0(Vercel)는 페이지 유형별로 구체적인 코드 스니펫을 프롬프트에 포함해서 UI 일관성을 확보하는 방식을 쓰고 있었습니다. 이걸 참고해서 getPageTypeReference() 함수를 만들었습니다. 패킷의 제목과 설명을 분석하고, 6가지 페이지 유형별로 구체적인 레이아웃 코드를 자동 주입합니다.

감지 키워드페이지 유형주입 내용
landing, hero, homeLanding PageHero 섹션 + 3열 Features + CTA
dashboard, analyticsDashboard통계 카드 4열 + 데이터 테이블
login, signup, authAuth Page중앙 카드 + 폼
settings, profileSettings섹션별 카드 + Danger Zone
list, table, historyList/Collection필터 + 빈 상태 + 데이터 목록
detail, edit, [id]Detail/Edit브레드크럼 + 헤더 + 콘텐츠 카드

예를 들어 패킷 제목에 “dashboard”가 포함되어 있으면 이런 코드가 자동으로 프롬프트에 추가됩니다.

<div className="max-w-6xl mx-auto">
  <h1 className="text-3xl font-bold mb-8">Dashboard</h1>
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
    {stats.map(s => (
      <Card key={s.label} className="p-6">
        <p className="text-sm text-[var(--text-secondary)] mb-1">{s.label}</p>
        <p className="text-2xl font-bold">{s.value}</p>
      </Card>
    ))}
  </div>
</div>

“멋지게 만들어줘”가 아니라 “이 코드 패턴을 따라서 만들어줘”로 바꾸니까 UI 품질의 일관성이 확실히 올라갈 것으로 기대됩니다!!


Phase 2 계획: 더 나아가기

Phase 1을 완료하고, 다음 단계도 계획을 세워두었습니다.

2-1. 역할 분리 에이전트

현재는 Claude Code 하나가 코딩 + 테스트 + 스타일링을 전부 합니다. 이걸 3단계로 분리하면 각 역할에 최적화된 프롬프트와 모델을 쓸 수 있습니다.

1단계: Coder (Sonnet) → 비즈니스 로직만 구현
2단계: Tester (Haiku) → 코드를 보고 테스트 작성 (저렴한 모델이면 충분)
3단계: Polisher (Sonnet) → UI 폴리싱 + 접근성

3편에서 말했던 “적재적소” 원칙의 확장입니다.

2-2. 라이브 프리뷰

현재는 파이프라인 완주 후 배포해야 결과를 볼 수 있습니다. 패킷 완료마다 dev 서버를 잠깐 띄우고 스크린샷을 캡처해서 대시보드에 표시하면 문제를 일찍 발견할 수 있을 것 같습니다.

2-3. 구조화된 컨텍스트 맵

현재 패킷 간 컨텍스트 전달은 “파일 트리 + export 목록”을 텍스트로 전달하는 수준입니다. 이걸 모듈 단위의 구조화된 JSON으로 바꾸면 AI가 전체 앱 구조를 더 깊이 이해할 수 있을 것이라 생각합니다.

{
  "modules": {
    "auth": { "files": ["src/lib/auth.ts"], "exports": ["login", "logout"], "dependencies": ["db"] },
    "meals": { "files": ["src/lib/meals.ts"], "exports": ["createMeal"], "dependencies": ["auth", "db"] }
  },
  "dataFlow": "user → auth → meals → monster"
}

클로드 코드와 페어 프로그래밍하는 느낌

이번 글에서 다룬 작업들은 대부분 클로드 코드(Claude Code)와 함께 진행했습니다.

흥미로운 점은, 이 과정이 일반적인 “AI에게 시키기”와는 좀 달랐다는 것입니다. “이거 만들어줘”라고 던지는 게 아니라 같이 고민하면서 만들어가는 느낌이었습니다.

예를 들어:

  • “Railway에서 git push가 exit code null로 실패해” → 클로드가 원인 분석 + 디버깅 로그 추가 → 로그 결과 공유 → 클로드가 수정안 제시 → 확인 → 커밋/푸시
  • “비슷한 도구들이 뭐가 있지?” → 시장 조사 → 비교 분석 → 사례 참고해서 개선 방향 수립
  • “패킷 간 import 충돌을 어떻게 줄이지?” → 여러 접근법 논의 → 빈 파일 스캐폴딩 구현 → 테스트

이런 식으로 “나는 방향을 잡고 판단하고, 클로드는 구현과 분석을 담당하는” 페어 프로그래밍이 자연스럽게 이루어졌습니다. 처음에 AI Factory를 시작한 동기가 “AI 코딩의 한계를 직접 부딪혀보고 싶다”였는데, 이렇게 AI와 협업하면서 개발하는 것 자체가 그 한계를 탐색하는 과정인 것 같습니다.


4편을 마치며

이번 글의 교훈을 정리하면 이렇습니다.

  1. 로컬에서 되는 것이 서버에서 안 되는 이유는 대부분 “환경” — HOME, git config, safe.directory 같은 사소한 것들
  2. maxTokens는 넉넉하게 — 실제 사용 토큰만 과금되니까, 부족해서 잘리는 것보다 넉넉한 게 낫다
  3. 비슷한 도구들의 사례가 큰 도움이 된다 — MoAI-ADK, Devin, v0 등의 접근 방식을 참고하면서 AI Factory를 고도화할 방향이 명확해짐
  4. 전면 재설계보다 점진적 개선이 현실적 — 빈 파일 스캐폴딩 + 자기 검증 루프 + 디자인 레퍼런스를 1~2일 만에 적용 가능하고, 효과도 즉각적

시장 조사를 하면서 느낀 것도 있습니다. AI Factory와 비슷한 시도를 하는 프로젝트들이 여럿 있었는데, 그게 실망이 아니라 오히려 “이 방향이 맞다는 검증”이었습니다. 그리고 각 도구의 접근 방식을 참고하면서 기존에 고민하던 문제들의 해결 실마리를 얻을 수 있었습니다.

다음 글에서는 Phase 2 작업 결과와 실제로 생성된 앱의 품질 변화를 다룰 예정입니다!!

감사합니다!!


이 시점의 파이프라인 구조 (Phase 1 적용 후)

flowchart TD
    subgraph row1 ["기획 → 설계"]
        direction LR
        Scout["Scout"] --> Planning["Planning"] --> Spec["Spec\nmaxTokens 32K"]
    end
    subgraph row2 ["준비 → 코딩 (Phase 1 개선)"]
        direction LR
        Arch["Architecture\n빈 파일 스캐폴딩"] --> Scaffold["Scaffold\n디자인 레퍼런스 6종"] --> Coding["Coding\n자기 검증 루프 ×3"]
    end
    subgraph row3 ["품질 → 배포"]
        direction LR
        Quality["Quality Gates\nTRUST-5 + fixLoop"] --> Deploy["Deploy\nRailway"]
    end
    row1 --> row2 --> row3
    style Arch fill:#fff3cd,stroke:#ffc107,color:#000
    style Scaffold fill:#fff3cd,stroke:#ffc107,color:#000
    style Coding fill:#fff3cd,stroke:#ffc107,color:#000

© 2025. All rights reserved.

김민제의 블로그