# 클로드 코드 루프로 매일 아침 자동으로 도는 작업 만들기

> 클로드 코드의 루프 네 가지 유형을 정리하고, 그중 시간 기반 루프를 macOS launchd로 구현해 매일 아침 사람 없이 스스로 갱신되는 팀 대시보드를 만든 실제 사례를, 왜 /loop이 아니라 launchd인지와 실패를 스스로 고치는 워치독까지 함께 정리했습니다.

- 발행: 2026-07-07
- 주제: 리포트 & 업무 자동화
- 발행처: 매드업 마케팅사업본부 AI 마케팅 개발실
- 웹 원문: https://madobi.madup.com/blog/claude-code-loops-launchd/

## 세 줄 요약

- 클로드 코드에서 루프는 에이전트가 정지 조건을 만족할 때까지 작업 사이클을 반복하는 것이다. 매번 새 프롬프트를 쓰는 대신 언제 시작하고 언제 멈출지를 설계해 작업을 맡기는 개념으로, 트리거와 종료 조건에 따라 턴 기반·목표 기반·시간 기반·프로액티브 네 가지로 나뉜다.
- 시간 기반 루프는 /loop이나 /schedule로 만들 수 있지만, 로컬 자원이 필요한 무인 작업은 macOS launchd 같은 OS 스케줄러로 구현하는 편이 맞다. /loop은 터미널 세션이 닫히면 함께 끝나고, /schedule은 클라우드에서 돌기 때문이다.
- 무인 루프의 성공은 종료 코드로 판정하면 위험하다. headless claude가 API 에러로 죽어도 종료 코드가 0인 경우가 있어서, 오늘 산출물이 새로 갱신됐는지 신선도로 확인하는 편이 정확하다.
- 무인 파이프라인은 실패한 날 아침이 진짜 비용이 나는 순간이다. 그래서 산출물 신선도부터 확인하는 워치독을 루프 끝에 붙이고, 이상이 있을 때만 진단 에이전트를 깨워 증거가 있는 부분만 다시 돌리게 하면 사람 개입 없이도 대부분의 아침을 스스로 넘긴다.

---
데이터솔루션팀 대시보드는 매일 아침 담당자가 출근하기 전에 이미 그날 것으로 갱신돼
있습니다. 아무도 버튼을 누르지 않았는데도요. 밤사이 Jira와 Slack에서 데이터가 모이고,
팀 주간 요약과 휴가 현황, 개인별 AI 활용 진단이 새로 그려지고, 화면이 빌드돼 배포까지
끝나 있습니다. 이걸 돌리는 건 클로드 코드의 루프입니다.

클로드 코드에서 루프는 같은 작업을 반복해서 시킬 때, 매번 프롬프트를 새로 쓰는 대신
언제 시작하고 언제 멈출지를 정해 두고 맡기는 방식입니다. 위 대시보드가 바로 그렇게
돕니다.

<img src="/madobi/thinking.png" alt="어떤 루프를 쓸지 고민하는 매도비" width="140" height="140" loading="lazy" style="display:block;margin:1.5rem auto 0;" />
<p style="text-align:center;color:#6b6e7b;font-size:0.85rem;margin-top:0.4rem;">어떤 일에 어떤 루프를 맞출지부터가 시작입니다.</p>

## 클로드 코드에서 루프란 무엇인가

클로드 코드 팀은 루프를 에이전트가 정지 조건을 만족할 때까지 작업 사이클을 반복하는
것으로 정의합니다. 에이전트는 지시를 받아 스스로 도구를 쓰고 중간 결과를 확인하며
일하는 AI입니다. 답 한 줄을 내놓고 끝나지 않고, 파일을 열어 읽고 명령을 실행하고 그
결과를 보고서 다음 수를 정합니다.

매번 프롬프트를 새로 쓰는 대신 언제 시작하고 언제 멈출지를 설계해 맡긴다는 게
루프의 요점입니다. 무엇이 한 바퀴를 시작시키는지, 언제 멈추는지를 어떻게 두느냐에
따라 크게 네 가지 패턴으로 나눠 볼 수 있습니다. 명령과 일대일로 딱 떨어지는 분류라기보다,
자동화를 설계할 때 무엇이 다음 실행을 밀고 무엇을 보고 멈추는지로 가르는 실무 구분입니다.

## 클로드 코드 루프의 네 가지 유형

