본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 04. 23:56

나만의 미니 Claude Code를 밑바닥부터 직접 만들어 보았습니다. 여기서 배운 점들

요약

Claude Code의 동작 방식을 역공학하여 TypeScript와 Vercel AI SDK 기반의 CLI 코딩 에이전트를 직접 구현한 과정을 다룹니다. 파일 시스템, Bash, Git, 웹 검색 등 다양한 도구를 갖춘 에이전트 루프 아키텍처와 구현 기술을 소개합니다.

핵심 포인트

  • Vercel AI SDK를 활용한 도구 호출 에이전트 루프 구현
  • Zod 스키마를 이용한 도구 입력 및 출력의 엄격한 타입 지정
  • 파일, Bash, Git, 웹 검색 등 다양한 도구 통합 방식
  • Ink를 사용한 React 스타일의 터미널 UI 구축

나만의 미니 Claude Code를 밑바닥부터 직접 만들어 보았습니다. 여기서 배운 점들

몇 달 전 저는 한 가지 주제에 깊이 빠져들었습니다. OpenCode의 GitHub 저장소를 읽고, Claude Code의 동작 방식이 담긴 스크린샷을 연구하며, 그 흐름을 역공학 (Reverse Engineering) 하는 것이었습니다. 저는 이러한 터미널 코딩 에이전트 (Terminal Coding Agents)가 단순히 어떻게 사용되는지를 넘어, 내부적으로 실제로 어떻게 작동하는지 이해하고 직접 만들어보고 싶었습니다. 이 포스트는 제가 무엇을 만들었는지, 제가 마주한 실제 기술적 도전 과제들은 무엇이었는지, 그리고 다음 단계로 무엇을 계획하고 있는지에 관한 것입니다.

내가 만든 것

CLI 기반의 AI 코딩 에이전트입니다. 터미널을 열고 프로젝트 내부에서 에이전트를 실행한 뒤 작업을 설명하면, 에이전트가 자율적으로 파일을 읽고, 수정하고, 명령어를 실행하고, 웹을 검색하며, Git에 커밋을 수행합니다. 이 모든 과정은 사용자의 코드를 변경하기 전에 반드시 사용자의 승인을 요청하는 상태에서 이루어집니다.

저는 이것을 단순히 병행하며 읽고 구현해 보기 위한 간단한 사이드 퀘스트 (Sidequest)로 시작했습니다. 이 프로젝트는 TypeScript로 구축되었으며, Bun에서 실행되고, Vercel의 AI SDK (Agents, Tools, Loop Control)를 사용합니다. 모델로는 Google Gemini 2.5 Flash를 사용하고 있는데 (무료 티어가 있기 때문입니다, 하하), 터미널 UI는 Ink로 구축했습니다. React로 프론트엔드를 작성하는 것과 매우 유사하기 때문에 Ink를 사용했습니다.

코딩 에이전트가 할 수 있는 기능의 대략적인 분류는 다음과 같습니다:

  • 파일 시스템 도구 (Filesystem tools): 파일 읽기, 쓰기, 검색 및 수정
  • bash 도구 (Bash tools): ls, pwd, grep
  • git 도구 (Git tools): commit, push, pull, github cli를 통한 PR 및 이슈 생성/관리
  • 명령어 실행 (Command execution): npm, pnpm, python, pip, cargo 등을 실행
  • 웹 도구 (Web tools): 어떤 쿼리로든 웹 검색, URL 가져오기
  • 플래너 서브 에이전트 (A planner sub-agent): 큰 작업을 더 작은 할 일 목록 (Todos)으로 분해
  • 메모리 시스템 (A memory system): .agent/ 마크다운 (Markdown) 파일을 통해 세션 간 컨텍스트 (Context) 유지

모든 것은 Zod 스키마 (Schemas)를 통해 엔드 투 엔드 (End-to-end)로 엄격하게 타입이 지정되어 있으며, 도구들은 타입이 지정된 입력과 출력을 가집니다. 이에 대해서는 아래에서 자세히 다루겠습니다.

