똑딱 하면 AI가 해주는 세상. 내가 들었던 이상적인 Agentic Coding은 "'○○ 만들어줘' 하면 끝"이었다.

처음 그 말을 들었을 땐 "그게 될까?" 싶었다. 우리가 걸어온 길을 돌아보면 자리가 조금씩 바뀌어 왔다. 처음엔 내가
하는 일을 곁에서 거들어 주는 정도였다 — Machine in the Loop(MITL). 점차 에이전트가 대부분을 하고 사람은
검수만 하게 됐다 — Human in the Loop(HITL). 저 이상에 가장 가까운 자리는 아마 Human on the Loop(HOTL)일
것이다. 사람은 AI가 만들어 낸 것을 그냥 쓰기만 하면 되는 자리. 어차피 사람이 쓰지도 않을 걸 만드는 건 의미가
없을 테니까.

이 글은 그 HOTL이라는 자리까지 실제로 밀어본 기록이다. 그리고 거기서 배운 건, 자동화의 진짜 걸림돌이 종종
사람 자신이더라
는 것이다.

봇이 아니라 사람이 병목이다

장면 하나. 봇이 일을 다 해놓고 멈춰 있다. 커밋도 했고, PR도 올렸고, "끝났다"는 댓글까지 달았다. 그런데 다음으로
넘어가질 않는다. 왜? 사람이 "다음"을 눌러 주지 않아서다.

봇을 아무리 여러 대 돌려도, 매 단계가 사람 손을 거쳐야 넘어가면 전체 속도는 결국 사람이 확인해 주는 속도에
묶인다. 느린 건 봇이 아니라 사람이다. 우리가 붙잡고 씨름한 게 바로 이 지점이었다.

사람의 자리는 셋

같은 자동화라도 사람이 어디에 서 있느냐가 다르다.

  • 곁에서 거드는 자리 (MITL) — 일은 사람이 하고 기계가 옆에서 돕는다. 자동완성이 그렇다.
  • 루프 안에 있는 자리 (HITL) — 일은 기계가 하되 매 단계 사람이 확인하고 승인해야 넘어간다. 안전하지만,
    모든 사이클이 사람을 거치니 곧 사람이 병목이 된다.
  • 루프 위에 있는 자리 (HOTL) — 루프는 알아서 돈다. 사람은 위에서 지켜보다가 어긋난 것만 손본다. 규칙을
    정해 두고, 이상하면 끼어든다. 평소엔 빠져 있다가 필요할 때만 들어온다.

어느 쪽이 무조건 낫다는 얘기는 아니다. 위험이 큰 일은 사람이 안에 있는 편이 낫다. 다만 HOTL은 우리가 아직
충분히 가보지 않은 자리
다. "여기까지는 한번 가보자"가 이 프로젝트의 목표가 된 이유다.

사람을 루프 밖으로 뺀 곳

우선 되풀이되는 조율부터 사람 손에서 뗐다.

  • 깨우는 일을 웹훅으로 자동화. 예전엔 사람이 채팅으로 봇을 하나하나 불렀다. 이제는 GitHub 이슈에
    dispatch:<봇이름> 라벨 하나만 붙이면 된다. 그러면 GitHub이 그 사건을 웹훅(webhook, 이벤트 알림) 으로
    우리 중계기에 쏘고, 중계기는 서명을 확인해 위조가 아닌지 본 뒤 어느 봇 일인지 판단해 Discord로 "이거 해줘"를
    보낸다. 사람의 호출이 라벨 한 줄로 바뀐 것이다.
  • 끝낸 뒤 넘기는 일을 자동으로. 이번 실험의 핵심이었다. 예전엔 봇이 한 토막을 끝내도 사람이 매번 "다음"을
    손으로 밀어야 했다. 이제는 봇이 끝내며 카드에 "검토 바람(awaiting-review)" 라벨만 남기면, 중계기가 끝낸
    봇의 처리 표시를 걷어 내고 곧바로 dispatch:<요청자> 라벨을 붙여 요청자를 자동으로 다시 깨운다. 그 라벨이
    다시 웹훅을 태워 검증 단계로 이어진다. 사람은 이 이음매를 건드리지 않는다.
  • 실패도 사람 없이 줍는다. 웹훅이 새거나 중계기가 잠깐 죽으면 놓친 카드가 생긴다. 그래서 20분마다 도는
    점검(reconcile)이 "dispatch 라벨은 붙었는데 아직 처리 안 된" 카드를 다시 찾아 보낸다. 반대로 깨웠는데
    하루가 지나도 진전이 없는 카드는 따로 잡아 사람에게 알린다(dead-letter).

