본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 21. 23:12

내가 잠든 사이 Claude가 내 루틴을 수행하게 만들기!

요약

Claude CLI와 cron을 활용해 반복적인 개발 루틴을 자동화하려는 시도와 그 과정에서 겪은 기술적 한계를 다룹니다. 단순 스케줄링의 문제점인 인증 오류, 실행 중첩, 가시성 부족을 해결하기 위한 '루틴 데스크' 구축 경험을 공유합니다.

핵심 포인트

  • Claude CLI를 활용한 개발 루틴 자동화 가능성 확인
  • 단순 cron 스케줄링 시 발생하는 인증 및 타임아웃 문제
  • AI 작업의 비결정론적 특성에 따른 에러 핸들링 필요성
  • 실행 상태 모니터링 및 가시성 확보를 위한 설계의 중요성

갈증
매일 아침, 똑같은 의식. Claude를 열고, 최근의 Pull Request (PR)를 요약해달라고 요청하고, 차단 요소(blockers)를 확인하며, 스탠드업(standup) 미팅을 준비합니다. 매일 단 3분입니다. 그리 긴 시간은 아닙니다. 하지만 개발자를 괴롭히는 종류의 시간입니다. 반복적이고, 예측 가능하며, 기계적이죠. Claude에는 CLI (Command Line Interface)가 있습니다. CLI는 무인으로 실행될 수 있습니다. 만약 cron이 그냥... 저를 위해 이 일을 해준다면 어떨까요?

학습 루프 (Learning Loop)
지난 몇 달 동안 — 새로운 도구, 새로운 패턴, AI와 협업하는 새로운 방식들을 배우면서 — 저를 계속 놀라게 하는 것은 바로 속도입니다. AI 자체의 속도가 아니라, 개발 루프 (development loop)의 속도 말입니다. 아침 커피를 마시며 떠올린 아이디어가 그날 저녁 노트북을 닫을 때쯤이면 작동하는 프로토타입 (prototype)이 되어 있을 수 있습니다. 틈틈이 시간을 내어 일주일 내내 매달려야 했던 사이드 프로젝트 (side project)들이 이제는 단 한 번의 집중적인 세션 만에 구체화됩니다.

이것이 혁명적인 것은 아닙니다. 이미 많은 사람들이 자신만의 "자율적인 Claude (autonomous Claude)" 버전을 만들어 왔습니다. cron 래퍼 (wrappers), 커스텀 스케줄러 (custom schedulers), Claude Code 확장 프로그램, 혹은 본격적인 에이전트 프레임워크 (agent frameworks) 등이 있죠. 어떤 이들은 OpenClaw와 같은 기존 도구를 사용하고, 어떤 이들은 bash 스크립트를 작성하며, 또 어떤 이들은 정교한 멀티 에이전트 시스템 (multi-agent systems)을 구축합니다. 이 분야는 실험으로 가득 차 있으며, 아직 정해진 정답은 없습니다. 제가 여기서 공유하려는 것은 저만의 버전 — 간단한 대시보드를 통해 AI 작업을 정의, 예약 및 모니터링할 수 있는 "루틴 데스크 (routine desk)"입니다. 이것을 구축한 이야기, 제가 배운 것, 그리고 그것을 유용하게 만든 구체적인 설계 선택들에 대한 이야기입니다. 여러분의 버전은 다를 것이며, 그것이 핵심입니다. 흥미로운 부분은 결과물 그 자체가 아니라, 그 과정에서 여러분이 발견하게 되는 것들입니다.

가장 단순한 버전
첫 번째 시도는 정확히 여러분이 예상하는 그대로였습니다:
0 9 * * 1-5 claude -p "Summarize my recent code changes" > /tmp/standup.txt
작동했습니다! 어느 정도는요. 약 이틀 동안은 말이죠. 그러다 문제들이 쌓이기 시작했습니다. 월요일 아침: CLI가 밤사이 인증 문제 (authentication issue)에 부딪혔기 때문에 출력 파일이 비어 있었습니다. cron이 에러를 조용히 삼켜버린 것입니다.