턴 기반(Turn-based)이 기본입니다. 프롬프트를 한 번 보내면 한 바퀴가 시작되고,
Claude가 다 됐다고 보거나 맥락이 더 필요할 때 멈춥니다. 좋아요 버튼 하나 만들기처럼
한 번 하고 마는 짧은 작업이 여기 해당합니다. 검증 절차를 스킬 문서로 적어 두면
결과를 넘기기 전에 Claude가 스스로 확인합니다.

목표 기반(/goal)은 완료 여부를 확인할 수 있을 때 씁니다. `/goal 홈페이지 Lighthouse
점수 90 이상, 5번 시도 후 중단`처럼 목표와 멈출 조건을 함께 주면, 거기 닿거나 시도
한도가 찰 때까지 반복합니다. 점수만이 아니라 테스트 통과, 처리할 큐 비움처럼 Claude가
결과로 확인할 수 있는 조건이면 됩니다. 애매한 "잘 만들어줘"보다 검증할 수 있는 조건일수록
잘 맞습니다.

시간 기반(/loop, /schedule)은 정해진 간격이 한 바퀴를 시작시킵니다. `/loop 5m PR
확인하고 리뷰 코멘트 반영, CI 실패 수정`이 그런 예입니다. 고정 간격으로 걸면 취소하거나
만료될 때까지 돌고, 처리할 일이 없어지면(PR이 머지되거나 큐가 비면) 멈추게 조건을 걸
수도 있습니다. /loop은 지금 열린 로컬 세션에서 돌고, /schedule은 같은 반복을 Anthropic이
관리하는 클라우드 루틴으로 올려 돌립니다.

프로액티브(Proactive)는 사람이 실시간으로 붙어 있지 않고 이벤트나 스케줄에 맡깁니다.
버그 리포트 처리, 이슈 트리아지, 의존성 업그레이드처럼 손이 익은 반복 업무에 씁니다.
앞의 세 방식에 다이나믹 워크플로우와 오토 모드를 얹어 구성합니다.

<figure style="margin:1.75rem 0;overflow-x:auto;">
<svg viewBox="0 0 720 300" role="img" aria-label="클로드 코드 루프 네 가지 유형 비교. 턴 기반: 트리거는 사용자 프롬프트, 종료는 Claude가 완료로 판단, 짧은 일회성 작업. 목표 기반(/goal): 트리거는 프롬프트, 종료는 목표 달성 또는 시도 한도, 완료를 확인할 수 있을 때. 시간 기반(/loop, /schedule, OS 스케줄러): 트리거는 정해진 간격, 종료는 취소나 작업 완료, 반복·모니터링. 프로액티브: 트리거는 이벤트나 스케줄, 사람 개입 없음, 잘 정의된 반복 업무." style="width:100%;min-width:600px;height:auto;font-family:inherit;">
  <g>
    <rect x="6" y="8" width="348" height="136" rx="12" fill="none" stroke="#3e4a78" stroke-width="1.5"/>
    <text x="24" y="36" fill="#2a2d3e" font-size="15" font-weight="700">턴 기반</text>
    <text x="24" y="55" fill="#6b6e7b" font-size="11">Turn-based · 기본</text>
    <text x="24" y="82" fill="#c7303b" font-size="11" font-weight="700">트리거</text><text x="80" y="82" fill="#2a2d3e" font-size="11.5">프롬프트 한 번</text>
    <text x="24" y="103" fill="#c7303b" font-size="11" font-weight="700">종료</text><text x="80" y="103" fill="#2a2d3e" font-size="11.5">Claude가 완료로 판단</text>
    <text x="24" y="128" fill="#6b6e7b" font-size="11.5">짧은 일회성 작업</text>
  </g>
  <g>
    <rect x="366" y="8" width="348" height="136" rx="12" fill="none" stroke="#3e4a78" stroke-width="1.5"/>
    <text x="384" y="36" fill="#2a2d3e" font-size="15" font-weight="700">목표 기반</text>
    <text x="384" y="55" fill="#6b6e7b" font-size="11">/goal</text>
    <text x="384" y="82" fill="#c7303b" font-size="11" font-weight="700">트리거</text><text x="440" y="82" fill="#2a2d3e" font-size="11.5">프롬프트 + 목표·시도수</text>
    <text x="384" y="103" fill="#c7303b" font-size="11" font-weight="700">종료</text><text x="440" y="103" fill="#2a2d3e" font-size="11.5">목표 달성 또는 시도 한도</text>
    <text x="384" y="128" fill="#6b6e7b" font-size="11.5">완료를 확인할 수 있을 때</text>
  </g>
  <g>
    <rect x="6" y="156" width="348" height="136" rx="12" fill="#f5be4b" fill-opacity="0.1" stroke="#e23d48" stroke-width="1.8"/>
    <text x="24" y="184" fill="#2a2d3e" font-size="15" font-weight="700">시간 기반</text>
    <text x="24" y="203" fill="#c7303b" font-size="11" font-weight="700">/loop · /schedule · OS 스케줄러 — 이 글의 사례</text>
    <text x="24" y="230" fill="#c7303b" font-size="11" font-weight="700">트리거</text><text x="80" y="230" fill="#2a2d3e" font-size="11.5">정해진 시간 간격</text>
    <text x="24" y="251" fill="#c7303b" font-size="11" font-weight="700">종료</text><text x="80" y="251" fill="#2a2d3e" font-size="11.5">취소하거나 일이 끝날 때</text>
    <text x="24" y="276" fill="#6b6e7b" font-size="11.5">반복 작업 · 외부 시스템 감시</text>
  </g>
  <g>
    <rect x="366" y="156" width="348" height="136" rx="12" fill="none" stroke="#3e4a78" stroke-width="1.5"/>
    <text x="384" y="184" fill="#2a2d3e" font-size="15" font-weight="700">프로액티브</text>
    <text x="384" y="203" fill="#6b6e7b" font-size="11">Proactive · 사람 실시간 개입 없음</text>
    <text x="384" y="230" fill="#c7303b" font-size="11" font-weight="700">트리거</text><text x="440" y="230" fill="#2a2d3e" font-size="11.5">이벤트 또는 스케줄</text>
    <text x="384" y="251" fill="#c7303b" font-size="11" font-weight="700">구성</text><text x="440" y="251" fill="#2a2d3e" font-size="11.5">앞의 셋 + 워크플로우·오토</text>
    <text x="384" y="276" fill="#6b6e7b" font-size="11.5">잘 정의된 반복 업무</text>
  </g>