여기까지가 사람을 루프 위로 올려 둔 부분이다. 이 사이클을 사람이 매번 지나가지 않는다.

일부러 루프 안에 남긴 곳

HOTL은 "사람을 없앤다"가 아니다. 위험과 판단은 사람이 안에서 붙잡는다.

  • 위험한 일에는 승인 관문. 배포, 열쇠 교체, 외부로 나가는 발송, 되돌릴 수 없는 삭제 — 이런 카드에는
    gated(위험) 라벨을 붙인다. 그러면 dispatch 라벨이 함께 있어도 중계기는 봇을 깨우지 않고 "승인 대기"로
    돌린 뒤 승인자에게 넘긴다. 사람이 확인하고 approved를 붙여야 그때 봇이 착수한다.
  • 머지는 사람 손으로. 코드는 PR까지 알아서 가지만, main에 넣는 마지막 한 걸음은 사람이 승인한다.
  • 판단이 필요한 관문도 사람이. 요구사항 지도가 개발로 넘어갈 만큼 여물었는지, 앱이 실제로 잘 도는지 —
    이런 건 사람이 본다.

정리하면 뼈대는 하나다. 되풀이되는 일은 루프 위에서, 위험하고 판단이 필요한 일은 루프 안에서.

나가려면, 되돌아올 길부터

사람이 루프 밖으로 나가는 데는 조건이 하나 있다. 루프가 스스로 못 갈 때 반드시 사람을 다시 부를 수 있어야
한다.
그게 없으면 애초에 나갈 수가 없다. 우리는 이걸 버그로 배웠다.

끝낸 뒤 넘기는 기능을 붙여 실전에 돌렸더니, 어떤 카드가 무한히 돌기 시작했다. 사정은 이랬다. 넘김은 "일꾼 봇 →
코디네이터" 방향으로 설계됐는데, 코디네이터가 자기 카드를 끝내며 검토 표시를 남기자 중계기가 다시
코디네이터를 깨웠다.
자기가 자기를 부르는 고리다. 사람을 빼놓은 자리에서 루프가 제풀에 폭주한 셈이다.

고친 방법이 HOTL의 핵심을 그대로 보여 준다.

  • 오간 횟수를 센다. 카드마다 몇 번 넘어갔는지 세고, 정해 둔 횟수를 넘으면 사람에게 올린다(끝없는 핑퐁 차단).
  • 자기 넘김을 막는다. 끝낸 쪽과 넘길 쪽이 같으면 되깨우지 않고 사람에게 드러낸다.
  • 멈춤을 알아챈다. 깨웠는데 진전 없는 카드는 하루 뒤 사람에게 알린다.

셋 다 결국 같은 말이다. 자율은 믿을 만한 예외 처리 위에서만 선다. 잘 도는 것보다, 못 돌 때 조용히 죽지 않고
사람을 부르는 게 먼저다.

정작 병목은 우리 자신이었다

돌아보면 재미있다. 이 작업을 하는 내내 병목은 곧 사람, 우리 자신이었다. PR을 머지하는 것도, 노드에 배포하고
서비스를 재시작하는 것도 매번 사람 몫이었다. 그게 아직 루프 안에 남아 있는 사람의 자리다.

그 자리를 줄여 보려다 버그를 만났고, 안전장치를 하나씩 붙이며 조금씩 안에서 위로 옮겨 왔다. 다 됐다는 게
아니라, 옮겨 가는 중이다.

얼마나 왔는지 재보자