아키텍처 (The Architecture)

핵심은 **도구 호출 에이전트 루프 (tool-calling agent loop)**입니다. 모델은 사용자의 프롬프트, 사용 가능한 도구 목록, 그리고 대화 기록을 전달받습니다. 모델은 어떤 도구를 호출할지 결정하고, 에이전트가 이를 실행하면 그 결과가 다시 피드백되어, 작업이 완료되거나 모델이 더 이상 도구 호출을 요청하지 않을 때까지 루프가 계속됩니다.

저는 스트리밍 (streaming)과 루프 제어를 처리하기 위해 Vercel의 AI SDK를 사용하였고, 모든 도구를 중앙 tools-registry.ts에 등록했습니다:

export const tools = {
  write_file: writeFileTool,
  read_file: readFileTool,
...

모든 도구는 입력과 출력을 위해 Zod 스키마 (Zod schema)로 정의됩니다. 이를 통해 모델은 각 도구가 무엇을 기대하고 무엇을 반환하는지에 대한 명확하고 타입이 지정된 계약 (typed contract)을 갖게 되며, 저는 전체 과정에서 안전한 실패 처리 (safe failure handling)를 할 수 있습니다. 예를 들어, 도구 호출의 입력이 잘못된 형식일 경우, Zod가 파일 시스템에 접근하기 전에 이를 잡아낼 수 있습니다.

도전 과제 1: 파일 편집 도구와 인간 참여형 루프 (Human-in-the-Loop)

이것은 해결하기 매우 흥미로운 문제였습니다.

파일 읽기, 명령 실행, 검색과 같은 다른 모든 도구들은 사용자의 개입 없이 자동으로 실행될 수 있습니다. 하지만 파일 편집은 파괴적 (destructive)입니다. 만약 에이전트가 잘못된 편집을 수행한다면, 디스크에 직접 쓰여지기 전에 이를 잡아낼 기회가 필요합니다.

제가 (Claude Code와 OpenCode를 공부하며) 영감을 얻은 패턴은 다음과 같습니다:

  1. 에이전트가 먼저 read_file을 호출하여 현재 파일 내용을 읽습니다.
  2. 에이전트가 기존 문자열과 교체하고자 하는 새로운 문자열 (diff)을 사용하여 edit_file을 호출합니다.
  3. 쓰기 전, 사용자에게 색상이 지정된 diff (삭제된 줄은 빨간색, 추가된 줄은 초록색)를 보여줍니다.
  4. 사용자가 승인하거나 거부할 때까지 기다립니다.

까다로운 부분은 4단계입니다. 에이전트 루프가 실행 중인 상태입니다. 루프 자체를 일시 중지할 방법 없이는 도구 실행 내부에서 단순히 사용자의 키 입력을 await 할 수 없습니다.

제가 이 문제를 해결한 방법은 다음과 같습니다: Vercel의 AI SDK의 streamText (및 generateText)는 루프 제어에 stopWhen 파라미터를 노출합니다. 저는 이를 사용하여 편집 도구 (edit tool)가 승인을 기다릴 때 에이전트 루프 (agent loop)를 일시 중지합니다. TUI는 isApproved 플래그를 비동기적으로 설정하고, 사용자는 Ink 컴포넌트를 통해 터미널에 렌더링된 차이점 (diff)을 확인한 후, 키를 눌러 승인 또는 거부를 결정합니다. 그러면 플래그가 바뀌고 루프가 재개되거나 편집이 폐기됩니다.

isApproved 필드는 실제로 편집 도구의 입력 스키마 (input schema)의 일부입니다:

export const EditFileInputSchema = z.object({
  filename: z.string(),
  folder: z.string().optional(),
...

그리고 출력 스키마 (output schema)는 needsApproval 플래그를 다시 전달합니다:

needsApproval: z.boolean().optional().describe(
  "Needs human approval to be true for the agent to write the changes in the file"
)

이것은 명확한 핸드셰이크 (handshake)를 생성합니다: 도구가 승인이 필요함을 신호하면, 루프가 일시 중지되고, 사람이 결정하면, 루프가 재개됩니다. 그 외의 모든 것은 자율적으로 실행됩니다.

또한 저는 경로 탐색 방지 (path traversal protection) 기능을 추가하여, 에이전트가 프로젝트 루트 디렉토리 외부에서 작동할 수 없도록 했습니다. 모든 파일 경로는 읽기 또는 쓰기가 수행되기 전에 루트 디렉토리를 기준으로 검증됩니다.

도전 과제 2: 플래너 서브 에이전트 (The Planner Sub-Agent)

작고 집중된 작업의 경우, 메인 에이전트가 모든 것을 직접 처리합니다. 하지만 사용자가 "이 모듈을 리팩터링해줘", "내 Node.js 백엔드에 인증 기능을 추가해줘"와 같이 더 크고 개방적인 작업을 주면, 단일한 평면 루프 (flat loop)로는 처리하기가 어려워집니다.

저의 해결책은 **플래너 서브 에이전트 (planner sub-agent)**였습니다. 메인 에이전트는 더 큰 작업을 감지하면 이를 하나의 도구로서 호출합니다. 플래너 서브 에이전트는 작업 분해 (task decomposition)에 완전히 집중된 자체 시스템 프롬프트 (system prompt)를 가집니다. 이는 작업을 구조화된 할 일 (todos) 목록으로 나누며, 각 항목은 다음과 같습니다:

  • 고유 ID
  • 작업 설명 (task description)
  • 상태 (not completed, ongoing, completed)
  • 우선순위 (1–5)

이 항목들은 Zod를 사용하여 타입이 지정됩니다:

export const SingleTodoSchema = z.object({
  id: z.string(),
  todo: z.string(),
...

플래너 (planner)가 할 일 (todos) 목록을 생성하면, 메인 에이전트 (main agent)가 getNextPendingTodoTool을 사용하여 이를 하나씩 가져옵니다. 그런 다음 사용 가능한 파일 시스템 (filesystem)/git/웹 (web) 도구들을 사용하여 작업을 실행하고, 다음 작업으로 넘어가기 전에 각 항목을 완료로 표시합니다. TUI (Terminal User Interface)는 실시간 할 일 목록을 렌더링하므로 에이전트가 작업을 수행하는 과정을 지켜볼 수 있습니다.

현재 이 방식은 **동기적 (synchronous)**이므로 한 번에 하나의 작업만 처리할 수 있습니다. 이는 제가 다음에 해결하려고 계획 중인 현재의 한계점입니다.

다음 단계: 병렬 서브 에이전트 (Parallel Sub-Agents)

플래너의 자연스러운 진화는 여러 개의 작은 에이전트들을 병렬로 실행하여, 각 에이전트가 분해된 할 일 목록에서 하나씩 독립적으로 가져가는 것입니다. 하지만 이는 명백한 충돌 문제를 야기합니다. 만약 두 에이전트가 동시에 동일한 파일을 수정하려고 한다면 어떻게 될까요?

제가 계획한 접근 방식은 이를 실행 단계 (execution level)가 아닌 **작업 메타데이터 단계 (task metadata level)**에서 해결하는 것입니다. 플래너 서브 에이전트가 작업을 분해할 때, 각 할 일은 어떤 파일들을 건드려야 하는지에 대한 메타데이터를 함께 포함하게 됩니다:

// 대략적인 아이디어이며, 아직 구현되지 않음
{
  id: "task-3",
...

작은 에이전트가 작업을 가져올 때, 해당 에이전트는 자신에게 할당된 파일에만 접근할 수 있습니다. 플래너는 두 작업이 동일한 파일을 공유하지 않도록 보장합니다. 이런 방식을 통해 병렬 에이전트들은 잘 정의된 경계 안에서 완전히 독립적으로 작동하며, 런타임 (runtime)에서의 충돌 해결이 필요 없습니다. 충돌이 계획 단계 (planning time)에서 구조적으로 방지되기 때문입니다. 또한 더 큰 컨텍스트 (context)를 유지하기 위해, 작은 에이전트들은 자신의 현재 상태(업데이트될 때)를 메인 에이전트에게 업데이트할 것입니다. 예: "task1 실패", "task2 성공", "task 3 진행 중".

왜 Gemini 2.5 Flash인가?

넉넉한 무료 티어 (free tier)를 제공하기 때문입니다. 무언가를 밑바닥부터 만들면서 하루에 수십 번의 테스트 실행을 반복할 때는 이 점이 매우 중요합니다.

계획은 모델을 설정 가능하게 (model configurable) 만드는 것입니다. 따라서 사용자는 자신의 **제공자 (provider)**와 **모델 (model)**을 선택하고 자신의 API 키를 직접 가져와 사용할 수 있습니다. 에이전트의 도구 호출 (tool-calling) 로직은 하위 모델이 함수/도구 호출 (function/tool calling)을 지원하기만 한다면 어떤 모델인지 상관하지 않습니다.

TUI: Ink (터미널을 위한 React)

터미널 UI는 [Ink]를 사용하여 구축되었습니다. 이를 통해 터미널에서 렌더링되는 React 컴포넌트를 작성할 수 있습니다. React를 알고 있다면 학습 곡선이 거의 없습니다.

저는 다음과 같은 용도로 Ink를 사용했습니다:

  • 파일 편집 중 차이점(diff) 표시 렌더링 (빨간색/초록색 승인 화면) +

  • 편집 도구의 승인 흐름(approval flow)부터 시작하세요. 이는 시스템의 가장 많은 부분(도구 스키마 (tool schema), 루프 제어 (loop control), TUI 상태 (TUI state), 비동기 조정 (async coordination))을 건드리며, 다른 모든 것의 패턴을 설정합니다.

  • 처음부터 파일 메타데이터를 포함하여 할 일 스키마 (todo schema)를 설계하세요. 나중에 병렬 에이전트 (parallel agents)를 지원하기 위해 이를 추가하려면 플래너 서브 에이전트 (planner sub-agent)의 프롬프트, 스키마, 그리고 메인 에이전트 (main agent)의 작업 선택 로직을 한꺼번에 수정해야 합니다.

  • 적절한 토큰 추적 디스플레이 (token tracking display)를 더 일찍 추가하세요. 현재는 향후 기능 목록에 포함되어 있습니다.

프로젝트 위치

GitHub 저장소는 여기 있습니다: https://github.com/subhraneel2005/sidequests
이 프로젝트를 다시 다듬기 시작했으며 몇 가지 개선 작업을 진행할 예정입니다. 저장소의 이슈 (issues) 섹션에서 확인하실 수 있으며, 제가 하는 모든 작업은 완전히 투명하게 공개될 것입니다.

만약 비슷한 것을 만들 계획이라면, 가장 좋은 시작 방법은 사용 중인 AI SDK에서 도구 호출 (tool-calling), 루프 (loops), 서브 에이전트 (sub-agents) 및 오케스트레이션 (orchestration)이 실제로 어떻게 작동하는지 읽어보는 것입니다. 나머지는 그 토대 위에 구축하는 것뿐입니다.

읽어주셔서 감사합니다

여기까지 읽어주셨다면 진심으로 감사드립니다. 이 프로젝트는 만드는 과정도 즐거웠고, 이에 대해 글을 쓰는 과정은 훨씬 더 즐거웠습니다.
저는 X에서 저의 프로젝트, 실험, 그리고 사이드 퀘스트 (side quests)에 대해 계속 게시물을 올리고 있습니다. 계속 지켜보고 싶으시다면 제 X(twitter) 계정을 팔로우해 주세요: @subhraneeltwt

생각이나 피드백, 비판, 질문이 있거나, 혹은 무언가 잘못되었거나 더 잘할 수 있는 방법이 있다면 DM을 보내거나, 댓글을 남기거나, 게시물을 인용하는 등 무엇이든 좋습니다. 저는 항상 배우고자 합니다. 공유하기에 너무 사소한 것은 없습니다. 다음에 만나요! :)

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0