</svg>
</figure>

이 대시보드는 세 번째, 시간 기반 루프입니다. 다만 /loop이나 /schedule을 쓰지
않았습니다.

## 매일 아침 파이프라인은 무슨 일을 하나

대시보드가 갱신되기까지 아침에 다섯 단계가 순서대로 돕니다.

- 평일 08:00, 코드가 Jira와 Slack에서 원본 데이터를 긁어 오고, headless claude가 에픽별 타임라인과 팀 주간 요약으로 정리합니다.
- 평일 08:30, Claude가 Slack 연차 스레드를 읽어 휴가 데이터를 갱신합니다. Slack에는 Slack MCP로 붙습니다.
- 평일 08:45, 정량 지표를 근거로 개인별 AI 활용 진단 코멘트를 갱신합니다.
- 매일 09:00, 파이썬이 정량 데이터를 모으고 웹을 빌드해 배포합니다. 앞 단계들이 만든 것이 이때 화면에 올라갑니다.
- 매일 09:40, 워치독이 오늘 산출물이 다 신선한지 보고, 이상하면 진단 에이전트를 깨웁니다.

headless claude는 사람이 보는 화면 없이 뒤에서 도는 클로드 코드입니다. 평소에는 창을
띄우고 대화하지만, 아침에는 아무도 모니터 앞에 없으니 명령으로만 실행합니다. Slack이나
Jira 같은 외부 도구에 붙는 통로가 MCP고, 08:30이 연차 스레드를 직접 읽는 것도 이
연결 덕입니다.