다행인 건 이걸 감이 아니라 숫자로 볼 수 있다는 점이다. 루프가 사람을 몇 번 거쳤는지가 로그에 그대로 남기
때문이다. 재볼 만한 값은 이렇다.

  • 자율 완주율 — 사람 손 안 거치고 깨우기부터 종료까지 간 카드의 비율.
  • 사람을 부른 횟수 — 승인 보류, 에스컬레이션, 손으로 한 머지·깨우기의 합(기간당).
  • 평균 오간 횟수 — 카드 하나가 사람에게 닿기까지 봇들 사이를 몇 번 오갔나.
  • 사람을 부른 이유별 건수 — 자기 넘김, 횟수 초과, 위험 보류, 멈춤 각각.

이 값들은 중계기가 남기는 로그에 이미 다 들어 있다. 이 글을 쓰기까지 하루만 봐도, 자기 넘김을 붙잡아 사람에게
올린 게 7건이었다 — 안전장치가 없었다면 7번 폭주했을 자리다.

꾸준히 재는 방법은 세 갈래를 두고 저울질했다.

  1. 중계기가 도는 노드에 주간 타이머를 하나 더 걸어, 로그를 훑어 지표를 계산하고 기록으로 남긴다. 로그가 거기
    있으니 손이 가장 덜 간다.
  2. 운영 점검을 맡은 봇의 정기 작업으로 둔다. 이 지표는 운영 지표이니 역할상 잘 맞는다.
  3. 예약 작업으로 주기적으로 불러다 계산한다.

역할로 보면 2번이 맞지만, 그 봇의 정기 작업 자체가 아직 다음 숙제다. 그래서 지금은 1번으로 가볍게 시작해
데이터를 쌓고
, 그 봇이 서면 그쪽으로 옮길 생각이다. (첫 측정 도구는 이미 붙였다.)

아직 안 가본 자리

언제 할지는 정하지 않았다. 방향만 적어 둔다. 이 글이 그 추적의 출발점이다.

  1. 검증을 봇이 스스로 끝맺기. 지금은 검증 뒤 카드를 닫는 걸 사람이 확인하는 경우가 많다. 통과가 확실하고
    위험이 낮으면 코디네이터가 스스로 닫도록 넓힌다.
  2. 보안·운영 점검을 상시로 (이미 실험 중). 보안은 Batou, 운영은 Tachikoma가 맡아, 사람이 부르지 않아도
    정기 작업(cron)으로 커밋·릴리즈 전 점검을 돌리고 걸리는 게 있으면 이슈로 올리게 한다. 지금은 cron으로 감을
    잡는 실험 단계이고, 결과를 보고 웹훅 흐름에 정식으로 물릴지 정한다.
  3. 승인 관문을 숫자로 좁히기. 무엇을 꼭 사람이 봐야 하는지 위 지표로 재서, 위험 표시의 범위를 근거를 갖고
    줄인다. "불안해서"가 아니라 "재보니 안전해서" 줄이는 게 목표다.

HOTL이 무조건 정답이라서가 아니다. 아직 가보지 않은 자리라서, 거기까지 가서 무엇이 보이는지 확인하고 싶은
것이다. 그 길을 숫자로 남기며 걸어가 보려 한다.

— Compose는 UI Toolkit이 아니라 패러다임 전환이다

Compose를 처음 접하면 보통은 이렇게 생각하기 쉽습니다.

“XML 대신 Kotlin으로 UI 만드는 기술”

실제로 처음 보이는 것도:

Text()
Button()
Column()

같은 UI 함수들입니다.

그래서 처음에는:

"새로운 UI DSL"

정도로 느껴질 수 있습니다.

하지만 Compose를 조금 깊게 사용하기 시작하면 점점 다른 느낌이 옵니다.


Compose를 쓰며 느끼는 변화

어느 순간부터 개발 흐름이 바뀌기 시작합니다.

예전에는:

View를 어떻게 조작할까?

를 고민했다면,

Compose에서는:

현재 상태를 어떻게 표현할까?

를 고민하게 됩니다.

그리고 이 변화는 단순 UI 기술 변경이 아닙니다.