화요일: 첫 번째 실행이 평소 3분이 아닌 8분이 걸리는 바람에 두 번의 실행이 겹쳤고, 첫 번째가 끝나기도 전에 두 번째가 예정된 시간에 시작되었습니다. 수요일쯤 되니 standup.txt, standup2.txt, standup-final.txt와 같은 임시 파일이 6개나 생겨 있었습니다... 어떤 상황인지 짐작이 가실 겁니다. 실행이 성공했는지 실패했는지 알 수 있는 가시성(visibility)이 없었습니다. 에러 처리(error handling)도 없었고, 히스토리(history)도 없었습니다. 시스템이 정상인지 고장 났는지 한눈에 파악할 방법도 없었습니다. cron은 결정론적인(deterministic) 명령어를 실행하는 데는 환상적인 도구입니다. 하지만 AI CLI 호출은 결정론적이지 않습니다. 멈춰 있거나(hang), 타임아웃(timeout)이 발생하거나, 예상치 못한 출력을 생성하거나, 혹은 조용히 실패할 수 있습니다. 저는 이를 이해할 수 있는 무언가가 필요했습니다. 작지만 제대로 된 무언가가 필요했습니다.

Markdown 형태의 루틴
모든 것을 결정지은 핵심 통찰은 다음과 같습니다: 루틴은 그저 프롬프트(prompt)에 스케줄링 메타데이터(scheduling metadata)를 더한 것에 불과하다는 점입니다. 그리고 이미 "구조화된 메타데이터 + 자유 형식의 텍스트"를 위한 완벽한 형식이 존재합니다. 바로 YAML 프론트매터(YAML frontmatter)를 포함한 Markdown입니다. 제 아침 스탠드업(standup) 루틴은 다음과 같이 생겼습니다:


title : " Morning Standup Summary"
schedule : " 0 0 9 * * 1-5 *"
model : sonnet
timeout : 300
max_turns : 50