<figure style="margin:1.75rem 0;overflow-x:auto;">
<svg viewBox="0 0 760 150" role="img" aria-label="매일 아침 파이프라인 타임라인: 08:00 히스토리 수집·정리, 08:30 연차 갱신, 08:45 AI 진단, 09:00 빌드·배포, 09:40 워치독 점검 순서로 이어지고, 09:40에서 이상이 감지되면 진단 에이전트가 가동된다." style="width:100%;min-width:660px;height:auto;font-family:inherit;">
  <line x1="40" y1="58" x2="720" y2="58" stroke="#3e4a78" stroke-width="1.5"/>
  <g text-anchor="middle">
    <circle cx="80" cy="58" r="5" fill="#3e4a78"/>
    <text x="80" y="42" fill="#c7303b" font-size="12" font-weight="700">08:00</text>
    <text x="80" y="82" fill="#2a2d3e" font-size="12" font-weight="600">히스토리</text>
    <text x="80" y="98" fill="#6b6e7b" font-size="10.5">Jira·Slack 수집</text>
    <circle cx="230" cy="58" r="5" fill="#3e4a78"/>
    <text x="230" y="42" fill="#c7303b" font-size="12" font-weight="700">08:30</text>
    <text x="230" y="82" fill="#2a2d3e" font-size="12" font-weight="600">연차</text>
    <text x="230" y="98" fill="#6b6e7b" font-size="10.5">Slack MCP</text>
    <circle cx="380" cy="58" r="5" fill="#3e4a78"/>
    <text x="380" y="42" fill="#c7303b" font-size="12" font-weight="700">08:45</text>
    <text x="380" y="82" fill="#2a2d3e" font-size="12" font-weight="600">AI 진단</text>
    <text x="380" y="98" fill="#6b6e7b" font-size="10.5">정량 지표 기반</text>
    <circle cx="530" cy="58" r="5" fill="#3e4a78"/>
    <text x="530" y="42" fill="#c7303b" font-size="12" font-weight="700">09:00</text>
    <text x="530" y="82" fill="#2a2d3e" font-size="12" font-weight="600">빌드·배포</text>
    <text x="530" y="98" fill="#6b6e7b" font-size="10.5">웹 빌드·배포</text>
    <circle cx="690" cy="58" r="5" fill="#e23d48"/>
    <text x="690" y="42" fill="#c7303b" font-size="12" font-weight="700">09:40</text>
    <text x="690" y="82" fill="#2a2d3e" font-size="12" font-weight="600">워치독</text>
    <text x="690" y="98" fill="#6b6e7b" font-size="10.5">신선도 점검</text>
  </g>
  <path d="M690 108 V128 H600" stroke="#e23d48" stroke-width="1.5" fill="none" stroke-dasharray="4 3"/>
  <path d="M608 122 L600 128 L608 134" stroke="#e23d48" stroke-width="1.5" fill="none"/>
  <text x="470" y="132" text-anchor="middle" fill="#c7303b" font-size="10.5" font-weight="600">이상 감지 시 진단 에이전트 가동</text>
</svg>
</figure>

## 왜 클로드 코드 스케줄러가 아니라 launchd인가

클로드 코드가 시간 기반 루프를 여러 방식으로 지원하는데도 굳이 launchd를 쓴 데는
이유가 있습니다. launchd는 macOS가 정해진 시각에 프로그램을 자동으로 실행해 주는
OS 스케줄러입니다.

/loop부터 걸립니다. /loop은 지금 살아 있는 세션 안의 반복이라, 터미널을 닫으면 루프도
끝납니다. 밤새 터미널이 켜져 있으리란 보장이 없으니 매일 아침 무인 실행에는 못
맡깁니다.

/schedule은 클라우드 루틴으로 올라갑니다. 그런데 이 파이프라인은 클라우드에 없는
것들을 필요로 합니다. 접근하려는 외부 시스템이 사내망만 허용해서 사내망 IP로 나가야
하고, 자격 증명은 이 컴퓨터에 있고, 빌드와 배포 환경도 여기 있습니다. 클라우드로는
이 중 어느 것도 성립하지 않습니다.

클로드 코드에는 로컬 컴퓨터에서 도는 예약 실행도 있습니다. 로컬 파일과 도구를 쓸 수
있으니 후보이긴 했습니다. 다만 이 사례는 여러 셸 스크립트와 파이썬 빌드, 배포, 로그
확인을 하나의 OS 작업 묶음으로 관리해야 했습니다. 그래서 클로드 코드 안의 스케줄러에
전부 담기보다, macOS의 launchd를 바깥 트리거로 두고 필요한 순간에만 headless claude를
부르는 쪽이 단순했습니다.

결국 트리거를 어디 둘지는 작업이 무엇을 필요로 하느냐를 따라갑니다. 로컬에 있는
것을 써야 하면 OS 스케줄러, 클라우드에서 지켜보기만 하면 되는 일이면 /schedule입니다.
수집과 판단을 나눌 수 있다면 섞어 쓰는 길도 있습니다. 사내망 안에서 필요한 데이터를
먼저 산출물로 만들고 민감한 값을 걸러낸 다음, 그 정리를 클라우드 루틴에 맡기는
식입니다. 이 사례는 원본 접근부터 정리, 빌드, 배포가 한 로컬 환경에 묶여 있어 launchd로
한데 뒀습니다.

