ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 에이전트가 지금 무슨 도구를 쓰는지 안 보인다 — OpenClaw 웹 채팅이 WebSocket 스트리밍으로 푼 방법
    IT 2026. 7. 9. 22:00
    에이전트가 지금 무슨 도구를 쓰는지 안 보인다 — OpenClaw 웹 채팅이 WebSocket 스트리밍으로 푼 방법

    오픈소스 개인 AI 어시스턴트 OpenClaw를 집 서버에 올려 쓰면서 가장 자주 여는 화면은 의외로 텔레그램이 아니라 브라우저의 Chat(채팅) 메뉴였다. 메신저로도 같은 에이전트와 대화할 수 있는데 왜 굳이 웹 화면이 따로 있을까? 답은 한 단어로 투명성이다. 에이전트가 내 질문에 답하려고 git을 돌리는지, 파일을 읽는지, 외부 검색을 하는지 — 텔레그램에선 최종 답만 툭 떨어지지만, 웹 Chat에선 그 과정이 실시간으로 흘러나온다. 이 글은 "에이전트가 무슨 일을 하는지 안 보인다"는 문제를 OpenClaw Chat이 게이트웨이 WebSocket 스트리밍으로 어떻게 풀었는지 따라간다.

    배경 — 에이전트와의 대화는 "한 번에 끝나는 요청"이 아니다

    일반적인 챗봇 API는 단순하다. 질문을 던지면(request) 답이 온다(response). 하지만 도구를 쓰는 AI 에이전트는 다르다. 사용자가 "이번 주 커밋 정리해줘"라고 하면, 에이전트는 곧장 답을 만들지 않고 여러 단계의 행동을 거친다 — git 로그를 읽고, 파일을 열어보고, 필요하면 외부 검색을 하고, 그 결과를 종합해 답을 쓴다. 이 과정이 수 초에서 수십 초까지 걸린다. OpenClaw의 Chat 화면은 이 긴 과정을 게이트웨이(Gateway, 모든 채널과 에이전트를 잇는 중앙 서버)의 WebSocket(서버와 브라우저가 연결을 열어 두고 양방향으로 메시지를 주고받는 통신 방식) 위에서 굴린다.

    diagram

    다이어그램 설명. 위 그림은 웹 Chat에서 질문 하나가 어떻게 처리되는지를 보여준다. 맨 위 브라우저 화면이 질문을 게이트웨이 WebSocket으로 보내면, 게이트웨이는 에이전트 실행(run)을 시작하고, 에이전트는 답을 만들기 위해 git 로그 읽기·파일 열기·외부 검색 같은 여러 도구를 차례로 호출한다. 핵심은 화살표의 방향이다 — 각 도구 호출과 최종 답변이 다시 아래쪽 브라우저로 거꾸로 흘러 들어온다. 즉 한 번 요청하고 한 번 응답받는 게 아니라, 하나의 질문에 대해 여러 개의 작은 이벤트가 연속으로 쏟아진다. 흔히 하는 오해가 "채팅이니까 단순 요청-응답이겠지"인데, 에이전트 채팅은 그 가정을 깨뜨린다. 이 다중 이벤트를 어떻게 브라우저로 전달하느냐가 곧 이 글의 주제다.

    핵심 문제 — 최종 답만 보이면 디버깅도 신뢰도 무너진다

    가장 단순한 설계는 텔레그램과 똑같이 하는 것이다 — 에이전트가 모든 도구를 다 돌리고 최종 답이 완성된 다음에 한 번에 화면에 띄운다. 구현이 쉽고 메신저 채널과 코드를 공유할 수 있다. 하지만 이렇게 하면 세 가지가 무너진다.

    diagram

    다이어그램 설명. 위 그림은 "최종 답만 한꺼번에 보여주는" 단순 방식에서 갈라지는 세 가지 실패를 보여준다. 왼쪽부터 보면, 첫째는 블랙박스 문제다 — 에이전트가 20초 동안 도구를 돌리는 사이 화면은 멈춰 있고, 사용자는 봇이 죽은 건지 열심히 일하는 건지 구별할 수 없다. 둘째는 디버깅 불가다 — 에이전트가 엉뚱한 답을 내놨을 때, 중간 과정이 안 보이면 잘못된 파일을 읽었는지 검색 결과를 오해했는지 원인을 짚을 수 없다. 셋째는 중단 불가다 — 에이전트가 명백히 잘못된 방향으로 가는 게 보여도 최종 답이 나올 때까지 손쓸 방법이 없다. 가장 놓치기 쉬운 함정은 둘째다 — 사람들은 보통 "답만 맞으면 되지"라고 생각하지만, 자기 코드와 파일을 만지는 에이전트에서는 "어떻게 그 답에 도달했는가"를 못 보면 그 답을 신뢰할 근거 자체가 사라진다.

    그래서 무슨 가치인가 — 에이전트를 외부 채널(텔레그램)에 두지 않고 굳이 웹 화면을 따로 만드는 이유가 여기 있다. 웹 Chat의 존재 이유는 "더 예쁜 채팅창"이 아니라 에이전트의 작업 과정을 실시간으로 들여다보는 관제 창이다.

    해결 방법 — 비동기 ACK + 이벤트 스트림

    OpenClaw Chat은 질문 전송과 응답 수신을 완전히 분리했다. 질문을 보내는 chat.send 호출은 답을 기다리지 않고 즉시 돌려준다 — "접수했고 실행을 시작했다"는 영수증만 먼저 준다. 진짜 응답은 그 뒤에 WebSocket을 타고 별도의 이벤트로 흘러나온다.

    diagram

    다이어그램 설명. 위 그림은 질문 하나가 처리되는 7단계 흐름을 시간 순서로 보여준다. 첫 단계에서 브라우저가 질문을 보내면, 두 번째 단계에서 게이트웨이는 답을 기다리게 하지 않고 실행 식별자(runId)와 "started" 상태만 담은 영수증을 곧바로 돌려준다. 이게 핵심 설계인 비동기 ACK(요청을 받았다는 짧은 확인만 먼저 보내고, 본 응답은 나중에 따로 보내는 패턴)다. 그 뒤로는 에이전트가 일하는 동안 도구 호출 카드, 도구 출력 카드, 답변 본문이 각각 별개의 chat 이벤트로 하나씩 도착하고, 마지막에 실행이 끝났다는 종료 이벤트가 온다. 왜 이렇게 나눴을까 — 만약 chat.send가 최종 답까지 기다렸다가 한 번에 돌려준다면, 그 20초 동안 연결은 한 응답에 묶여 도구 진행 상황을 중간에 내보낼 길이 없다. 보내기와 받기를 끊어 놨기에 비로소 "보내자마자 → 진행 상황이 줄줄이 → 답"이라는 실시간 흐름이 가능해진다. 놓치기 쉬운 점은 영수증의 runId다 — 이 식별자가 있어야 나중에 "이 실행을 멈춰라"라고 정확히 지목할 수 있다.

    도구 호출이 카드로 흘러나온다 — agent 이벤트

    에이전트가 도구를 하나 부를 때마다, 게이트웨이는 session.tool 계열의 이벤트를 WebSocket으로 내보낸다. Chat 화면은 이걸 받아 도구 카드(어떤 도구를 무슨 인자로 불렀고 무엇이 돌아왔는지를 보여주는 작은 카드 UI)로 실시간 렌더링한다. 답변 텍스트 사이사이에 "지금 git log를 실행 중", "파일 3개를 읽음" 같은 카드가 끼어드는 식이다.

    diagram

    다이어그램 설명. 위 그림은 도구 하나가 화면에 그려지는 경로를 보여준다. 에이전트가 도구를 부르기 시작하면 게이트웨이가 도구 이벤트를 내보내고, Chat 화면은 일단 도구 이름만 적힌 카드를 먼저 띄운 뒤(호출 단계), 결과가 도착하면 같은 카드에 출력 미리보기를 채워 넣는다(출력 단계). 카드 하나가 두 박자로 완성되는 것이다. 오른쪽 아래 점선이 영리한 부분이다 — Chat의 도구 카드와 별도 메뉴인 Activity 탭(에이전트의 도구 활동을 시간순으로 모아 보여주는 관찰 로그)이 같은 이벤트 스트림을 공유한다. Activity 탭은 새 이벤트 종류나 별도 저장소를 추가하지 않고, Chat이 쓰는 바로 그 도구 이벤트를 재활용해 요약만 따로 쌓는다. 왜 이 패턴인가 — 도구 활동을 보여주는 통로를 두 개 만들면 둘이 어긋날 수 있지만, 하나의 스트림에서 두 화면을 파생시키면 항상 일치한다. 놓치기 쉬운 함정은 보안 쪽이다 — Activity 탭은 도구의 인자 값을 저장하지 않고 인자가 몇 개였는지 개수만 남기며, 출력도 잘라내고 민감 정보를 가린(redaction) 미리보기만 보관한다. 도구 인자엔 토큰이나 경로 같은 민감한 값이 섞일 수 있어서다.

    긴 대화를 안전하게 — 라이브 스트림과 영구 기록의 분리

    여기서 미묘한 문제가 하나 생긴다. 실시간으로 흘러나온 chat 이벤트는 그 순간의 전달용이지 영구 저장본이 아니다. 페이지를 새로고침하거나 다른 기기에서 접속하면 그 스트림은 사라진다. 그래서 OpenClaw는 데이터 경로를 둘로 나눈다 — 흘려보내는 라이브 이벤트와, 세션 기록(transcript)에서 다시 읽어 오는 영구 history다.

    diagram

    다이어그램 설명. 위 그림은 "지금 보이는 것"과 "새로고침해도 남는 것"이 어떻게 합쳐지는지를 보여준다. 실행 중에는 라이브 스트림이 화면에 즉시 텍스트를 그려 주고(왼쪽 경로), 동시에 같은 내용이 세션 기록에 영구 저장된다(오른쪽 경로). 도구 종료 이벤트가 오면 Chat은 chat.history로 영구 기록을 다시 읽어, 방금 화면에 떠 있던 낙관적(optimistic) 임시 메시지(서버 확정 전에 화면에 미리 그려 둔 메시지) 중 꼬리 부분만 살짝 병합한다. 왜 이렇게 번거롭게 하나 — 라이브 스트림만 믿으면 새로고침 시 대화가 통째로 날아가고, 영구 기록만 믿으면 실행 중 실시간 표시가 불가능하다. 둘을 합쳐야 "지금 즉시 보이면서도 새로고침에 견디는" 화면이 된다. 놓치기 쉬운 함정은 chat.history의 크기 제한이다 — 긴 세션 전체를 그대로 보내면 브라우저가 거대한 기록을 다 그리느라 채팅이 뜨기도 전에 멈춘다. 그래서 게이트웨이는 최근 일부 구간만, 메시지마다 글자 수 상한을 걸어 보내고, 너무 큰 메시지는 [chat.history omitted: message too large] 같은 자리표시자로 대체한 뒤, 잘린 메시지는 사용자가 펼칠 때만 chat.message.get으로 원문을 따로 불러온다.

    실행을 멈추거나 끼어들기 — Stop과 Steer

    실시간으로 과정이 보이니, 잘못 가는 걸 봤을 때 끼어들 수 있어야 가치가 완성된다. OpenClaw Chat은 두 가지 개입 수단을 둔다 — 실행을 완전히 멈추는 Stop과, 진행 중인 실행에 추가 지시를 밀어 넣는 Steer(조타)다.

    diagram

    다이어그램 설명. 위 그림은 실행 중 사용자가 끼어드는 두 갈래를 보여준다. 가운데 판단 지점에서 왼쪽으로 가면 중단이다 — Stop 버튼을 누르거나 그냥 "stop"이라고 입력하면 chat.abort가 호출돼 실행이 즉시 멈춘다. 이때 중단 시점까지 만들어진 부분 답변은 버려지지 않고 기록에 남아, 에이전트가 어디까지 갔는지 사후에 확인할 수 있다. 오른쪽으로 가면 조타다 — 실행 중에 보낸 후속 메시지는 기본적으로 다음 차례로 대기열에 쌓이지만, 그 메시지에 Steer를 누르면 지금 돌고 있는 턴 안으로 곧바로 주입돼 에이전트의 방향을 실시간으로 틀 수 있다. 왜 둘을 나눴나 — 중단은 "처음부터 다시"이고 조타는 "가던 길을 수정"이라 성격이 전혀 다르다. 명백히 틀렸으면 멈추고, 거의 맞는데 한 가지만 더하면 될 땐 조타한다. 함정은 후속 메시지의 기본 동작이다 — 실행 중 메시지는 자동으로 끼어드는 게 아니라 대기열에 쌓일 뿐이며, Steer를 명시적으로 눌러야 현재 턴에 주입된다. 이 구별이 없으면 무심코 친 메시지가 진행 중인 작업을 멋대로 흔들게 된다.

    결과 — 무엇이 좋아졌나

    비동기 ACK와 이벤트 스트림 구조가 들어오면서, 앞서 본 세 가지 문제가 이렇게 정리된다.

    최종 답만 보여주던 방식 WebSocket 이벤트 스트림 방식
    20초간 화면 멈춤 — 죽었는지 일하는지 모름 도구 카드가 실시간으로 흘러 진행 상황이 보임
    엉뚱한 답의 원인을 추적 불가 어느 도구가 무엇을 읽고 뭘 반환했는지 카드로 확인
    잘못 가도 끝날 때까지 못 멈춤 Stop으로 중단, Steer로 진행 중 방향 수정
    새로고침하면 대화 유실 라이브 스트림 + 영구 transcript 병합으로 유지

    정리하면, OpenClaw의 Chat 메뉴는 단순한 "웹 채팅창"이 아니라 에이전트의 작업 과정을 실시간으로 들여다보고 필요하면 끼어드는 관제 창이다. 텔레그램으로도 같은 에이전트와 대화할 수 있지만, 거기선 최종 답만 떨어진다. 자기 코드와 파일을 만지는 에이전트를 믿고 맡기려면 "어떤 도구를 어떤 순서로 썼는가"가 보여야 하고, 잘못 가는 게 보이면 즉시 멈추거나 방향을 틀 수 있어야 한다. 비동기 ACK로 보내기와 받기를 끊고, 도구 호출을 별도 이벤트로 흘리고, 라이브 스트림과 영구 기록을 분리한 이 설계가 바로 그 투명성과 개입 가능성을 가능하게 한 토대다 — 화려한 기능보다 이 "들여다보고 손댈 수 있음"이 사실 에이전트를 실제로 쓸 만하게 만드는 조건이다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.