실제로는:

  • Architecture 변화
  • Data Flow 변화
  • 상태 관리 방식 변화
  • Navigation 사고 변화
  • UI 설계 방식 변화

까지 이어집니다.


Android UI는 원래 어떤 구조였을까

Android의 전통적인 UI 시스템은 기본적으로:

Mutable Object Tree

기반이었습니다.

즉:

View 객체 생성
→ 속성 수정
→ invalidate()
→ redraw

구조입니다.

예:

textView.text = "Hello"
button.visibility = View.GONE

우리는 오랫동안:

“화면 객체를 직접 수정”

하는 방식으로 UI를 개발했습니다.


이 방식의 장점

이 구조는 매우 직관적입니다.

특히 Android 초기에는:

  • 단순 화면
  • 로컬 상태
  • 적은 비동기 처리

환경에서는 잘 동작했습니다.

그리고 Android Framework는:

  • Lifecycle
  • View hierarchy
  • Adapter
  • Fragment

등을 중심으로 발전했습니다.


하지만 앱이 복잡해졌다

문제는 앱 구조가 점점 복잡해졌다는 것입니다.

현대 앱은:

  • 실시간 데이터
  • streaming UI
  • AI 응답
  • 지도
  • reactive stream
  • 멀티 디바이스
  • 비동기 이벤트

가 매우 많아졌습니다.

예:

채팅
뉴스 피드
실시간 운전 정보
AI Assistant
Video Streaming

이런 구조에서:

View를 직접 수정하는 방식

은 점점 복잡해졌습니다.


Compose가 바꾼 것

Compose는 이 흐름을 바꾸기 시작했습니다.

핵심 철학은 단 하나입니다.

UI = f(state)

즉:

“현재 상태를 기반으로 UI를 계산한다”

입니다.


중요한 변화

Compose는:

UI 객체를 조작

하는 것이 아니라

상태를 선언

합니다.


예:

기존 Android

button.isEnabled = true

Compose

Button(
    enabled = canSubmit
)

Compose는:

현재 상태가 무엇인가?

만 보고 UI를 다시 계산합니다.


Compose의 진짜 핵심

많은 사람들이 Compose의 핵심을:

Declarative UI

라고 말합니다.

물론 맞는 말입니다.

하지만 실제 더 중요한 변화는:

Control 중심 Architecture
→
State 중심 Architecture

입니다.


기존 Android 사고

기존 Android에서는:

화면을 어떻게 조작할까?

가 중심이었습니다.


Compose 사고

Compose에서는:

현재 상태를 어떻게 모델링할까?

가 중심이 됩니다.


그래서 Flow와 Compose가 잘 맞는다

Compose를 쓰다 보면 자연스럽게 이런 구조로 갑니다.

Repository
   ↓
Flow
   ↓
ViewModel
   ↓
StateFlow
   ↓
Compose UI

왜냐하면 Compose는:

상태(State)를 기반으로
UI를 다시 계산하는 시스템

이기 때문입니다.


Compose는 함수형 UI에 가깝다

Compose는 결국:

UI를 함수처럼 생각하는 방식

입니다.

즉:

UI = f(state)

입니다.


기존 View 시스템

기존에는:

mutable object

를 직접 수정했습니다.


Compose 시스템

Compose에서는:

현재 상태 → 함수 실행 → UI 계산

입니다.

즉:

UI가 “객체”보다 “계산 결과”에 가까워집니다.


React / SwiftUI / Flutter와의 공통점

흥미로운 점은:

  • React
  • SwiftUI
  • Flutter
  • Compose

모두 비슷한 방향으로 이동하고 있다는 것입니다.

공통 철학은:

State-driven UI

입니다.


왜 이런 흐름이 생겼을까

현대 UI는:

  • 비동기 이벤트
  • 실시간 데이터
  • 네트워크 상태
  • 사용자 상호작용

이 계속 변화합니다.

즉:

UI가 "고정된 화면"

이 아니라:

계속 변화하는 상태의 흐름

이 되었습니다.

그래서:

상태를 중심으로
UI를 계산하는 구조

가 더 자연스러워졌습니다.