아침 스탠드업 요약을 생성하세요. 다음을 수행하십시오:

  1. 최근 PRs : 지난 24시간 동안의 내 최근 풀 리퀘스트(pull requests)를 검색하세요. 각 항목의 제목, 상태, 그리고 한 줄 요약을 나열하세요.
  2. 진행 중인 작업 (Active Tasks) : 내 진행 중인 작업들을 검색하세요. 각 항목의 제목, 우선순위, 현재 상태를 나열하세요.
  3. 차단 요소 (Blockers) : 승인을 기다리는 코드 리뷰나 차단된 작업이 있는지 확인하세요.
  4. 오늘의 집중 과제 (Today's Focus) : 위 내용을 바탕으로 오늘을 위한 2~3가지 우선순위를 제안하세요.
    출력 형식은 팀 채팅에 게시하기 적합하도록 깔끔한 마크다운 요약으로 만드세요.

파일명이 곧 루틴의 이름이 됩니다. morning-standup.md 파일은 morning-standup이라는 루틴을 생성합니다. 디렉토리에 파일을 넣으면 스케줄이 등록되고, 파일을 삭제하면 중단됩니다. 프론트매터에서 enabled: false로 설정하면 파일을 삭제하지 않고도 일시 중지할 수 있습니다. 각 루틴은 자신만의 모델을 선택합니다.

Anthropic의 Claude 모델은 빠르고 저렴한 모델(Haiku)부터 강력하고 비용이 높은 모델(Sonnet, Opus)까지 다양하며, 서로 다른 작업에는 서로 다른 트레이드오프 (trade-offs)가 필요합니다. 저의 스탠드업 (standup) 루틴은 Sonnet을 사용합니다. 코드 변경 사항을 검색하고 보고서를 합성해야 하므로 추론 (reasoning) 품질이 중요하기 때문입니다. 반면 저의 헬스 체크 (health-check) 루틴은 Haiku를 사용합니다. 이는 빠른 상태 확인 (status ping)이며, 속도와 비용에 최적화되어 있습니다. Haiku 호출 비용은 Sonnet의 아주 일부분에 불과하며 몇 초 내에 결과를 반환합니다. 단지 "문제가 발생했나요? 예/아니오"라는 답변만 필요할 때는 가장 강력한 모델이 필요하지 않습니다.


title : Metrics Health Check
schedule : " 0 0 8 * * * *"
model : haiku
timeout : 120
max_turns : 30

주요 운영 지표에 대해 빠른 헬스 체크를 수행합니다...

피벗 (The Pivot)과 엔진 (The Engine)
원래 저는 이것을 TypeScript로 구축할 계획이었습니다. Claude Code SDK는 깔끔한 query() API를 제공하고, node-cron은 스케줄링을 처리하며, node:sqlite는 영속성 (persistence)을 처리합니다. 참고할 수 있는 구현체도 있었습니다. 정확히 동일한 스택으로 구축된 리뷰 에이전트 (review agent)가 있었죠. 저는 구축을 시작했습니다. 그러다 벽에 부딪혔습니다. 보안이 강화된 저의 개발 환경에서는 패키지 레지스트리 (package registry)에 접근할 수 없었습니다. 패키지를 설치하기 위한 외부 네트워크 접근이 차단되어 있었습니다. npm install도 안 되고, 의존성 (dependencies)도 설치할 수 없는 막다른 길이었죠.

그래서 저는 피벗 (pivot)했습니다. 전체를 Rust로 다시 작성했습니다. 제약 사항으로 시작된 것이 프로젝트의 가장 최고의 아키텍처 결정이 되었습니다. 최종 결과물은 단일 바이너리 (single binary)입니다. Tokio를 통한 비동기 (async) 처리, rusqlite를 통한 SQLite 임베디드 (embedded), 그리고 런타임 의존성 (runtime dependencies) 제로를 실현했습니다. 어떤 머신에든 복사해서 바로 실행할 수 있습니다. node_modules도, 패키지 매니저 (package manager)도, 맞출 필요가 있는 런타임 버전도 없습니다. 스케줄러 (scheduler), 실행기 (executor), 데이터베이스 (database), 그리고 대시보드 (dashboard)를 포함하는 단 하나의 파일일 뿐입니다. 돌이켜보면, 이는 사람이 개입하지 않고 실행되어야 하는 인프라 (infrastructure)에 정확히 필요한 형태입니다.

실행기 (The Executor)는 핵심 루프 (core loop)입니다. 이는 claude CLI를 서브프로세스 (subprocess)로 생성하고, tokio::select!를 통해 stdout과 stderr를 동시에 스트리밍합니다.

, 메시지가 스트리밍되는 동안 메시지 수와 도구 사용 (tool use) 횟수를 계산하며, 엄격한 타임아웃 (hard timeout)을 적용합니다: let result = timeout ( timeout_duration , async { let mut child = cmd .spawn () ? ; let mut stdout_reader = BufReader :: new ( child .stdout .take () ? ) .lines (); let mut stderr_reader = BufReader :: new ( child .stderr .take () ? ) .lines (); loop { tokio :: select! { line = stdout_reader .next_line () => { match line { Ok ( Some ( line )) => { message_count += 1 ; if line .contains ( "tool_use" ) { tool_use_count += 1 ; } output_lines .push ( line ); } Ok ( None ) => break , Err ( _ ) => break , } } line = stderr_reader .next_line () => { /* log and continue */ } } } // ... }) .await ; 스케줄러 (Scheduler)는 루틴 (routine)당 하나의 비동기 태스크 (async task)를 생성합니다. 각 태스크는 자신의 cron 표현식을 파싱하고, chrono::Utc::now()를 사용하여 다음 트리거까지의 지속 시간을 계산한 뒤, 정확히 그 시간 동안 대기(sleep)했다가 실행기 (executor)를 작동시킵니다. 이는 각 루틴이 자신만의 Tokio 태스크에서 실행됨을 의미합니다. 즉, 중앙 집중식 폴링 루프 (polling loop), 우선순위 큐 (priority queue), 또는 타이머 휠 (timer wheel)이 없습니다. 각 루틴은 스스로를 깨우는 책임을 독립적으로 집니다. 중복 실행 방지 (Overlap protection)는 뮤텍스 (mutex) 뒤에 있는 HashSet을 사용합니다. 실행하기 전에 루틴 이름이 세트에 있는지 확인합니다. 만약 있다면, 이번 틱 (tick)을 건너뜁니다. 가장 흔한 cron 실수 (footgun)를 방지하는 다섯 줄의 코드입니다: { let mut locks = locks .lock () .unwrap (); if locks .contains ( & name ) { tracing :: warn! ( routine = % name , "Already running, skipping tick" ); continue ; } locks .insert ( name .clone ()); } 충돌 복구 (Crash recovery) 또한 매우 최소한으로 구현되었습니다. 시작 시, cleanup_stale_runs() 함수가 SQLite를 쿼리하여 상태가 running 또는 pending인 실행 건을 찾아 failed로 표시합니다. 만약 프로세스가 실행 도중 충돌했다면, 이 기능 없이는 해당 기록들이 영원히 멈춰있게 될 것입니다. 비정상 종료를 처리하는 다섯 줄의 코드입니다. 핫 리로드 (Hot-reload)는 notify 크레이트 (crate)를 사용하여 routines/ 디렉토리를 감시합니다. .md 파일이 변경되면, 감시자 (watcher)가 모든 루틴을 다시 로드하고 스케줄러를 업데이트합니다.

주목할 만한 실용적인 해킹(hack)이 하나 있습니다: std::mem::forget(watcher) — watcher를 누출(leaking)시켜 설정 함수가 끝날 때 드롭(drop)되는 대신 프로그램의 수명 동안 유지되도록 하는 것입니다. "제대로 된" 코드베이스라면 watcher 핸들을 어딘가에 저장해 두었다가 종료 시점에 드롭해야 합니다. 하지만 여기서는 프로그램을 직접 종료할 때까지 실행되므로, 누출시키는 것이 기능적으로 올바르며 복잡한 수명(lifetime) 관련 기교를 줄여줍니다. 순수성보다는 실용성을 택한 것입니다. 전체 엔진 — 기반(foundation), 스케줄링(scheduling), 출력 처리(output handling), 대시보드(dashboard), 다듬기(polish) — 은 단 한 번의 앉은 자리에서 구축되어 작동했습니다. 이것이 제가 서두에서 언급한 압축된 개발 루프(compressed development loop)입니다. 몇 주가 걸릴 사이드 프로젝트가 Claude를 페어 프로그래머(pair programmer)로 활용하여 집중적인 저녁 시간 한 번 만에 구체화되었습니다. Rust의 컴파일러는 TypeScript였다면 런타임(runtime)에 발생했을 버그 카테고리 전체를 컴파일 타임(compile time)에 잡아냈습니다. 빌림 검사기(borrow checker)는 비동기 스케줄러(async scheduler)에서 데이터 경합(data race)으로부터 당신을 구해줄 때까지는 짜증스럽지만, 그때가 되면 당신의 가장 친한 친구가 됩니다.

┌──────────────────────────────────────────────────────────────┐
│ Claude Routines Engine │
│ │
│ ┌──────────┐ ┌────────────┐ ┌──────────────────────┐
│ │ │ File │ │ │
│ │ │ Watcher │───►│ Scheduler │───►│ ┌────────────────┐ │
│ │ │ (notify) │ │ (cron) │ │ │ claude -p "..." │ │
│ │ │ └──────────┘ │ │ │ --model sonnet │ │
│ │ │ │ │ │ │ --max-turns 50 │ │
│ │ │ │ │ │ │ ▲ │
│ │ │ │ │ │ │ ┌────────┐ │
│ │ │ │ │ │ │ │Overlap │ │
│ │ │ │ │ │ │ │ Lock │ │
│ │ │ │ │ │ │ └────────┬───────┘ │
│ │ │ routines/*.md │ │ │ │
│ │ │ │ │ │ │ │ └────────┘ │
│ │ │ │ │ │ │ stdout/stderr │ │
│ │ │ └────────────┘ └──────────┬─────────┘ │
│ │ │ │ │ ┌────────────┐ ┌──────────▼─────────┐ │
│ │ │ │ │ │ Dashboard │ │ SQLite Store │ │
│ │ │ │ │ │ :3456 │◄──►│ (WAL mode) │ │
│ │ │ │ │ │ runs, status, │ │
│ │ │ │ │ │ crash recovery │ │
│ │ │ │ │ └────────────────────┘ │
│ └──────────────────────────────────────────────────────────────┘

대시보드 (The Dashboard)
대시보드는 가공되지 않은 TCP를 기반으로 구축되었습니다 — 웹 프레임워크(web framework)도, Axum도, Actix도 사용하지 않았습니다. HTTP 요청을 수동으로 파싱하고, 라우팅하며, 전체 HTML을 Rust 문자열로 생성하는 단 하나의 파일로 구성됩니다.

TcpListener::bind, 연결당 tokio::spawn, 그리고 (method, path)에 대한 pattern-match. 이것이 웹 서버의 전부입니다. 왜 프레임워크를 사용하지 않았을까요? 의존성(dependency)을 추가한다는 것은 복잡성을 추가한다는 의미이며, 이 대시보드는 정확히 네 가지 기능만 수행하면 되었기 때문입니다: 루틴 카드 표시, 실행 이력 표시, 로그 표시, 그리고 수동 실행 트리거. 프레임워크를 사용하는 것이 아키텍처적으로는 옳겠지만, 실질적으로는 과잉(overkill)이었습니다. 이 시스템은 다크 모드 테마의 싱글 페이지 대시보드 역할을 합니다. 상태 표시 점(마지막 실행 성공 시 녹색, 실패 시 빨간색, 실행된 적 없음 시 회색)이 포함된 루틴 카드, 각 실행의 시간과 상태를 보여주는 실행 이력 테이블, 그리고 모든 실행에 대한 Claude의 원시 출력(raw output)을 보여주는 로그 뷰어로 구성됩니다. 모든 것은 서버 사이드(server-side)에서 렌더링됩니다. 즉, HTML은 거대한 Rust 문자열 보간(string interpolation)입니다. 클라이언트 사이드 프레임워크도, 하이드레이션(hydration)도, 빌드 단계도 없습니다. 핵심 기능은 "새 루틴(New Routine)" 버튼입니다. 이 버튼을 누르면 루틴(제목, 일정, 모델, 프롬프트)을 정의하는 모달(modal)이 열리며, 저장 버튼을 누르면 /api/routines로 POST 요청을 보냅니다. 서버는 routines 디렉토리에 .md 파일을 작성합니다. 파일 와처(file watcher)가 새 파일을 감지합니다. 스케줄러(scheduler)가 이를 가져와 실행을 시작합니다. 시스템이 브라우저로부터 스스로 확장되는 것입니다. 새로운 루틴을 추가하기 위해 SSH 접속이나 텍스트 에디터가 필요하지 않습니다. 그저 브라우저와 Claude가 다음에 무엇을 해야 할지에 대한 아이디어만 있으면 됩니다.

브라우저 ──POST /api/routines──► 대시보드 서버 │ metrics-check.md 작성 │ ▼ routines/ 디렉토리 │ 파일 와처 감지 │ ▼ 스케줄러가 새 루틴을 다시 로드 및 스케줄링

CDN도, 빌드 단계도, 외부 CSS도 없습니다. setInterval을 사용하여 30초마다 자동 새로고침을 수행하며, 모달이 열려 있을 때는 편집 중인 양식이 사라지지 않도록 새로고침을 일시 중지합니다. CSS, JavaScript, HTML 템플릿을 포함한 전체 UI는 바이너리(binary) 내에 자체적으로 포함되어 있습니다. cargo build --release 명령만 실행하면 모든 것이 준비됩니다.

"아하(Aha)" 모먼트
커피를 만드는 동안 오전 9시에 이미 생성되어 있는 스탠드업 요약본을 보며 잠에서 깨어난 첫날 아침, 무언가 변화가 느껴졌습니다. 저는 Claude를 열어 무언가를 시키지 않았습니다. Claude는 이미 그것을 해냈습니다.

제 지표 확인 (metrics check) 작업은 오전 8시에 실행되었고, 제가 노트북을 열기도 전에 문제를 포착했습니다. 알림은 이미 와 있었습니다. 컨텍스트 (context)도 갖춰져 있었습니다. 저는 그저 조치를 취하기만 하면 되었습니다. 객관적으로 보면 이것은 작은 일입니다. Claude를 호출하는 크론 잡 (cron job)일 뿐이죠. 하지만 주관적으로는 느낌이 다릅니다. 정신적인 전환은 "내가 Claude를 사용한다"에서 "Claude가 나를 위해 일한다"로 바뀝니다. 이 도구는 에이전시 (agency)를 가집니다. 제한적이고, 예정되어 있으며, 관찰 가능한 에이전시이지만, 어쨌든 에이전시입니다. 그리고 이것은 당신이 자신의 시간을 생각하는 방식을 변화시킵니다. 매일 아침 스탠드업 (standup)을 준비하며 보냈던 그 3분? 사라졌습니다. 하지만 단순히 3분을 아낀 것이 아닙니다. 그것을 해야 한다는 것을 기억해야 하는 인지적 오버헤드 (cognitive overhead), 새로운 세션을 여는 데 드는 컨텍스트 스위칭 (context-switching) 비용, 그리고 백 번째 똑같은 프롬프트 (prompt)를 작성해야 하는 마찰 (friction)까지 모두 포함된 것입니다. 그 모든 것이 증발합니다. 당신은 그저... 요약본을 갖게 됩니다. 노트북을 열면 작업은 이미 완료되어 있습니다. 복리 효과도 있습니다. 일단 시스템이 구축되면, 새로운 루틴을 추가하는 한계 비용 (marginal cost)은 기본적으로 제로에 가깝습니다. 마크다운 (markdown) 파일을 작성하고, 그것을 넣기만 하면...

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0