<figure style="margin:1.75rem 0;overflow-x:auto;">
<svg viewBox="0 0 680 250" role="img" aria-label="트리거 선택 결정 흐름. 시작 질문: 작업이 로컬 자원(사내망 IP·로컬 자격 증명·빌드/배포 환경)을 필요로 하는가. 예이면 OS 스케줄러(launchd)로 로컬에서 무인 실행. 아니오이면 다음 질문: 세션이 닫혀도 계속 돌아야 하는가. 예이면 클라우드 실행(/schedule). 아니오이면 로컬 세션 반복(/loop)." style="width:100%;min-width:600px;height:auto;font-family:inherit;">
  <rect x="180" y="8" width="320" height="46" rx="10" fill="#3e4a78" fill-opacity="0.08" stroke="#3e4a78" stroke-width="1.5"/>
  <text x="340" y="30" text-anchor="middle" fill="#2a2d3e" font-size="12.5" font-weight="700">로컬 자원이 필요한가?</text>
  <text x="340" y="46" text-anchor="middle" fill="#6b6e7b" font-size="10.5">사내망 IP · 로컬 자격 증명 · 빌드/배포 환경</text>
  <path d="M180 31 H60 V96" stroke="#e23d48" stroke-width="1.5" fill="none"/><path d="M55 88 L60 98 L65 88" stroke="#e23d48" stroke-width="1.5" fill="none"/>
  <text x="120" y="24" text-anchor="middle" fill="#c7303b" font-size="11" font-weight="700">예</text>
  <rect x="8" y="98" width="200" height="60" rx="10" fill="#f5be4b" fill-opacity="0.14" stroke="#e23d48" stroke-width="1.8"/>
  <text x="108" y="124" text-anchor="middle" fill="#2a2d3e" font-size="13" font-weight="700">OS 스케줄러 (launchd)</text>
  <text x="108" y="143" text-anchor="middle" fill="#6b6e7b" font-size="11">로컬에서 무인 실행 · 이 글의 사례</text>
  <path d="M340 54 V96" stroke="#3e4a78" stroke-width="1.5" fill="none"/><path d="M335 88 L340 98 L345 88" stroke="#3e4a78" stroke-width="1.5" fill="none"/>
  <text x="356" y="80" text-anchor="middle" fill="#6b6e7b" font-size="11">아니오</text>
  <rect x="200" y="98" width="280" height="46" rx="10" fill="#3e4a78" fill-opacity="0.08" stroke="#3e4a78" stroke-width="1.5"/>
  <text x="340" y="120" text-anchor="middle" fill="#2a2d3e" font-size="12.5" font-weight="700">세션이 닫혀도 계속 돌아야 하는가?</text>
  <text x="340" y="136" text-anchor="middle" fill="#6b6e7b" font-size="10.5">밤새·주말에도 무인으로</text>
  <path d="M480 121 H600 V186" stroke="#3e4a78" stroke-width="1.5" fill="none"/><path d="M595 178 L600 188 L605 178" stroke="#3e4a78" stroke-width="1.5" fill="none"/>
  <text x="560" y="114" text-anchor="middle" fill="#6b6e7b" font-size="11">예</text>
  <rect x="500" y="188" width="172" height="56" rx="10" fill="none" stroke="#3e4a78" stroke-width="1.5"/>
  <text x="586" y="212" text-anchor="middle" fill="#2a2d3e" font-size="13" font-weight="700">/schedule</text>
  <text x="586" y="230" text-anchor="middle" fill="#6b6e7b" font-size="11">클라우드에서 실행</text>
  <path d="M340 144 V186" stroke="#3e4a78" stroke-width="1.5" fill="none"/><path d="M335 178 L340 188 L345 178" stroke="#3e4a78" stroke-width="1.5" fill="none"/>
  <text x="356" y="170" text-anchor="middle" fill="#6b6e7b" font-size="11">아니오</text>
  <rect x="254" y="188" width="172" height="56" rx="10" fill="none" stroke="#3e4a78" stroke-width="1.5"/>
  <text x="340" y="212" text-anchor="middle" fill="#2a2d3e" font-size="13" font-weight="700">/loop</text>
  <text x="340" y="230" text-anchor="middle" fill="#6b6e7b" font-size="11">열린 로컬 세션에서 반복</text>
</svg>
</figure>

launchd로 스케줄을 걸면 손으로는 되던 게 자동 실행에서만 실패하는 일이 흔합니다.
launchd는 사람이 여는 터미널과 달리 로그인 셸의 프로필을 읽지 않아서, `.zshrc`
같은 데서 잡히던 PATH나 API 토큰이 비어 있습니다. 그래서 스크립트 안에서 PATH를
직접 지정하고, 토큰이 든 설정 파일을 명시적으로 불러옵니다.