Compose를 배우며 다시 보이는 Android Architecture

Compose를 깊게 사용하다 보면 기존 Android Architecture도 다시 보이기 시작합니다.

예:

  • Fragment는 상태 컨테이너처럼 보이고
  • RecyclerView는 state rendering 문제처럼 보이고
  • Navigation은 route state처럼 보이고
  • Lifecycle도 state transition처럼 보입니다.

즉:

Android Architecture 전체를
상태 흐름 관점으로 다시 보게 된다

는 변화가 생깁니다.


Compose가 중요한 이유

Compose의 진짜 의미는:

XML 제거

가 아닙니다.

실제로는:

Android 개발 사고방식 자체 변화

에 가깝습니다.


Android 경험자는 왜 유리한가

기존 Android 경험이 깊다면:

  • Lifecycle
  • Threading
  • Performance
  • Navigation
  • Multimedia
  • Reactive stream

경험이 이미 있기 때문에 Compose에서도 강점이 있습니다.

특히:

  • Flow
  • Coroutine
  • 상태 변화
  • 이벤트 흐름

에 익숙하다면 Compose를 훨씬 빠르게 이해할 수 있습니다.


하지만 가장 어려운 변화

Compose에서 가장 어려운 것은 결국 이것입니다.

View 중심 사고를
버리는 것

입니다.


Compose를 잘한다는 것

Compose를 잘한다는 것은:

Fancy UI를 만드는 것

보다:

상태 흐름을 명확하게 설계하는 것

에 가깝습니다.


앞으로 Android는 어떻게 될까

개인적으로 Compose 이후 Android 개발은 점점:

Reactive
+
State-driven
+
Function-oriented

방향으로 갈 가능성이 높다고 생각합니다.

그리고 이는 단순 Android만의 흐름이 아닙니다.

전체 UI 개발 생태계가 같은 방향으로 이동하고 있습니다.


Compose와 AI 시대

흥미로운 부분은 Compose의 구조가 AI 시대와도 잘 맞는다는 점입니다.

왜냐하면 AI 시스템 역시:

상태(State)
+
Event
+
Stream
+
Reactive Flow

중심으로 움직이기 때문입니다.

예를 들어:

  • Streaming Text
  • Tool Execution State
  • AI Agent Workflow
  • Real-time Suggestion UI

등은 모두:

State 변화 기반 UI

와 잘 맞습니다.


Compose를 한 문장으로 정리하면

Compose는 단순히:

새로운 Android UI Toolkit

이 아닙니다.

실제로는:

상태(State)를 중심으로
UI와 Architecture를 다시 설계하는 방식

에 가깝습니다.


시리즈를 마무리하며

이번 시리즈에서는:

  1. Compose는 왜 등장했는가
  2. Android 개발자의 사고는 어떻게 바뀌는가
  3. Compose 핵심 개념
  4. 예제로 배우는 Compose
  5. 실무 Compose Architecture
  6. Compose를 배우며 다시 생각한 Android Architecture

흐름으로 Compose를 정리해보았습니다.

결국 Compose의 핵심은:

UI를 어떻게 만들까?

보다

상태 흐름을 어떻게 설계할까?

에 있습니다.

그리고 이 변화는 단순 UI 기술 변화가 아니라:

Android 개발 사고방식 자체의 변화

라고 볼 수 있습니다.

OpenClaw의 ACP(Agent Client Protocol)를 사용하면 OpenClaw에서 Claude Code를 외부 코딩 에이전트로 실행할 수 있습니다. 전체 과정은 다음 네 단계입니다.

  1. ACPX 플러그인 설치 및 활성화
  2. 전역 ACP 설정
  3. Claude Code 설치 및 인증
  4. ACP 연결 검증

1. ACPX 플러그인 설치 및 활성화

OpenClaw Gateway에서 다음 명령을 실행합니다.

openclaw plugins install @openclaw/acpx
openclaw config set plugins.entries.acpx.enabled true

설정 파일에는 다음 항목이 생성되거나 활성화됩니다.

{
  "plugins": {
    "entries": {
      "acpx": {
        "enabled": true
      }
    }
  }
}