## 성공과 실패를 무엇으로 판정하나

무인 실행이 성공했는지 어떻게 알까요. 흔히 종료 코드를 봅니다. 스크립트가 0으로
끝났으면 성공으로 치는 방식인데, 이 파이프라인은 종료 코드를 믿지 않습니다.

headless claude가 API 에러로 죽었는데도 종료 코드가 0으로 찍힌 적이 있어서입니다.
그 숫자만 봤다면 그날을 성공으로 기록했겠지만, 대시보드에는 어제 것이 그대로 걸려
있었습니다. 아침마다 사람이 화면을 눈으로 대조하지 않으니 이런 조용한 실패는 한참
뒤에야 들통납니다.

<figure style="margin:1.75rem 0;overflow-x:auto;">
<svg viewBox="0 0 680 176" role="img" aria-label="같은 실행을 두 기준으로 본 대비. 종료 코드로 보면 exit 0이라 성공으로 판정하지만, 산출물 신선도로 보면 결과 파일이 어제 날짜 그대로라 실패로 판정한다. 종료 코드만 믿으면 조용한 실패를 놓친다." style="width:100%;min-width:600px;height:auto;font-family:inherit;">
  <rect x="8" y="8" width="324" height="160" rx="12" fill="none" stroke="#6b6e7b" stroke-width="1.5" stroke-dasharray="5 4"/>
  <text x="170" y="34" text-anchor="middle" fill="#6b6e7b" font-size="13" font-weight="700">종료 코드로 보면</text>
  <text x="170" y="82" text-anchor="middle" fill="#2a2d3e" font-size="22" font-weight="700">exit 0</text>
  <text x="170" y="112" text-anchor="middle" fill="#6b6e7b" font-size="12">스크립트는 정상 종료</text>
  <rect x="106" y="128" width="128" height="28" rx="14" fill="#6b6e7b" fill-opacity="0.12" stroke="#6b6e7b" stroke-width="1.3"/>
  <text x="170" y="146" text-anchor="middle" fill="#6b6e7b" font-size="12.5" font-weight="700">성공으로 기록</text>
  <rect x="348" y="8" width="324" height="160" rx="12" fill="#e23d48" fill-opacity="0.05" stroke="#e23d48" stroke-width="1.8"/>
  <text x="510" y="34" text-anchor="middle" fill="#c7303b" font-size="13" font-weight="700">산출물 신선도로 보면</text>
  <text x="510" y="80" text-anchor="middle" fill="#2a2d3e" font-size="15" font-weight="700">대시보드 = 어제 날짜</text>
  <text x="510" y="112" text-anchor="middle" fill="#6b6e7b" font-size="12">오늘 것으로 갱신 안 됨</text>
  <rect x="446" y="128" width="128" height="28" rx="14" fill="#e23d48" fill-opacity="0.14" stroke="#e23d48" stroke-width="1.5"/>
  <text x="510" y="146" text-anchor="middle" fill="#c7303b" font-size="12.5" font-weight="700">실패로 판정</text>
</svg>
</figure>

그렇다고 종료 코드를 버리지는 않습니다. 다만 그걸 최종 성공으로 삼지 않습니다. 종료
코드와 로그는 프로세스가 어떻게 끝났는지 말해 주고, 산출물 신선도는 사람이 볼 화면이
실제로 갱신됐는지 말해 줍니다. 그래서 판정의 마지막 기준을 산출물로 뒀습니다. 오늘
결과물이 오늘 날짜, 오늘 시각으로 새로 쓰였는지를 파일의 생성·수정 시각으로 봅니다.
어제 것이 그대로면 종료 코드가 0이어도 실패입니다. 프로그램이 잘 끝났느냐가 아니라
결과물이 새것이냐를 봅니다.

## 실패하면 스스로 고치는 워치독

무인 루프에서 진짜 비용이 나는 건 실패한 날 아침입니다. 그래서 진단만 전담하는
에이전트를 따로 두고, 그걸 자동으로 깨우는 워치독을 루프 끝에 붙였습니다. 워치독은
자동 실행이 조용히 실패하는 걸 잡아내는 마지막 점검자입니다. 09:40 워치독은 세
단계로 움직입니다.

먼저 오늘 산출물이 다 신선한지만 확인합니다. 여기서 LLM은 안 씁니다. 정상이면 그대로
끝나니, 아무 문제 없는 날은 비용이 0입니다.

이상하다 싶어도 곧장 손대지 않고 20분을 기다렸다 다시 봅니다. Mac이 슬립에서 늦게
깨어 잡이 밀리는 날이 있는데, 그런 날을 실패로 오해하지 않으려는 여유입니다.

그래도 이상이면 그때 진단 에이전트가 headless로 기동합니다. 스케줄러 상태, 로그,
산출물 순으로 원인을 좁히고, 증거가 있는 모듈만 다시 돌립니다. 그날 배포가 빠졌으면
배포까지 복구하고, 결과는 담당자에게 DM으로 보냅니다.

<figure style="margin:1.75rem 0;overflow-x:auto;">
<svg viewBox="0 0 720 168" role="img" aria-label="워치독 세 단계: 산출물 신선도 확인에서 정상이면 LLM 호출 없이 종료, 이상이면 20분 대기 후 재확인, 그래도 이상이면 진단 에이전트가 스케줄러 상태·로그·산출물 순으로 진단하고 증거 있는 모듈만 재실행한 뒤 DM으로 보고한다." style="width:100%;min-width:640px;height:auto;font-family:inherit;">
  <rect x="8" y="60" width="150" height="48" rx="10" fill="none" stroke="#3e4a78" stroke-width="1.5"/>
  <text x="83" y="80" text-anchor="middle" fill="#2a2d3e" font-size="12.5" font-weight="600">① 신선도 확인</text>
  <text x="83" y="97" text-anchor="middle" fill="#6b6e7b" font-size="10.5">결정적 헬스체크</text>
  <path d="M158 84 H188" stroke="#3e4a78" stroke-width="1.5" fill="none"/><path d="M182 79 L188 84 L182 89" stroke="#3e4a78" stroke-width="1.5" fill="none"/>
  <text x="173" y="74" text-anchor="middle" fill="#6b6e7b" font-size="10">이상</text>
  <rect x="188" y="60" width="150" height="48" rx="10" fill="none" stroke="#3e4a78" stroke-width="1.5"/>
  <text x="263" y="80" text-anchor="middle" fill="#2a2d3e" font-size="12.5" font-weight="600">② 20분 대기</text>
  <text x="263" y="97" text-anchor="middle" fill="#6b6e7b" font-size="10.5">재확인·오탐 방지</text>
  <path d="M338 84 H368" stroke="#3e4a78" stroke-width="1.5" fill="none"/><path d="M362 79 L368 84 L362 89" stroke="#3e4a78" stroke-width="1.5" fill="none"/>
  <text x="353" y="74" text-anchor="middle" fill="#6b6e7b" font-size="10">이상</text>
  <rect x="368" y="60" width="180" height="48" rx="10" fill="none" stroke="#e23d48" stroke-width="1.5"/>
  <text x="458" y="80" text-anchor="middle" fill="#2a2d3e" font-size="12.5" font-weight="600">③ 진단 에이전트</text>
  <text x="458" y="97" text-anchor="middle" fill="#6b6e7b" font-size="10.5">상태·로그·산출물</text>
  <path d="M548 84 H578" stroke="#e23d48" stroke-width="1.5" fill="none"/><path d="M572 79 L578 84 L572 89" stroke="#e23d48" stroke-width="1.5" fill="none"/>
  <rect x="578" y="60" width="134" height="48" rx="10" fill="none" stroke="#e23d48" stroke-width="1.5"/>
  <text x="645" y="80" text-anchor="middle" fill="#2a2d3e" font-size="12.5" font-weight="600">재실행·복구</text>
  <text x="645" y="97" text-anchor="middle" fill="#6b6e7b" font-size="10.5">DM 보고</text>
  <path d="M83 60 V30 H360" stroke="#3e4a78" stroke-width="1.5" fill="none"/>
  <path d="M352 25 L360 30 L352 35" stroke="#3e4a78" stroke-width="1.5" fill="none"/>
  <text x="225" y="22" text-anchor="middle" fill="#2a7d5a" font-size="10.5" font-weight="600">정상 → LLM 호출 없이 종료 (비용 0)</text>
  <rect x="360" y="16" width="120" height="26" rx="8" fill="#f5be4b" fill-opacity="0.18" stroke="#2a7d5a" stroke-width="1.2"/>
  <text x="420" y="33" text-anchor="middle" fill="#2a7d5a" font-size="11" font-weight="700">종료</text>
</svg>
</figure>