ACPX는 OpenClaw가 Claude Code, Codex, Gemini CLI 등의 ACP harness를 실행할 때 사용하는 공식 런타임 플러그인입니다. Claude용 ACP 어댑터는 첫 실행 시 자동으로 준비되므로 별도의 claude-agent-acp 패키지를 직접 설치할 필요는 없습니다.

2. 전역 ACP 설정

openclaw.json의 최상위에 다음 acp 블록을 추가합니다.

{
  "acp": {
    "enabled": true,
    "dispatch": {
      "enabled": true
    },
    "backend": "acpx",
    "defaultAgent": "claude",
    "allowedAgents": [
      "claude",
      "gemini"
    ],
    "maxConcurrentSessions": 8
  }
}

각 항목의 의미는 다음과 같습니다.

  • enabled: ACP 기능 전체 활성화
  • dispatch.enabled: ACP 세션 자동 dispatch 허용
  • backend: ACP 백엔드로 acpx 사용
  • defaultAgent: 기본 harness를 Claude로 지정
  • allowedAgents: 사용할 수 있는 ACP harness 목록
  • maxConcurrentSessions: 동시에 실행할 수 있는 ACP 세션 수

Claude만 사용할 경우에는 다음처럼 간단히 구성해도 됩니다.

"allowedAgents": ["claude"]

별도의 OpenClaw agent를 Claude ACP 전용으로 고정하지 않는다면 agents.list 설정은 필요하지 않습니다. 전역 ACP 설정만으로 필요할 때 Claude를 호출할 수 있습니다.

3. Claude Code 설치 및 인증

Claude Code가 Gateway가 실행되는 호스트에 설치되어 있어야 합니다.

npm install -g @anthropic-ai/claude-code

설치 상태를 확인합니다.

claude --version
claude doctor

처음 실행하면 인증 절차가 진행됩니다.

claude

화면의 안내에 따라 Anthropic Console 또는 Claude Pro/Max 계정으로 로그인합니다. ACP는 OpenClaw의 인증을 대신하지 않으므로, Claude Code 인증은 반드시 Gateway 호스트에서 별도로 완료해야 합니다.

4. ACP 사용 검증

먼저 Gateway에서 ACP 백엔드 상태를 확인합니다.

/acp doctor

정상이라면 ACP 백엔드가 enabled/healthy 상태로 표시됩니다.

다음 검증 프롬프트를 입력합니다.

OpenClaw ACP 연결 검증을 수행해 주세요.

1. 현재 ACP harness 이름을 Claude Code라고 답하세요.
2. 현재 작업 디렉터리의 절대 경로를 알려 주세요.
3. 현재 세션이 ACP를 통해 실행되었다고 명시해 주세요.
4. 파일을 생성하거나 수정하지 말고, 명령 실행도 하지 마세요.
5. 마지막 줄에 정확히 "ACP 연결 검증 완료"라고 출력해 주세요.

다음과 같은 응답을 받으면 기본 연결이 정상입니다.

Claude Code ACP 세션입니다.
현재 작업 디렉터리: /...
ACP를 통해 실행되었습니다.
ACP 연결 검증 완료

문제 발생 시 확인 순서

/acp doctor
  1. ACPX 플러그인이 설치·활성화되어 있는지 확인
  2. acp.enabledtrue인지 확인
  3. acp.allowedAgentsclaude가 포함되어 있는지 확인
  4. Gateway 호스트에서 claude --version이 동작하는지 확인
  5. Claude Code 인증이 완료되어 있는지 확인
  6. cwd로 지정한 작업 디렉터리가 실제로 존재하는지 확인
  7. 설정 변경 후 Gateway를 재시작

설정 변경 후에는 Gateway를 재시작해야 합니다.

openclaw gateway restart

참고 문서:

주의할 점은 Claude Code 설치와 ACPX 플러그인 설치가 서로 다른 단계라는 것입니다. Claude Code는 실제 harness와 인증을 제공하고, ACPX는 OpenClaw가 그 harness를 ACP 세션으로 실행하도록 연결하는 역할을 합니다.

+ Recent posts