진단 에이전트에는 두 가지 철칙이 있습니다. 로그에 증거가 없으면 재시작하지 않습니다.
그리고 로그인 만료처럼 사람이 손대야만 풀리는 원인 앞에서는, 억지로 고치려 들지 않고
필요한 조치를 보고하는 선에서 멈춥니다. 뭐든 자동으로 고치려다 상황을 더 어그러뜨리는
게 더 큰 사고이기 때문입니다.

한 번 겪은 고장은 에이전트 정의에 적어 둡니다. 슬립으로 잡이 밀린 날, CLI 세션이
만료된 날, 봇이 채널에서 강퇴된 날. 이렇게 적어 두면 같은 데서 두 번 헤매지 않습니다.
겪은 실패가 규칙으로 쌓여 다음 실행이 덜 헤매는 이 흐름을 자기개선 루프라고 부릅니다.
[실패를 규칙으로 되먹이는 하네스 이야기](/blog/madobi-imagegen-consistency/)에서 이미지
생성으로도 한 번 다뤘습니다.

## 만들면서 확인한 것들

클로드 코드는 한 번 로그인해 두면 로컬에 저장된 인증을 계속 씁니다. 그래서 사람이
자리를 비운 무인 상태에서도 백그라운드에서 정상으로 돕니다. 여기에 미리 붙여 둔
Slack MCP 연결이 더해져, 단순한 스크립트로 끝나는 일뿐 아니라 LLM의 판단과 외부 도구
연결이 필요한 일까지 OS 레벨에서 자동으로 돌아갑니다. 08:30 연차 갱신이 Slack을 읽어
정리하는 게 그렇습니다. 인증은 클로드 코드 쪽, MCP는 도구 연결 쪽으로 역할이 나뉘어
있어서, 둘 다 미리 확인해 둬야 무인 실행이 멈추지 않습니다.

launchd에는 단계 사이의 의존성 체인이 없습니다. 앞 단계가 끝났다고 다음 단계가 저절로
이어지지 않습니다. 그래서 08:00에서 09:40까지 시각을 벌려, 앞 단계가 끝날 시간을 주고
다음 단계를 예약해 순서를 맞췄습니다. 다만 시각 간격만으로 순서가 완벽히 보장되지는
않습니다. Mac이 슬립에서 늦게 깨면 밀린 단계들이 몰려 돌 수 있어서, 마지막의 산출물
신선도 점검이 순서가 어긋난 날을 걸러 주는 안전판입니다.

## 직접 만들어 본다면

작은 자동화부터 같은 뼈대로 만들 수 있습니다.

1. 무엇을 매일 만들지, 그 산출물부터 정합니다. "이게 새것이면 성공"이라고 말할 수 있는 결과물이 있어야 성공을 판정할 수 있습니다.
2. 만드는 과정을 스크립트 하나로 손으로 먼저 돌려 봅니다. PATH와 토큰을 스크립트 안에서 직접 챙겨, 자동 실행으로 넘어가도 환경이 비지 않게 합니다.
3. 트리거를 정합니다. 로컬에 있는 게 필요 없으면 /schedule로 클라우드에, 로컬 자격 증명이나 사내망 접근, 빌드·배포가 필요하면 launchd 같은 OS 스케줄러로 겁니다.
4. 성공은 종료 코드가 아니라 산출물 신선도로 봅니다. 실행 뒤 결과물이 오늘 날짜로 새로 쓰였는지 확인하고, 아니면 실패로 다뤄 다시 돌리거나 알림을 남깁니다.
5. 실패하면 그 교훈을 규칙으로 적어, 다음 실행이 먼저 읽게 합니다. 사람이 손대야만 풀리는 원인은 자동 수리 대신 보고로 멈추게 해 둡니다.

클로드 코드를 아직 안 써봤어도 시작할 수 있습니다. launchd도 워치독도 없이, 지금 쓰는
AI 대화창 하나면 됩니다. 반복해서 시키는 일의 지시문을 매번 새로 쓰지 말고 파일에
저장해 두고, 그 옆에 교훈 파일을 하나 만들어 실수할 때마다 한 줄씩 적습니다. 다음에
일을 시킬 때 지시문과 교훈 파일을 함께 붙여 넣으면, AI가 지난 실수를 피한 자리에서
시작합니다. 이게 손에 익어 사람이 안 봐도 되겠다 싶어지면, 그때 스케줄러와 워치독을
얹어 무인으로 올려도 늦지 않습니다. 🐹

<img src="/madobi/rocket.png" alt="무인으로 스스로 도는 자동화에 올라탄 매도비" width="150" height="150" loading="lazy" style="display:block;margin:1.75rem auto 0;" />