작성자: Sam LEE (여러분의 개발 코치)
"이해되지 않는 기술은 기술이 아니다."
모든 코드는 우리네 인생(학교, 직장, 도로) 가장 가까운 주제에 빗대어 설명합니다. 기술적 개념이 낯설게 느껴질 때마다, 이 책은 여러분의 일상에서 친숙한 은유(Metaphor) 법을 사용하여 설명할 예정입니다.
이 책의 모든 설명은 다음 4단계(4-Step Analysis)를 따릅니다:
- 은유(Metaphor): 우리 주변의 익숙한 상황(학교, 회사, 신호체계 등)으로 개념을 잡습니다.
- 개념(Concept): 실제 기술 용어를 쉽게 정의합니다.
- 코드(Code): 모든 예제 코드는 주석을 달아 이해가 쉽도록 합니다.
- 배경(Context): 왜 이 기술을 선택했는지, 업계의 뒷이야기를 들려드립니다.
이 책은 코딩의 '코'자만 알아도 이해하고 싶어 하는 열정적인 초심자를 위한 것입니다. 전문 개발자가 아니더라도, 전공자가 아니더라도, 기술의 원리를 깊이 있게 이해하고 싶은 분이라면 누구나 환영합니다.
최근 IT 산업계에서는 신입 개발자 채용을 기피하는 경향이 뚜렷해지고 있습니다. 인공지능의 등장으로 개발자의 역할이 단순 코딩에서 '기획·설계' 중심의 기획개발로 전환되는 가운데, 기업들은 기획력과 문제 해결 능력이 검증된 경력자를 선호하게 된 것입니다. 이러한 현상은 업계의 당연한 수요 변화로 자리 잡았으며, IT 업계 전반으로 빠르게 확산되고 있는 실정입니다.
이에 필자는 스스로에게 묻게 되었습니다. 신입 사원의 경험 부재라는 문턱을 어떻게 하면 극복하고, 단기간에 경력자 수준의 실전 역량을 갖출 수 있을까? 실제로 수많은 신입 지원자들의 면접과 코딩 테스트를 진행하면서, 이들의 어려움에 깊은 공감과 안타까움을 느꼈습니다. 과연 이들에게 희망적인 대안은 없는 것일까.
현실은 더욱 녹록지 않습니다. 기업들은 인턴 채용마저 축소하고 있어, 실무 경험을 쌓을 수 있는 기회조차 점차 좁아지고 있습니다. 바로 이러한 구조적 문제의식에서 본서를 기획하게 되었습니다.
필자는 이 책을 통해 초보 개발자, 신입 지망생, 그리고 비전공자이나 개발에 대한 열정을 품은 모든 이들에게 경력자들이 수년에 걸쳐 체득한 실전 경험과 노하우를 압축하여 전달하고자 합니다. 단순한 이론서가 아닌, 현장에서 마주하는 문제들을 직접 해결하며 몸으로 익히는 '선배의 비밀노트' 를 만들고 싶었습니다. 그리고 그 핵심 방법론으로 리버스 엔지니어링(Reverse Engineering) 이라는 역발상의 접근법을 제시하고 싶었습니다.
이미 만들어진 시스템을 분해하고 분석함으로써 설계자의 의도와 구조적 원리를 빠르게 파악하는 이 방법은, 신입 개발자가 단기간에 경력자 수준의 통찰력을 갖출 수 있는 가장 효과적인 경로입니다. 이 책이 여러분의 실력 향상에 든든한 동반자가 되기를 바랍니다.
컴퓨터공학 전공자이든 비전공자이든, 개발자의 꿈을 품은 모든 분들께 진심 어린 응원의 마음을 전합니다. 어려운 환경일수록 스스로의 성장을 위해 끊임없이 노력하시길 바랍니다. 필자는 이 책을 통해 여러분의 도전을 끝까지 지원하겠습니다. 힘내십시오.
AI 혁명은 결코 여러분의 직업을 빼앗아 가지 못합니다. 끊임없이 생기는 새로운 문제를 해결해 나가려는 열정은 인공지능에게는 없기 때문입니다. 이 책을 밤새 탐독하며 인공지능을 여러분의 것으로 만드는 그날까지, 본서가 조금이나마 용기가 되길 기원합니다.
이 문서는 OpenClaw(Moltbot) 이라는 2026년 시작과 함께 불어닥친 거대한 소프트웨어 파장을 “밑바닥부터(from scratch)” 다시 파헤치는 과정을 담은 건축 일지입니다. 단순히 인공지능이 코드를 생성하는 것만 “복사-붙여넣기” 하는 것이 아니라, “왜 여기에 기둥을 세웠지?”, “이 배관은 어디로 연결되지?” 를 집요하게 파헤치고 사고하고자 작성되었습니다.
인공지능 혁명의 물결 속에서 코딩은 전통적으로 인간만의 전유물이었던 시대를 지나, 이제는 인간과 기계가 협력하는 새로운 패러다임으로 진화하고 있습니다. 그러나 우리는 한 가지 근본적인 진리를 간과해서는 안 됩니다. 현존하는 모든 인공지능 시스템이 궁극적으로는 인간의 코드로 구현된 산물이라는 사실을 말입니다.
코드의 정확한 의미와 작동 원리를 이해하지 못한 채, 인공지능이 생성하는 환각(hallucination)을 비판적으로 검토하지 않고 수용한다면, 이는 우리에게 잠재적 위험이자 반드시 극복해야 할 중대한 과제가 될 것입니다.
본서는 이러한 시대적 상황을 인식하고, 인공지능을 효과적인 학습 도구로 활용하여 코딩의 본질을 깊이 있게 이해하고 리버스 엔지니어링(1) 역량을 배양하고자 하는 독자들을 위한 실전 지침서로 제작하였습니다.
기술적인 이야기로 넘어가기 전에, 제(Sam LEE) 옛날이야기를 하나 해드릴까 합니다.
제가 개발자로 밥을 먹고 산 지 어느덧 20년이 다 되어갑니다. 많은 신입 개발자분들이 저에게 묻습니다. "팀장님, 어떤 알고리즘을 공부해야 개발을 잘하나요?" "수학을 잘해야 코딩을 잘하나요?"
그럴 때마다 저는 웃으며 대답합니다. "개발 생활의 8할은 '알고리즘'이 아니라 '눈치'와 '통역'이란다."
실제로 현업에서 가장 골치 아픈 문제는 '어려운 로직' 을 짜는 게 아닙니다. 바로 '말이 안 통하는 두 시스템을 연결하는 것' 입니다.
- 기획팀은 "한국어"로 말하고, 개발팀은 "외계어(Code)"로 말합니다.
- 옆 부서 김 대리님이 만든 서버는
JSON을 달라고 하는데, 우리 팀 박 과장님이 만든 앱은XML을 던집니다. - 이 사이에서 "아니, 그게 아니라요..." 하며 중간 다리를 놓는 과정. 그것이 개발자가 겪는 매일매일의 스트레스이자, 가장 중요한 업무입니다.
우리가 지금 만들려고 하는 OpenClaw(Moltbot) 도 똑같습니다. 이 친구는 일종의 "통역사(Translator)" 입니다.
- WhatsApp: "나는 내 방식대로 말할 거야!"
- Slack: "나도 내 규칙이 있어!"
- AI(ChatGPT): "나는 텍스트만 주면 답할게."
이 제각각인 녀석들 사이에서 등 터지지 않고, 능수능란하게 말을 전달해 주는 '센스 있는 중재자'. 그것이 바로 Gateway입니다.
그러니 앞으로 나올 복잡한 그림들을 보고 겁먹지 마세요. "아, 이 코드는 지금 WhatsApp이랑 Slack 사이에서 통역하느라 땀 뻘뻘 흘리고 있구나." 이렇게 생각하면, 차가운 코드 덩어리가 조금은 사람 냄새 나고 귀여워 보일 겁니다.
자, 그럼 이제 이 기특한 통역사의 가방 속에 어떤 도구들이 들어있는지 한번 열어볼까요?
이 앱(OpenClaw)은 단순한 챗봇이 아닙니다. 다양한 메신저(WhatsApp, Slack 등)와 AI 모델을 연결하는 Gateway Platform(게이트웨이 플랫폼) 으로서, AI 에이전트(Agent) 생태계의 혁신을 이끌며 2026년 1월 초급속도로 확산되고 있는 업계 최대 관심사 중 하나가 되었습니다.
이 장에서는 OpenClaw(Moltbot)을 구성하는 핵심 기술들을 리버스 엔지니어링(Reverse Engineering) 관점에서 심층 분석합니다. 단순히 기술을 나열하는 데 그치지 않고, 각 기술이 선택된 배경과 설계 철학을 파헤치며, 독자 여러분이 이 기술들을 '내 것' 으로 체득할 수 있도록 충분한 예제와 실습 과제를 제공할 것입니다.
| 구분 | 기술 / 도구 | 버전 (권장) | 선정 이유 (Why?) |
|---|---|---|---|
| Runtime | Node.js | >=22.12.0 | 최신 LTS. 비동기 I/O 처리가 핵심인 게이트웨이 서버에 최적화됨. |
| Language | TypeScript | ^5.9.3 | 거대한 모노레포 프로젝트의 타입 안정성을 보장하는 엄격한 문법 검사기. 인터페이스 기반 설계 필수. |
| Package Manager | pnpm | 10.23.0 | 모노레포 구조에서 디스크 공간 절약 및 빠른 설치 속도. |
| Server Core | Hono (+ WS) | 4.11.4 | Express보다 가볍고 빠르며, Edge Runtime 호환성이 좋음. HTTP/WebSocket 통합 용이. |
| Channel Layer | Baileys | 7.0.0-rc.9 | WhatsApp Web API를 리버스 엔지니어링한 라이브러리. 브라우저 없이 Node.js에서 WhatsApp 구동 가능. |
| AI Agent | Pi Agent Core | 0.49.3 | LLM(Large Language Model)과의 통신, 도구(Tool) 호출, 컨텍스트 관리를 담당하는 자체/외부 라이브러리. |
| UI Framework | Lit + Vite | 3.3.2 | React보다 가벼운 Web Components 표준 기반. 봇 제어 패널용으로 가볍고 빠름. |
| Database/State | SQLite | - | 로컬 임베딩 벡터 저장 및 경량 상태 관리에 적합. |
표에 나온 외계어들을 시원하게 풀어드립니다.
- LTS (Long Term Support): "장기 지원 버전". 자동차로 치면 '신형 쏘나타' 가 아니라 '택시 모델 쏘나타' 입니다. 최신 기능은 적지만, 고장이 안 나고 부품을 5년 이상 책임지고 공급해 줍니다. 서버는 멋진 기능보다 '안 죽는 것' 이 중요해서 LTS를 씁니다.
- 모노레포 (Monorepo): "Mono(하나) + Repo(저장소)". '한 지붕 대가족' 입니다. 서버 코드, 앱 코드, 관리자 페이지 코드를 폴더 하나에 다 몰아넣고 관리하는 방식입니다. 반대말은 '멀티레포(각방 쓰기)'입니다.
- 리버스 엔지니어링 (Reverse Engineering): "역공학". 맛집 떡볶이 소스 비법을 알아내려고, 다 만들어진 떡볶이를 현미경으로 들여다보고 성분을 분석하는 행위입니다. 우리는 완성된 OpenClaw 코드를 뜯어보며 설계를 유추할 것입니다.
- 컨텍스트 (Context): "문맥/정황". AI는 금붕어입니다. 방금 한 말을 잊어버리죠. 그래서 "아까 네가 사과라고 했잖아"라고 이전 대화 내용 전체를 다시 던져줘야 알아듣습니다. 이 '대화의 족보'를 컨텍스트라고 부릅니다.
- React: 메타(페이스북)에서 만든 '레고 블록 조립 세트' 입니다. "로그인 버튼", "메뉴바" 같은 블록을 조립해서 웹사이트를 만듭니다. 가장 유명하지만 조금 무겁습니다.
- Web Components: React 같은 특정 브랜드 레고가 아니라, '국제 표준 규격 벽돌' 입니다. 브라우저 자체가 기본으로 지원하는 기능이라서, React나 Vue 같은 라이브러리 없이도 돌아갑니다. 가볍고 빠릅니다.
- 로컬 임베딩 벡터 저장 (Local Embedding Vector Storage):
- 임베딩: "사과"라는 글자를
[0.1, 0.5, 0.9]같은 숫자 좌표로 바꾸는 것.- 벡터 저장: 이 숫자를 저장해 두는 도서관.
- Why?: AI에게 "과일 찾아줘"라고 하면, AI는 '과일'의 숫자 좌표와 가장 가까운 '사과'의 좌표를 계산해서 찾아냅니다. 이걸 내 컴퓨터(로컬)에서 한다는 뜻입니다.
- LLM (Large Language Model): "거대 언어 모델".
- 은유: "전 세계 도서관의 책을 통째로 삼킨 암기 천재".
- 설명: ChatGPT나 Claude 같은 AI의 본체입니다. 수조 개의 문장을 학습하여 인간처럼 대화하고 추론할 수 있는 '초거대 AI 뇌'라고 생각하면 됩니다. 단순히 검색하는 것이 아니라 문맥을 이해하고 새로운 문장을 창조합니다.
- CLI (Command Line Interface): "명령줄 인터페이스".
- 은유: "컴퓨터와 나누는 문자 메시지".
- 설명: 마우스로 아이콘을 클릭하는 방식(GUI)이 '그림 위주의 소통'이라면, CLI는 검은 화면(터미널)에 직접 글자를 타이핑해서 컴퓨터에게 명령을 내리는 방식입니다. 영화 속 해커들이 쓰는 방식이 바로 이것이며, 자동화와 정밀한 조작에 유리합니다.
- API (Application Programming Interface): "프로그램 간의 약속".
- 은유: "식당의 메뉴판". 손님이 주방의 요리 과정을 몰라도 메뉴판만 보고 주문할 수 있듯이, 한 프로그램이 다른 프로그램의 기능을 쉽게 빌려 쓸 수 있게 만든 전달 통로이자 규칙입니다.
- API 서버 (API Server): "데이터/기능 배달 전문점".
- 은유: "24시간 주문 대기 중인 배달 전문 식당". 외부에서 API 규칙(메뉴)에 맞게 주문(요청)을 보내면, 그에 맞는 데이터나 서비스를 즉시 배달(응답)해주는 서버입니다.
💬 코치 코멘트(Sam LEE): "용어가 계속 나오지만, 깊게 파고들지 말고 '아, 대충 이런 거구나' 하고 넘어가세요. 계속 새로운 개념들과 경험이 쌓이다 보면, 이 낯선 용어들은 자연히 여러분의 몸속에 녹아들어 '실력' 이라는 형태로 나타나게 되어 있습니다. 믿고 따라오세요."
초보자도 따라 할 수 있도록 각 도구의 설치 방법을 정리했습니다.
-
Node.js: 자바스크립트 실행기. (건설 현장의 인부)
- 설치법 (macOS/Linux):
nvm이나fnm을 추천합니다.# fnm 설치 (Fast Node Manager) curl -fsSL https://fnm.vercel.app/install | bash # Node.js 22버전 설치 fnm install 22
- 설치법 (Windows): 공식 홈페이지(nodejs.org)에서 LTS 버전을 다운로드하여 설치.
- 설치법 (macOS/Linux):
-
pnpm: 효율적인 패키지 매니저. (자재 관리 소장)
- 설치법: Node.js가 깔린 상태에서 아래 명령어를 입력.
npm install -g pnpm # 버전 확인 pnpm -v
- 설치법: Node.js가 깔린 상태에서 아래 명령어를 입력.
-
TypeScript: 거대한 모노레포 프로젝트의 타입 안정성을 보장하는 엄격한 문법 검사기. (감리사)
- 설치법: 프로젝트마다 로컬로 설치하는 것이 정석입니다.
pnpm add -D typescript
- 은유 (Metaphor): "철저한 설계도 감리사". 건물을 짓기 전 설계도가 법규에 맞는지, 자재가 규격품인지 꼼꼼히 체크하여 나중에 건물이 무너지는 대형 사고를 막아줍니다.
- 설치법: 프로젝트마다 로컬로 설치하는 것이 정석입니다.
-
Hono: 초고속 웹 프레임워크. (건물 뼈대)
- 설치법: 프로젝트 시작 후 설치.
pnpm add hono @hono/node-server
[!NOTE] 🛑 [배경 지식] "프로젝트 시작 후 설치"란 무슨 뜻인가요?
"전입신고를(집을 계약) 해야 가구를 들여놓을 수 있습니다."
우리가 인터넷 쇼핑몰에서 가구를 주문하려면, 배송받을 '우리 집 주소' 가 먼저 확정되어야 합니다.
- 프로젝트의 탄생(
pnpm init): 이것은 컴퓨터에게 "지금부터 이 폴더는 'OpenClaw'라는 이름을 가진 하나의 독립된 하우스(프로젝트)다!" 라고 선포하고, 현관문에 문패(package.json) 를 다는 작업입니다. - 설치(
pnpm add): 이제 문패가 달렸으니, 택배 기사(pnpm)에게 "Hono라는 소파를 이 집 장부(package.json)에 등록하고 배송해주세요" 라고 요청할 수 있게 된 것입니다. - 결론: 문패(
package.json)도 없는 허허벌판에는 택배를 시킬 수 없습니다. 그래서 항상 "문패 달기(init) -> 가구 들이기(add)" 순서를 지켜야 합니다.
[!NOTE] 🏎️ [기술 해부] Hono가 도대체 뭔가요? (vs Express)
"웹 서버 만들 때 다들
Express쓴다던데, 왜 처음 들어보는Hono를 쓰나요?"1. 은유(Metaphor): 중형 세단 vs F1 레이싱카
- Express (중형 세단): 2010년에 나온 베스트셀러. 편안하고 익숙하지만, 무겁고 느립니다. 엔진이 옛날 방식이라 최신 도로(Edge Computing)에서는 잘 못 달립니다.
- Hono (F1 레이싱카): 2022년에 나온 최신 머신. 불필요한 기능 다 떼버리고 오직 '속도'와 '표준'에만 집중했습니다.
2. 결정적 차이 (Web Standard)
- Express: 자기들만의 독자 규격(
req,res)을 씁니다. Node.js 밖으로 나가면 숨을 못 쉽니다. - Hono: 전 세계 웹 표준 규격(
Request,Response) 을 그대로 씁니다. Node.js뿐만 아니라 AWS, Cloudflare 어디서든 돌아갑니다. 게다가 5배 빠릅니다.
-
Baileys: WhatsApp 연결 라이브러리. (해커의 무전기)
- 설치법:
pnpm add @whiskeysockets/baileys - 은유(Metaphor): "해커의 무전기". WhatsApp은 공식적으로 봇을 만드는 걸 까다롭게 굽니다(비즈니스 API 유료, 승인 절차 복잡). Baileys는 우리가 마치 진짜 'WhatsApp 웹' 브라우저인 척 가장해서 서버와 통신하게 해주는 무전기입니다.
- Why?: 이 "특수 부품"이 없으면 여러분은 페이스북(Meta) 본사에 사업자 등록증 내고, 승인받고, 돈을 내야 봇을 돌릴 수 있습니다. Baileys는 그 모든 과정을 건너뛰고 "지금 당장" 봇을 돌리게 해주는 마법의 열쇠입니다.
- 설치법:
-
SQLite: 데이터베이스. (캠핑용 배낭)
- 설치법:
pnpm add better-sqlite3 - 은유(Metaphor): "캠핑용 배낭". 보통 DB(MySQL, Oracle)라고 하면 거대한 컨테이너 트럭을 떠올립니다. 시동 거는 데만 한 세월이죠. SQLite는 그냥 파일 하나(
database.sqlite)가 곧 DB입니다. 가방에 쏙 들어갑니다. - Why?: OpenClaw(Moltbot)은 라즈베리 파이 같은 작은 컴퓨터에서도 돌아가야 합니다. 덩치 큰 DB 서버를 따로 띄우는 건 사치입니다. "파일 하나만 복사하면 이사 끝" 인 간편함 때문에 이 특수 부품을 씁니다.
- 설치법:
[!TIP] 🖥️ [실습 가이드] VSCode에서 프로젝트 0부터 세팅하기 (2025년 12월 버전)
"명령어는 알겠는데, 도대체 어디에 치라는 건가요?" 막막해하실 분들을 위해, 마우스 클릭 하나하나까지 다 알려드립니다. 천천히 따라오세요.
🤔 [잠깐] VSCode가 뭔가요? 메모장이랑 뭐가 달라요?
- 메모장(Notepad): 단순히 글자만 적는 '과도(과일 깎는 칼)' 입니다. 사과 하나 깎을 땐 좋지만, 요리는 못 합니다.
- VSCode (Visual Studio Code): 마이크로소프트가 만든 '최첨단 셰프의 주방' 입니다.
- 재료 색깔 표시: 코드가 알록달록하죠? 소금인지 설탕인지 헷갈리지 말라고 색칠해 주는 겁니다. (Syntax Highlighting)
- 보조 셰프:
consol만 쳐도console이라고 추천해 줍니다. (Auto-completion) - 상황실 통합: 코드를 짜면서 바로 서버를 켜고 끄는 버튼(터미널)이 내장되어 있습니다.
- 결론: 개발을 처음 한다면 무조건 이걸 쓰세요. 무료이고, 전 세계 개발자의 70%가 씁니다.
1. 폴더 만들기 (집터 닦기)
-
먼저 컴퓨터 하드디스크(C드라이브 혹은 홈 디렉토리)에
Workspace라는 이름의 폴더를 만드세요. -
그
Workspace폴더 안에 들어가서, 다시openclaw-lab이라는 새 폴더를 만듭니다.- 구조:
내 컴퓨터->Workspace->openclaw-lab
- 구조:
-
이
openclaw-lab폴더가 앞으로 우리의 베이스캠프가 됩니다.[!TIP] 💡 [Coach's Tip] 왜 굳이
Workspace폴더를 만드나요?개발을 하다 보면 "어? 제 파일 어디 갔죠?" 하고 길을 잃는 경우가 정말 많습니다. 윈도우(C드라이브) 사용자, 맥(홈) 사용자 모두
Workspace라는 공통된 약속 장소를 정해두면, 나중에 "경로를 못 찾겠어요" 같은 문제를 99% 예방할 수 있습니다. 이것은 전 세계 개발자들의 '국룰(불문율)' 같은 습관이며, 경로 문제로 헷갈릴 일을 원천 봉쇄해 줍니다.
2. VSCode 열기 (입주)
- VSCode를 실행합니다.
- 방금 만든
openclaw-lab폴더를 마우스로 끌어서(Drag & Drop) VSCode 화면 정중앙에 놓으세요. - 또는 메뉴에서
File->Open Folder...를 눌러서 찾아도 됩니다. - 왼쪽 탐색기(Explorer)에 폴더 이름이 보인다면 입주 성공입니다.
3. 터미널 열기 (상황실 개방)
- 이제 검은 화면, 터미널(Terminal) 을 열 차례입니다. 이곳이 바로 우리가 컴퓨터에게 직접 명령을 내리는 대화창입니다.
- 단축키:
Ctrl+`(물결표시 키, 맥/윈도우 공통) - 메뉴: 상단 메뉴바
Terminal->New Terminal - 하단에 패널이 열리면서 깜빡이는 커서가 보이나요? 준비 완료입니다.
4. 명령 하달 (Start)
- 이제 아래 명령어를 한 줄씩 복사해서 터미널에 붙여넣고 엔터(Enter) 를 치세요.
# 1. 호적 등본(package.json) 만들기 pnpm init # 2. 필수 자재(라이브러리) 주문하기 pnpm add hono @hono/node-server @whiskeysockets/baileys better-sqlite3 # 3. 설계 도구(TypeScript) 주문하기 (개발용이라 -D 옵션 붙임) pnpm add -D typescript @types/node
5. 확인 사살 (Check)
- 왼쪽 탐색기에서
package.json파일을 클릭해보세요. - 파일 내용이 아래와 비슷하다면 성공입니다. (버전 숫자는 조금 댤라도 괜찮습니다.)
{ "name": "openclaw-lab", "version": "1.0.0", "dependencies": { "@hono/node-server": "^1.x.x", "@whiskeysockets/baileys": "^6.x.x", "better-sqlite3": "^11.x.x", "hono": "^4.x.x" }, "devDependencies": { "@types/node": "^22.x.x", "typescript": "^5.x.x" } }- 축하합니다! 성공적으로 환경 세팅을 마치셨습니다. 여기까지 따라오시느라 고생 많으셨습니다.
- 설치법: 프로젝트 시작 후 설치.
가장 먼저 튼튼한 기반을 다집니다. 이 프로젝트는 모든 구성 요소(서버, 앱, 플러그인)가 하나의 저장소에 있는 Monorepo 구조를 따릅니다. (모노레포가 뭔지 까먹으셨다면? 👉 상단 '용어 미니 사전 2번' 을 다시 보고 오세요!)
-
Node.js 설치:
v22.12.0이상 필수.- 설명: Node.js는 자바스크립트 실행기로, 건설 현장의 작업자와 같은 역할을 합니다. 최신 LTS(Long Term Support, 장기 지원) 버전을 사용하는 이유는 비동기 I/O(입출력) 처리가 핵심인 게이트웨이 서버에 최적화되어 있기 때문입니다.
- 설치 방법 (OS별 가이드):
- 🪟 Windows: 공식 홈페이지 (https://nodejs.org/) 에서 LTS 버전(v22.x) 을 다운로드하여 설치합니다. (
Next만 계속 누르면 됩니다) - 🍎 Mac: 터미널에
brew install node@22입력. (Homebrew가 없다면 여기 (https://brew.sh/index_ko) 참조) - 🐧 Linux:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs
- 🪟 Windows: 공식 홈페이지 (https://nodejs.org/) 에서 LTS 버전(v22.x) 을 다운로드하여 설치합니다. (
[!TIP] 🎓 [개념 수업] 비동기 I/O (Asynchronous I/O)가 뭔가요?
"맛집 웨이터의 서빙 방식" 으로 이해하면 쉽습니다.
- 동기(Synchronous = Blocking): 웨이터가 손님 1명에게 주문을 받습니다. 손님이 "음... 뭐 먹지?" 하고 고민하는 5분 동안 웨이터는 아무것도 안 하고 옆에 서 있습니다. 다른 손님들은 기다리다 지쳐 나갑니다.
- 비동기(Asynchronous = Non-Blocking): 웨이터가 주문을 받다가 손님이 고민하면 "결정되시면 벨 눌러주세요" 하고 다른 테이블로 가서 일을 합니다. 그러다 벨이 울리면(이벤트 발생) 다시 와서 주문을 받습니다.
Why?: OpenClaw 게이트웨이는 수천 명의 사용자가 동시에 메시지를 보냅니다. 한 명 메시지 처리하느라 다른 사람들을 기다리게 하면 안 되겠죠? 그래서 Node.js의 비동기 방식이 필수입니다.
-
Language: TypeScript (^5.9.3):
- 설명: TypeScript(타입스크립트)는 거대한 모노레포(Monorepo, 단일 저장소) 프로젝트의 타입 안정성을 보장하는 엄격한 문법 검사기입니다. 인터페이스 기반 설계가 필수적인 대규모 프로젝트에서 타입 오류를 사전에 방지합니다.
- 설치 방법: 윈도우/맥 공통 터미널에서 아래 명령어를 입력하세요.
npm install -g typescript # 설치 확인 tsc -v - 은유 (Metaphor): "철저한 설계도 감리사".
- 일반 자바스크립트가 "일단 건물을 짓고 나중에 무너지면 고치자"는 식이라면, 타입스크립트는 "벽돌 하나를 쌓을 때마다 설계도(Type)와 일치하는지 확인" 합니다. 조금 귀찮을 수 있지만, 나중에 건물이 무너지는(런타임 에러) 대형 사고를 99% 막아줍니다.
- 실사용 예제 (Concept Class):
- 자바스크립트 (무책임):
[사과] + 100 = "사과100"(이상하지만 에러 없이 진행됨) - 타입스크립트 (책임감): "잠깐! 과일(String)에 숫자(Number)를 더하다니요? 말도 안 됩니다!" 하고 빨간 줄을 쫙 그어줍니다.
- 자바스크립트 (무책임):
-
pnpm 설치:
npm install -g pnpm@10.23.0.
"npm도 잘 되는데 굳이 왜 이걸 또 깔아야 하나요?" 라는 질문은 매우 타당합니다. 하지만 OpenClaw(Moltbot) 같은 대규모 프로젝트(Monorepo)에서 npm을 쓰면 재앙이 닥칩니다.
- npm (비효율의 끝판왕): 학생 100명이 "해리포터" 책을 읽어야 한다고 합시다. npm 방식은 100권의 책을 새로 사서 각 학생의 가방에 넣어주는 것입니다. 가방(디스크)이 터지고, 책값(설치 시간)이 엄청 듭니다.
- pnpm (효율적 공유): 학교 도서관에 "해리포터" 딱 한 권만 사놓습니다. 학생들에게는 책 위치가 적힌 쪽지(Symlink) 만 줍니다. 읽을 때는 도서관 책을 참조합니다. 100명이든 1000명이든 책은 한 권이면 됩니다.
- OpenClaw(Moltbot) 프로젝트는 수백 개의 라이브러리를 씁니다.
- npm 사용 시: 프로젝트를 복사할 때마다 1GB가 넘는
node_modules폴더가 계속 복제됩니다. 하드디스크가 순식간에 꽉 찹니다. - pnpm 사용 시: 100개를 복사해도 용량은 거의 늘어나지 않습니다. (Content Addressable Store 방식)
- npm의 유령(Phantom Dependencies): npm은 내가 설치하지 않은 라이브러리도 실수로 쓸 수 있게 허용하는 버그(혹은 관대함)가 있습니다. 나중에 배포할 때 "어? 내 컴퓨터에선 되는데 서버에선 안 돼요!" 하는 지옥을 맛보게 됩니다.
- pnpm: 내가
package.json에 명시한 것만 정확히 쓸 수 있게 막아줍니다. "정의되지 않은 것은 없다" 는 철학입니다.
"개발자들은 왜 자꾸
npm npm거리는 걸까요?"- 아이폰 앱스토어: 카카오톡이 필요하면 우리가 직접 코딩해서 만들지 않죠? 앱스토어에서 다운로드 받습니다.
- npm (Node Package Manager): 개발할 때 "달력 기능"이나 "암호화 기능"이 필요하면 직접 짜지 않고 npm이라는 무료 부품 상점에서 다운로드 받습니다.
- 여러분이 "성(Castle)" 을 만든다고 상상해 보세요.
- 플라스틱을 녹여서 블록 하나하나를 직접 만드는 건 미친 짓입니다. (이게 npm 없이 개발하는 것)
- npm은 "문", "창문", "성벽" 블록을 미리 만들어 파는 가게입니다. 우리는
npm install castle-wall명령어 한 줄로 성벽 블록을 가져와서 조립만 하면 됩니다.
- 시간 절약: 남들이 10년 동안 만든 검증된 코드를 1초 만에 가져와 씁니다.
- 검증된 품질: 전 세계 수백만 명이 같이 고쳐나가는 코드라서, 내가 짠 코드보다 튼튼할 확률이 높습니다.
npm install [부품이름]: "이 부품 우리 집(프로젝트)에 배달해 줘."pnpm install: "이 부품 배달해 주는데, 아까 말했듯이 더 효율적으로(도서관 방식) 배달해 줘."
-
프로젝트 생성 (집터 닦기):
- 먼저 컴퓨터 하드디스크(C드라이브 혹은 홈 디렉토리)에
Workspace라는 이름의 폴더를 만드세요. - 그
Workspace폴더 안에 들어가서, 다시openclaw-lab이라는 새 폴더를 만듭니다.- 구조:
내 컴퓨터->Workspace->openclaw-lab
- 구조:
 <!-- slide --> - 터미널에서 해당 폴더로 이동한 뒤, 정보를 기입합니다.
cd ~/Workspace/openclaw-lab pnpm init
[!TIP] 💡 [Coach's Tip] 왜 굳이
Workspace폴더를 만드나요?개발을 하다 보면 "어? 제 파일 어디 갔죠?" 하고 길을 잃는 경우가 정말 많습니다. 윈도우(C드라이브) 사용자, 맥(홈) 사용자 모두
Workspace라는 공통된 약속 장소를 정해두면, 나중에 "경로를 못 찾겠어요" 같은 문제를 99% 예방할 수 있습니다. 이것은 전 세계 개발자들의 '국룰(불문율)' 같은 습관이며, 경로 문제로 헷갈릴 일을 원천 봉쇄해 줍니다. - 먼저 컴퓨터 하드디스크(C드라이브 혹은 홈 디렉토리)에
많은 분들이 "일단 server.ts 파일 만들고 코드부터 치면 안 되나요?" 라고 묻습니다. 결론부터 말하면, OpenClaw(Moltbot) 같은 거대한 프로젝트에서는 불가능합니다. 그 이유를 아주 쉽게 설명해 드립니다.
- 일반적인 코딩 (HTML/JS): 맨땅에 텐트를 치는 것과 같습니다. 그냥 폴대(
index.html) 꽂고 천(script.js) 덮으면 집이 됩니다. 규칙이 없어도 됩니다. - OpenClaw(Moltbot) (TS/ESM): 100층짜리 마천루를 짓는 것과 같습니다. 땅을 파기 전에 "여기는 상업지구이며, 내진 설계 기준은 7.0이고, 철근은 KS규격을 써야 한다" 는 법적/행정적 허가(
tsconfig.json) 가 먼저 떨어져야 합니다. 허가증 없이 철근(import)을 하나라도 세우면 공사 중지 명령(Syntax Error)이 떨어집니다.
- 동네 축구 (Legacy JS Project): 심판도 없고 규칙도 대충입니다. "손만 안 쓰면 돼." 바로 공 차면 됩니다.
- 월드컵 (Modern TypeScript Project): 경기 시작 전에 "오프사이드 반칙 기준", "VAR 판독 여부", "교체 선수 숫자"가 룰북(
tsconfig.json)에 정의되어야 합니다. OpenClaw(Moltbot)은 월드컵 결승전급의 엄격한 규칙 하에 돌아가는 시스템입니다.
만약 설정 없이 바로 코드를 짠다면?
- 개발자: (자신있게)
import { Server } from 'http'; - 컴퓨터(Node.js): "잠깐,
import가 뭐죠? 저는 옛날 방식(require)밖에 모르는데요?" -> 에러 발생 (Crash) - 개발자: (당황하며) "어? 요즘 다 이거 쓰던데?"
- 컴퓨터: "그건
tsconfig.json이라는 번역기가 있을 때 얘기입니다. 저한테 미리 언질을 안 주셨잖아요."
결론: tsconfig.json을 먼저 만드는 것은 컴퓨터에게 "지금부터 우리는 최신 유행어(ESM)와 엄격한 문법(TypeScript)으로 대화할 거야. 준비해." 라고 선전포고를 하는 것입니다. 이 합의가 없으면 단 한 줄의 코드도 실행되지 않습니다.
---
### 1-3. 🛠️ [실전 가이드] tsconfig.json 설정: 새로운 나라의 헌법 제정
"Moltbot은 대형 프로젝트라서 이미 설정이 다 되어있지만, 맨땅에서 시작하는 나만의 작은 앱은 어떻게 시작하나요?"
1. 은유(Metaphor): 새로운 국가를 세울 때의 "헌법 제정"
백지에 그림을 그리는 것과 같습니다. 나라를 처음 세울 때, 우리 국민은 어떤 언어를 쓰고(Target), 주변 나라와는 어떻게 소통하며(Module), 범죄(Error)에는 얼마나 엄격할 것인지(Strict)를 정하는 **'국가 헌법'**을 만드는 과정이 바로 tsconfig.json 초기화입니다. 헌법이 모호하면 나라가 혼란에 빠지듯, 이 설정이 부실하면 개발 과정 내내 충돌이 일어납니다.
2. 개념(Concept): 초기화의 두 가지 경로 (자동 vs 실전)
TypeScript는 개발자가 처음부터 고생하지 않도록 **'표준 헌법 초안'**을 자동으로 생성해주는 도구(npx tsc --init)를 제공합니다. 초보자는 자동 생성된 초안에서 시작하고, 숙련자는 프로젝트의 목적(고성능 서버, 프론트엔드 등)에 맞춰 그 초안을 수정하여 사용합니다.
3. 코드(Code): 헌법의 탄생과 핵심 3조항
-
방법 A: 표준 초안 자동 생성 (권장) 터미널에 다음 명령어를 입력하면 수많은 주석과 함께 설정 파일이 생성됩니다.
npx tsc --init
-
방법 B: 실전 프로젝트 핵심 설정 (수동 수정) 자동 생성된 파일에서 아래 **'핵심 3개 조항'**만은 반드시 확인하고 수정해 주세요.
{ "compilerOptions": { "target": "ES2022", // [제1조] 사용할 언어의 문법 수준 (최신 사투리 인정) "module": "NodeNext", // [제2조] 소통 규약 (최신 스마트폰 규격 사용) "strict": true // [제3조] 법 집행의 엄격함 (무관용 원칙) } }
4. 배경 및 실전 가이드(Context & Guide): 왜 이 3가지인가?
- 왜
target: ES2022인가?: 과거에는 구식 브라우저를 위해 코드를 어렵게 번역해야 했습니다. 하지막 지금 우리가 만드는 서버(Node.js 22)는 똑똑합니다. 최신 문법을 그대로 써야 속도가 빠르고 코드가 깔끔해집니다. - 왜
module: NodeNext인가?: 자바스크립트 세계는 지금 '구식(CommonJS)'과 '신식(ESM)'이 교체되는 과도기입니다.NodeNext는 "우리는 신식을 쓰되, 구식도 알아듣겠다"는 가장 현명한 중재안입니다. - 왜
strict: true인가?: "대충 짜고 나중에 고치지 뭐"라는 생각은 개발자의 가장 큰 착각입니다. 처음부터 엄격하게 관리하는 것이, 나중에 버그를 찾느라 밤을 새우는 것보다 100배 경제적입니다.
Tip
요약: 작은 프로젝트라도 npx tsc --init으로 시작하고, Target(최신), Module(NodeNext), Strict(True) 이 세 가지만 기억하세요. 이것만으로도 여러분의 프로젝트는 구글이나 메타(Facebook) 부럽지 않은 튼튼한 기초를 갖추게 됩니다.
1. 은유(Metaphor): 건물의 설계 규격과 "헌법(憲法)"
아파트 단지를 지을 때, 모든 건물의 자재 규격이 같아야 하고 배선은 통일된 규칙을 따라야 합니다. tsconfig.json은 이 프로젝트가 어떤 규칙으로 지어지고, 최종적으로 어떤 결과물(JavaScript)로 번역될지를 정하는 최상위 법전이자 설계 도면입니다. 이 도면이 틀리면 아무리 코드를 잘 짜도 집이 무너집니다.
2. 개념(Concept): TypeScript 컴파일러 설정 안내서
이 파일은 tsc(컴파일러)에게 "이 프로젝트의 소스 코드는 여기에 있고, 번역된 결과물은 저기에 저장하고, 번역할 때는 이런 규칙을 지켜줘!"라고 상세히 지시하는 메뉴얼입니다. 단순한 설정 파일이 아니라, 프로젝트의 **정체성(ESM vs CJS)**을 결정짓는 핵심 문서입니다.
3. 코드(Code): 원본 tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"noEmitOnError": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": [
"node_modules",
"dist",
"src/**/*.test.ts",
"src/**/*.test.tsx",
"src/**/test-helpers.ts"
]
}4. 배경 및 초정밀 분석(Context & Micro-Analysis): 설계자의 의도 파악
-
[1]
target: "ES2022"- 은유(Metaphor): "번역기의 목표 언어 버전". (현대어를 조선시대 고어로 번역하면 말이 길어지듯이, 구식 JS로 번역하지 않고 최신 문법을 그대로 씁니다.)
- 설명: TypeScript를 최종적으로 어떤 버전의 JS로 번역할지 정합니다.
- Why?: Node.js 18+ 최신 기능을 네이티브로 활용하여 별도의 polyfill 없이 성능을 최적화하고 코드 길이를 줄이기 위함입니다.
-
[2]
module&moduleResolution: "NodeNext"- 은유(Metaphor): "최신 통신 규약 매뉴얼". (주변 사람들이 다 5G 스마트폰을 쓰는데 우리만 2G 폴더폰을 쓸 수 없듯이, 최신 라이브러리들의 소통 방식에 맞추는 것입니다.)
- 설명: 최신 Node.js의 모듈 시스템(ESM)을 엄격하게 따르도록 지시합니다.
- Why?: Hono, Baileys 같은 최신 라이브러리들은 ESM 전용으로 나오기 때문에, 이 설정이 없으면 아예 프로젝트를 시작할 수도 없습니다.
-
[3]
strict: true- 은유(Metaphor): "절대로 타협하지 않는 깐깐한 감리사". (조금이라도 부실한 시공이 보이면 공사를 아예 중단시킵니다. 귀찮지만 그게 나중에 무너지는 것보다 낫습니다.)
- 설명: 모든 타입 체크 규칙을 가장 엄격하게 적용합니다.
- Why?: 런타임에서 서버가 갑자기 죽는 'Null Pointer Exception' 같은 끔찍한 사고를 코딩 단계에서 원천 봉쇄하기 위한 최고 수준의 안전장치입니다.
-
[4]
outDir: "./dist"- 은유(Metaphor): "완성된 가구만 모아두는 출고 창고". (작업장(src)과 출고장(dist)을 분리하여 관리를 용이하게 합니다.)
- 설명: 컴파일된 결과물(.js)이 저장될 위치를 정합니다.
- Why?: 배포 시 소스 코드는 제외하고 가벼운 결과물만 쏙 뽑아서 Docker 이미지로 만들기 위한 배포 자동화의 기초입니다.
🚦 시작점(Entry Point)과의 연결:
이 설정은 package.json의 "type": "module" 설정과 하나의 유기체처럼 움직입니다.
package.json이 전체 프로젝트의 '모듈 방식'을 ESM으로 선포하면,tsconfig.json이 그 선포에 맞춰 세부 번역 규칙을 집행합니다.- 그 결과,
server.ts에서 최신 라이브러리들을 아무 문제 없이 불러올 수 있게 됩니다.
지금까지 1장: 프로젝트 초기화에서 수행한 모든 단계를 한눈에 정리했습니다. 터미널을 열고 이 순서대로만 진행하셨다면, 여러분의 컴퓨터는 이제 강력한 통역 서버를 가동할 완벽한 준비를 마친 상태입니다.
컴퓨터 어느 곳에서나 쓸 수 있는 필수 도구들을 먼저 설치합니다.
# 1. Node.js 설치 확인 (버전 22 이상)
node -v
# 2. TypeScript 글로벌 설치 (문법 감리사 선임)
npm install -g typescript
# 3. pnpm 글로벌 설치 (자재 관리 소장 영입)
npm install -g pnpm프로젝트가 위치할 물리적인 주소를 정하고 '문패'를 답니다.
# 1. 폴더 생성 및 이동 (집터 닦기)
mkdir -p ~/Workspace/openclaw-lab
cd ~/Workspace/openclaw-lab
# 2. 프로젝트 시작 (호적 만들기)
pnpm init서버를 돌리는 데 필요한 핵심 부품과 설계 도구를 주문합니다.
# 1. 핵심 런타임 및 통신 부품 (Hono, Baileys, SQLite)
pnpm add hono @hono/node-server @whiskeysockets/baileys better-sqlite3
# 2. 개발용 보안 도구 (TS 및 타입 정의)
pnpm add -D typescript @types/node이 건물을 지을 때 지켜야 할 엄격한 규칙을 선포합니다.
# 1. tsconfig.json 파일 생성
npx tsc --init💬 코치 코멘트(Sam LEE):
"축하합니다! 드디어 험난한 기초 공사(1장) 를 무사히 마치셨습니다. 이제 여러분의 폴더에는
package.json과tsconfig.json이라는 든든한 설계도가 놓여 있습니다. 2장부터는 이 설계도를 바탕으로 실제 서버의 '심장'과 '입/귀'를 하나씩 조립해 나갈 것입니다. 준비되셨나요? 가봅시다!"
Gateway는 단순한 서버가 아닙니다. 모든 데이터(메시지, 명령)가 지나가는 고속도로이자, 시스템의 중추 신경망입니다.
"그냥 서버라고 하면 되지, 왜 게이트웨이라는 어려운 말을 쓰나요?"
게이트웨이(Gateway)는 직역하면 "관문" 입니다. 서로 다른 네트워크나 시스템이 만나는 '통로' 역할을 하는 특수 서버를 말합니다. 우리 프로젝트에서 Gateway는 "외부 메신저(WhatsApp 등)와 내부 AI 에이전트 사이의 중계실" 입니다.
- 외부 세상: 미국, 중국, 일본 등 언어와 규격이 다른 여러 나라 (WhatsApp, Slack, Telegram...)
- 공항 (Gateway): 모든 손님은 일단 공항으로 들어옵니다. 여기서 여권을 검사하고, 공통된 서류(표준 데이터)를 작성합니다.
- 내부 도시 (AI Brain): 공항을 통과한 손님은 이제 '대한민국 표준 법규'를 따릅니다. AI는 손님이 어느 나라에서 왔든 상관없이, 공항에서 처리해준 표준 양식만 보고 대화하면 됩니다.
- 언어 통일 (Normalization): WhatsApp의 복잡한 데이터를 AI가 알아듣기 쉬운 깔끔한 텍스트로 바꿔줍니다.
- 보안 검문 (Security): 이상한 명령이나 해킹 시도가 없는지 입구에서 컷(Cut)합니다.
- 교통 정리 (Routing): 질문은 AI에게 보내고, 설정 변경은 데이터베이스로 보내는 등 업무를 배분합니다.
개발자가 단순히 API 서버를 만들지 않고 Gateway 패턴을 도입한 데에는 명확한 건축학적 의도(Architectural Intent) 가 있습니다.
-
추상화와 통일 (The Universal Translator):
- 문제: WhatsApp은
JID를 쓰고, Slack은channel_id를 쓰고, Telegram은chat_id를 씁니다. 메시지 포맷도 제각각(JSON 구조 상이)입니다. 에이전트(LLM)가 이 모든 차이를 알게 하면 코드가 지옥이 됩니다. - 해결: Gateway는 들어오는 모든 요청을 "표준화된 내부 이벤트(Normalized Events)" 로 변환합니다. 에이전트는 상대방이 WhatsApp인지 슬랙인지 알 필요 없이 오직 "메시지가 왔다" 는 사실만 처리하면 됩니다.
[!NOTE] 🎓 개념 수업: 추상화(Abstraction)가 도대체 뭔가요?
"추상화"라는 말이 어렵게 느껴지시나요? 사실 우리는 태어날 때부터 추상화를 하며 살고 있습니다.
1. 정의 (Definition)
- 사전적 의미: 중요한 특징만 남기고, 불필요한 세부 사항을 지우는 것.
- 코딩적 의미: "복잡한 내부 원리는 숨기고, 단순한 사용법(버튼)만 노출하는 것"
2. 은유 (Metaphor): 자동차 운전
- 구체화 (Anti-Abstraction): 엑셀을 밟을 때마다 "연료 밸브를 30도 열고, 공기 흡입량을 10% 늘리고, 스파크 플러그를 점화해라"라고 직접 기계 조작을 해야 한다면? 아무도 운전을 못 할 겁니다.
- 추상화 (Abstraction): 그냥 "오른쪽 페달을 밟으면 -> 앞으로 간다" 라는 사실만 알면 됩니다. 내부 엔진이 휘발유인지 전기인지는 몰라도 됩니다. 이 "페달"이 바로 추상화의 결과물입니다.
3. 코드 예시 (Code Example)
- 나쁜 코드 (구체적): 커피 타는 과정을 일일이 다 적음.
// 물 끓이기 water.temperature = 100; // 원두 갈기 beans.grind(); // 내리기 filter.pour(water); // 컵에 담기 cup.fill();
- 좋은 코드 (추상화): "커피 주세요" 한 마디면 됨.
makeCoffee(); // 끝. (내부에서 무슨 일이 일어나는지는 이 함수를 만든 사람만 알면 됨)
4. 개발자의 지침 (Guideline) 앞으로 코드를 짤 때 항상 스스로에게 물어보세요. "내가 만든 이 코드를 쓸 동료(혹은 미래의 나)에게 이 복잡한 과정을 다 설명해야 하나? 아니면 버튼 하나(
startGatewayServer)만 쥐여주면 되나?"버튼 하나만 쥐여주는 것, 그것이 바로 "추상화를 잘하는 개발자" 입니다.
- 문제: WhatsApp은
-
결합도 감소 (Decoupling):
- 의도: "뇌(Agent)"와 "입/귀(Channel)"를 완벽하게 분리합니다.
- 효과: 나중에
Discord채널을 추가하고 싶을 때, Gateway에 플러그인만 끼우면 됩니다. 에이전트 코드는 단 한 줄도 수정할 필요가 없습니다. 이것이 Micro-Kernel 아키텍처의 핵심입니다.
[!TIP] 🎓 개념 수업: Micro-Kernel(마이크로 커널) 아키텍처란?
"스마트폰과 앱의 관계" 로 이해하면 가장 완벽합니다.
1. 정의 (Definition)
- 핵심 기능(심장)은 아주 작고 단단하게 유지하고, 나머지 기능들은 플러그인(부품) 처럼 끼워 쓰는 방식입니다. 앱스토어 아키텍처라고도 불립니다.
2. 은유 (Metaphor): 멀티탭 (Power Strip)
- 커널(Kernel): 전기가 흐르는 멀티탭 본체입니다. (Gateway의 통신 및 이벤트 관리 로직)
- 플러그인(Plug-in): 멀티탭에 꽂는 TV, 냉장고, 충전기입니다. (WhatsApp 채널, Slack 채널, LLM 엔진 등)
- Why?: 냉장고가 고장 났다고 멀티탭 전체를 버리지 않죠? 그냥 냉장고 코드만 고치거나 플러그를 뽑으면 됩니다. 또 새로운 기계(새로운 메신저)가 나와도 멀티탭에 구멍만 있다면 언제든 꽂아서 전기를 쓸 수 있습니다.
3. 장점 (Benefits in OpenClaw)
- 확장성: 사용자님이 나중에 "저는 텔레그램도 연결하고 싶어요"라고 할 때, 기존 서버 코드를 건드리지 않고 텔레그램용 **'플러그'**만 하나 더 만들어서 꽂으면 끝납니다.
- 안정성: 특정 채널(플러그인)에서 에러가 나도, 본체(커널)는 죽지 않고 다른 채널들을 계속 유지할 수 있습니다.
핵심: 본체는 최대한 가볍게(Micro), 기능은 외부에서(Plugin)! 이것이 OpenClaw가 수많은 메신저를 자유자재로 다루는 비결입니다.
-
상태 및 세션 중앙 관리 (Centralized State):
- LLM은 "기억"이 없습니다. 사용자의 대화 맥락(Context)을 저장하고, 적절한 시점에 잘라서(Pruning) LLM에 넣어주는 기억 관리자 역할을 Gateway가 중앙 통제합니다.
"꼭 이렇게 복잡하게 해야 하나요? 더 쉬운 방법은 없나요?"
- 방식:
server.ts파일 하나에 WhatsApp 코드, LLM 코드, 웹사이트 코드를 다 때려 넣는 방식. - 은유(Metaphor): 북 치고 장구 치는 1인 서커스단. 혼자서 다 하니까 합을 맞출 필요가 없어 시작은 빠릅니다.
- 문제점: "피아노(Discord)"를 추가하려면? 서커스단원 전체가 악기 배우느라 공연을 멈춰야 합니다. 코드가 3,000줄 넘어가면 본인도 못 알아봅니다.
- 방식: AI 에이전트가 WhatsApp 서버, Slack 서버, Telegram 서버에 각각 따로따로 말을 거는 방식.
- 은유(Metaphor): 통역사 없는 국제시장. AI가 독일어(WhatsApp), 영어(Slack), 러시아어(Telegram)를 다 배워야 합니다.
- 문제점: 카카오톡 하나 추가할 때마다 AI 뇌수술을 다시 해야 합니다. (유지보수 지옥)
- 방식: 모든 손님(메시지)은 일단 공항(Gateway)으로 옵니다. 공항에서 검역(Validation)하고, 표준 비자(Standardized Event)를 발급해서 입국시킵니다.
- 승리 요인: AI는 "공항에서 온 손님"만 상대하면 됩니다. 그 손님이 미국에서 왔든 달나라에서 왔든 신경 쓸 필요가 없습니다.
[!IMPORTANT] 이것이 100년 가는 소프트웨어의 비밀입니다.
"파일이 너무 많아요! 어디서부터 건드려야 하죠?" 걱정 마세요. 100개가 넘는 파일 중, 핵심 장기(Core Organs) 5개만 알면 살아있는 서버를 만들 수 있습니다.
src/gateway/
├── server.ts # [1] 얼굴 (Face): 외부에서 부르는 이름표 (Entry Point)
├── server.impl.ts # [2] 뇌 (Brain): 모든 장기를 연결하고 명령을 내리는 곳
├── server-http.ts # [3] 입 (Mouth): HTTP 요청(웹사이트 접속)을 처리
├── server-ws-runtime.ts # [4] 귀 (Ears): WebSocket 요청(실시간 대화)을 처리
└── server-channels.ts # [5] 팔다리 (Limbs): 카톡, 슬랙 같은 실제 도구들을 관리
무작정 전체 코드를 복사하면 에러가 100개 넘게 뜹니다. 우리는 의존성(Dependency) 순서에 따라, 마치 프라모델을 조립하듯 "빈 껍데기(Stub)" 부터 하나씩 만들어 갈 것입니다.
Tip
VS Code 단축키 팁: Cmd + N (Mac) 또는 Ctrl + N (Windows)을 누른 후 Cmd + S / Ctrl + S를 눌러 아래 경로로 저장하세요.
- 역할: 우리 서버의 "공식 연락처"입니다. 외부(CLI/App)에서는 오직 이 파일만 보고 명령을 내립니다.
- VS Code 작업:
src/gateway/폴더에서server.ts파일을 만듭니다. - 필수 코드 (Skeleton):
// 나중에 실제 구현체(impl)에서 가져와서 밖으로 던져줄 예정입니다. // 지금은 빨간 줄이 뜨겠지만, '구조'만 먼저 잡습니다. export { startGatewayServer } from "./server.impl.js"; export type { GatewayServer } from "./server.impl.js";
- 이해: "나는 안내원이야. 사장님(impl)이 오시면 바로 연결해줄게!"라고 선언하는 단계입니다.
- 역할: 서버의 모든 기능을 총괄하는 "뇌(Brain)" 입니다. 심장박동, 입, 귀, 팔다리를 여기서 다 연결합니다.
- VS Code 작업:
src/gateway/폴더에서server.impl.ts파일을 만듭니다. - 필수 코드 (Skeleton):
import { narrate } from "../narrator.js"; export interface GatewayServer { /* 서버의 명함 정보 */ } export async function startGatewayServer() { narrate({ who: "startGatewayServer", role: "총사령관", action: "시스템 기동 시작" }); // 여기에 앞으로 HTTP, WS, Channel 부품들을 조립할 겁니다. console.log("🚀 Gateway Server is ready!"); }
- 이해: 모든 부품이 모이는 **"중앙 제어반"**을 설치한 것입니다.
- 역할: 웹브라우저나 외부 요청을 받는 "입(Mouth)" 입니다. (
Hono라이브러리 사용) - VS Code 작업:
src/gateway/폴더에서server-http.ts파일을 만듭니다. - 필수 코드 (Skeleton):
import { Hono } from "hono"; export function createHttpServer() { const app = new Hono(); app.get("/", (c) => c.text("Hello! This is Gateway.")); return app; }
- 이해: 외부 사람들과 대화할 수 있는 **"창구"**를 개설한 것입니다.
- 역할: 실시간으로 데이터를 주고받는 "귀(Ears)" 입니다. (WebSocket 처리)
- VS Code 작업:
src/gateway/폴더에서server-ws-runtime.ts파일을 만듭니다. - 필수 코드 (Skeleton):
export function attachGatewayWsHandlers() { // WebSocket 연결이 들어오면 "누구세요?" 하고 듣는 로직이 들어갑니다. console.log("👂 Listening for real-time messages..."); }
- 이해: 전화선을 뽑지 않고 계속 들고 있는 **"실시간 통화 대기상태"**를 만든 것입니다.
- 역할: 왓츠앱, 슬랙 등으로 직접 메시지를 쏘는 "팔다리(Limbs)" 입니다.
- VS Code 작업:
src/gateway/폴더에서server-channels.ts파일을 만듭니다. - 필수 코드 (Skeleton):
export function createChannelManager() { return { sendMessage: (msg: string) => console.log(`[Limbs] Sending: ${msg}`), connect: () => console.log("[Limbs] Ready to move!") }; }
- 이해: 실제로 물건을 나르고(메시지 전송) 행동하는 **"실행 부서"**를 만든 것입니다.
"이 많은 걸 언제 다 치고 앉아있나요? 그러다가 지쳐서 포기하면 어떡하죠?"
절대 다 치지 마세요. 여러분의 목표는 '타자 연습'이 아니라 '구조 이해(Architectural Understanding)' 입니다.
-
코끼리를 먹는 법 (How to Eat an Elephant):
- 전략: 한 번에 한 입씩. 전체 코드를 다 이해하려 들지 마세요.
- "블랙박스 이론":
loadConfig()함수가 있다? "아, 설정 불러오는 놈이구나" 하고 넘어가세요. 그 안에서fs.readFile을 쓰는지Bun.file을 쓰는지 지금 알 필요 없습니다. 큰 흐름(Flow) 만 잡으세요.
-
복붙(Ctrl+C, V)은 죄가 아닙니다:
- 단, "읽고(Read) -> 이해하고(Understand) -> 붙여넣으세요(Paste)".
- 무지성 복붙은 독이지만, 구조를 파악하고 "이 덩어리는 여기 필요해"라고 알고 붙여넣는 건 '조립(Assembly)' 입니다. 이미 있는 레고 블록을 다시 깎을 필요는 없습니다.
-
AI를 '개인 과외 선생님'으로 쓰세요:
- "이 코드 500줄 다 설명해줘" (X) -> 너무 많아서 눈에 안 들어옵니다.
- "이 파일에서 가장 중요한 함수 3개만 꼽아서, 각각 무슨 일을 하는지 한 줄로 요약해줘" (O) -> 핵심 뼈대만 머리에 넣으세요.
"작동하는 코드가 아니라, 스스로 설명하는 코드를 만드세요."
여러분이 작성할 첫 번째 파일은 서버 코드가 아닙니다. 바로 로그 파일에 자기소개를 남기는 도구입니다. 이 도구를 쓰면, 프로그램이 실행될 때마다 마치 영화 해설처럼 로그가 남습니다.
💬 코치 코멘트: "맞습니다! 나중에 서버가 멈추거나 이상하게 작동할 때, 이 로그 기록만 추적하면 누가(어떤 함수가) 범인인지 즉시 색출해낼 수 있거든요. 에러를 잡는 가장 강력한 수사 기록이 되는 셈이죠."
파일 생성: src/narrator.ts
import fs from 'fs';
// "해설자" 함수: 누가, 어디서, 누구에게 연락하는지 기록합니다.
export function narrate(info: {
who: string; // 함수 이름 (예: startGatewayServer)
role: string; // 역할 (예: 총사령관)
action: string; // 하는 일 (예: 심장 박동기 켜기)
friend?: string; // 연결 대상 (예: startHeartbeatRunner)
}) {
const logMessage = `[${new Date().toISOString()}]
👤 WHO: ${info.who}
🛡️ ROLE: ${info.role}
🎬 ACTION: ${info.action}
${info.friend ? `🔗 CONTACT: ${info.friend}` : ""}
--------------------------------------------------\n`;
// 1. 화면에 보여주기
console.log(logMessage);
// 2. 파일에 기록하기 (learning.log)
fs.appendFileSync('learning.log', logMessage);
}이제부터 모든 함수를 만들 때, 로직 대신 이 narrate() 함수를 맨 앞에 박아둘 겁니다.
가장 먼저 이 파일을 만듭니다. 이 파일은 서버의 "안내 데스크(Front Desk)" 입니다.
Note
🛠️ VS Code 실전 조립 절차
- 파일 생성: 탐색기(Explorer)에서
src/gateway폴더를 우클릭하고New File을 선택합니다. - 이름 입력:
server.ts를 입력하고 엔터를 칩니다. - 코드 작성: 아래 '#### 2-1-2. 코드 씹어먹기' 섹션에 있는
export문장들을 입력합니다. 이 문장들은 다른 파일에 있는 기능을 가져와서 곧바로 밖으로 연결해주는 '전달 통로' 역할을 합니다.
오시다시피 이 파일은 아무런 로직이 없습니다. 오직 다른 파일로 연결만 해줍니다. 이것을 디자인 패턴에서는 퍼사드 패턴(Facade Pattern) 이라고 합니다.
[외부 세상: CLI, Docker]
⬇️ (접속: "서버 켜줘!")
⬇️
src/gateway/server.ts (안내 데스크)
│
├── 1. "사장실 연결해 드립니다." (startGatewayServer)
│ ⤷ 연결 ➡ src/gateway/server.impl.ts (진짜 CEO: 뇌)
│
├── 2. "보안 규정집 여기 있습니다." (truncateCloseReason)
│ ⤷ 연결 ➡ src/gateway/server/close-reason.ts (규정집)
│
└── 3. "명함 양식입니다." (GatewayServer Type)
⤷ 연결 ➡ src/gateway/server.impl.ts (타입 정의)
src/gateway/server.ts
// [1] 보안 요원 연결 (Security)
// 역할: 웹소켓 연결 끊을 때 이유가 너무 길면(해킹 시도 등) 적당히 자르는 함수를 내보냅니다.
export { truncateCloseReason } from "./server/close-reason.js";
// [2] 명함(Type) 공유 (Public Card)
// 역할: "우리 사장님(GatewayServer)은 이렇게 생겼습니다"라고 외부에 알려줍니다.
export type { GatewayServer, GatewayServerOptions } from "./server.impl.js";
// [3] 사장실 연결 (The Real Boss)
// 핵심: 실제로 서버를 켜는 함수(startGatewayServer)를 밖으로 내보냅니다.
// 외부(CLI)에서는 이 파일의 startGatewayServer만 호츨하면, 내부 사정을 몰라도 서버가 켜집니다.
export { __resetModelCatalogCacheForTest, startGatewayServer } from "./server.impl.js";초보자가 가장 헷갈려 하는 3가지 문법을 완벽하게 분해해 드립니다.
-
1.
export { ... } from "...": "국제 배송 대행 서비스"- 의미: "저쪽 파일(
from)에 있는 물건을 가져와서, 내 이름으로 다시 해외 수출(export)하겠다." - 은유(Metaphor): 쿠팡(server.ts)이 삼성전자(server.impl.ts)의 냉장고를 떼와서 쿠팡 이름으로 파는 것과 같습니다. 소비자는 삼성 공장에 갈 필요 없이 쿠팡에서 사면 됩니다.
- 의미: "저쪽 파일(
-
2.
export type: "설계도면 vs 실제 건물"- 의미: "Java/C#의 Class나 Interface 같은 껍데기(모양) 만 내보냅니다."
- 은유(Metaphor):
export class(GatewayServer): 실제 살 수 있는 아파트입니다. (메모리 차지함)export type(GatewayServer): 아파트 설계도(청사진) 종이입니다. (메모리 차지 안 함)
- Why?: TypeScript는 컴파일되면 JavaScript로 변합니다. 이때
type으로 정의된 건 흔적도 없이 사라집니다(Ghost). 개발할 때 자동완성 힌트만 주고, 런타임에는 가볍게 사라지기 위해 씁니다.
-
3.
{ a, b }(중괄호): "종합 선물 세트 뜯기"- 문법 이름: 구조 분해 할당 (Destructuring Assignment)
- 의미: "
server.impl.js라는 큰 선물 상자 안에 여러 가지가 들어있는데, 그중에서__reset...이랑startGatewayServer라는 딱 2개만 콕 집어서 꺼내겠다." - 예시:
const Box = { candy: 10, chocolate: 5, gum: 3 }; const { candy, gum } = Box; // 초콜릿은 안 꺼냄!
이것은 소프트웨어 공학에서 "변경에 대한 방어막(Decoupling)" 을 치는 기술입니다. 쉬운 예시를 들어보겠습니다.
🏠 실생활 예시: "고객센터 대표번호 (1588-0000)"
- 상황: 여러분이 쇼핑몰 CEO입니다. 전국에 흩어진 상담원들이 전화를 받습니다.
- 껍데기(
server.ts): 대표번호 1588-0000 - 실체(
server.impl.ts): 실제 상담 센터 (처음엔 서울 사무실)
🌪 사건 발생: 서울 사무실 임대료가 비싸서 부산으로 이사 가기로 했습니다. (server.impl.ts -> server.busan.ts 로 파일명 변경)
-
대표번호가 없을 때 (No Facade):
- 고객 100만 명에게 전부 문자를 보내야 합니다.
- "죄송합니다. 번호가 02-123-4567에서 051-987-6543으로 바뀌었습니다. 다시 저장해주세요."
- 코드로 치면, 이 파일을
import해서 쓰는 모든 파일 100개를 찾아서 수정해야 합니다. (대재앙)
-
대표번호가 있을 때 (Facade Pattern):
- 고객은 아무것도 몰라도 됩니다. 여전히 1588-0000으로 걸면 됩니다.
- CEO인 여러분은 대표번호 연결 설정(Routing) 만 서울에서 부산으로 1줄 바꾸면 끝입니다.
- 코드로 치면,
server.ts의import경로 1줄만 수정하면 프로젝트 전체가 멀쩡하게 돌아갑니다.
결론: server.ts는 귀찮은 껍데기가 아니라, 미래의 변화로부터 우리를 지켜주는 "안전 장치" 입니다.
"말로만 듣지 말고, 진짜 안전한지 직접 터트려봅시다."
이 개념을 확실히 잡고 가기 위해, 아주 간단한 실험실(Lab) 을 만들어보겠습니다. 폴더 아무데나 test-facade 폴더를 만들고 3개의 파일을 만드세요.
1. core-v1.js (실체: 구형 엔진)
// 실제 로직이 들어있는 파일
export function startEngine() {
console.log("🚗 V1 엔진: 부릉부릉 (시끄러움)");
}2. gateway.js (껍데기: 안전장치)
// 여기가 핵심입니다! 실체를 import해서 그대로 내보냅니다.
export * from "./core-v1.js"; 3. client.js (사용자: 운전자)
// 운전자는 'core-v1.js'를 모릅니다. 오직 'gateway.js'만 압니다.
import { startEngine } from "./gateway.js";
console.log("👤 운전자: 시동을 겁니다.");
startEngine();🧪 실험 시작: 미래를 바꾸는 시나리오 4단계
1단계: 평화로운 일상 (Initial Run)
먼저 터미널에서 node client.js를 입력해서 실행해 보세요.
화면에 "🚗 V1 엔진: 부릉부릉" 하고 시끄러운 소리가 날 겁니다.
지금 운전자(client.js)는 gateway.js를 통해 구형 엔진을 쓰고 있습니다. 평화롭죠?
2단계: 위기 발생 (Refactoring Needed)
어느 날, 정부에서 "소음 규제"를 발표합니다. 더 이상 시끄러운 V1 엔진을 쓸 수 없게 되었습니다.
자, 이제 core-v1.js 파일을 과감하게 삭제해 버리세요. 그리고 core-v2.js 라는 새 파일을 만들고 아래 코드를 넣습니다.
export function startEngine() {
console.log("⚡️ V2 모터: 위이잉 (조용함)");
}이제 실체(엔진)가 완전히 바뀌어 버렸습니다.
3단계: 마법의 방어 (The Fix)
보통 같으면 운전자(client.js) 코드를 뜯어고쳐야겠지만, 우리는 그럴 필요가 없습니다.
중간에 있는 gateway.js 파일만 살짝 여세요. 그리고 from 뒤에 있는 경로만 바꿔주면 됩니다.
export * from "./core-v2.js"; // v1 -> v2 로 숫자 하나만 바꿨습니다.운전자 몰래, 카센터 사장님(Gateway)이 엔진을 최신형으로 바꿔치기한 겁니다.
4단계: 결과 확인 (Verification)
다시 node client.js를 실행해 보세요.
놀랍게도 운전자는 아무런 에러 없이 "⚡️ V2 모터: 위이잉" 하고 전기차를 몰게 됩니다.
운전자는 엔진이 바뀐 줄도 모릅니다. 단지 차가 조용해졌다고 좋아할 뿐이죠.
Important
🎓 결론: 이것이 바로 '유지보수'입니다
여러분이 방금 하신 일이 바로 "의존성 분리(Decoupling)" 입니다.
내무 부품이 싹 다 바뀌어도, 바깥세상(Client)에는 충격을 주지 않는 것. 이것이 시니어 개발자들이 server.ts 같은 껍데기 파일을 굳이 만들어 두는 이유입니다.
Important
🚀 이 책(매뉴얼)의 최종 목표 비록 지금은 초보자일지라도, 이런 고급 아키텍처 경험을 하나하나 빠르게 쌓아서 "시니어 개발자의 통찰력" 을 갖추는 것이 목표입니다. 그래야만 단순히 AI가 짜주는 코드를 받아 적는 것을 넘어, AI 코딩 에이전트들을 내 손바닥 위에서 자유자재로 지휘하고 주무르는 진짜 사령관이 될 수 있습니다.
안내 데스크(server.ts)의 문을 열고 들어오셨군요. 환영합니다.
이곳은 OpenClaw(Moltbot) 시스템의 가장 깊숙한 곳, "종합 상황실(NASA Control Room)" 입니다.
Note
🛠️ VS Code 실전 조립 절차
- 파일 생성:
src/gateway/server.impl.ts파일을 생성합니다. - 자동 완성 활용:
interface나function을 칠 때 VS Code가 제안하는 코드 힌트를 적극적으로 활용하세요. - 임포트 체크: 상단에 있는
narrate함수가 정확한 경로(../narrator.js)에서 오고 있는지 확인하세요. (ESM 방식이므로.js확장자를 붙이는 것이 포인트입니다!)
이 코드는 OpenClaw 시스템의 모든 부품을 조립하는 최종 설계도입니다. 500줄이 넘는 방대한 양이지만, 핵심이 되는 startGatewayServer 함수의 내부 로직을 상세히 파헤쳐 보겠습니다.
// [1] 임포트 섹션 (생략)
import { ... } from "..."
import { narrate } from "../narrator.js";
// [2] 타입 정의
export type GatewayServer = {
close: (opts?: { reason?: string; restartExpectedMs?: number | null }) => Promise<void>;
};
// [3] 사령관 함수: 서버 기동의 모든 단계를 지휘합니다.
export async function startGatewayServer(
port = 18789,
opts: GatewayServerOptions = {},
): Promise<GatewayServer> {
// 1단계: 환경 변수 및 설정 준비
process.env.CLAWDBOT_GATEWAY_PORT = String(port);
let configSnapshot = await readConfigFileSnapshot();
// 2단계: 레거시 설정 마이그레이션 (과거의 유산 정리)
if (configSnapshot.legacyIssues.length > 0) {
const { config: migrated, changes } = migrateLegacyConfig(configSnapshot.parsed);
await writeConfigFile(migrated);
log.info(`gateway: migrated legacy config entries`);
}
// 3단계: 장기(Subsystems) 초기화
narrate({ who: "startGatewayServer", role: "총사령관", action: "장기 부착 시작" });
const nodeRegistry = new NodeRegistry();
const channelManager = createChannelManager({
loadConfig,
channelLogs,
channelRuntimeEnvs,
});
// 4단계: 실시간 통신망(WebSocket) 및 HTTP 서버 기동
const { httpServer, wss, clients, broadcast } = await createGatewayRuntimeState({
cfg: loadConfig(),
bindHost: "0.0.0.0",
port,
// ... 기타 설정들
});
// 5단계: 신경망 핸들러 연결 (데이터가 흐르기 시작하는 지점)
attachGatewayWsHandlers({
wss,
clients,
port,
gatewayMethods,
logGateway: log,
broadcast,
context: {
// ... 서버의 모든 기능을 핸들러에 주입 (Dependency Injection)
nodeRegistry,
channelManager,
cron,
loadGatewayModelCatalog,
}
});
// 6단계: 외부 노출 및 보안 설정 (Tailscale 등)
const tailscaleCleanup = await startGatewayTailscaleExposure({ ... });
// 7단계: 기동 완료 보고
log.info("🚀 Gateway Server is ready for action!");
narrate({ who: "startGatewayServer", role: "총사령관", action: "기동 완료 보고" });
// 8단계: 통제권 반환 (종료 버튼 제공)
return {
close: async (opts) => {
await close(opts); // 모든 장기 정지 및 메모리 해제
log.info("🛑 Gateway Server shut down safely.");
},
};
}Tip
500줄의 전체 미로를 탐험하고 싶다면? 실시간 업데이트되는 전체 원본 소스 코드는 프로젝트 내 server.impl.ts 파일에 보존되어 있습니다. 이 코드는 **"어떻게 70개의 파일이 하나로 꿰어지는가"**에 대한 정답지입니다.
눈앞에 펼쳐진 약 600줄의 코드가 위압적으로 느껴지십니까? 당연합니다. 이곳은 단순한 코드가 아닙니다. 서버의 오감을 깨우고, 인공지능 두뇌를 연결하고, 전 세계와 통신하는 신경망을 지휘하는 "사령부" 이기 때문입니다.
- 좌측 스크린:
HTTP Server가 웹에서 들어오는 신호를 감시합니다. - 우측 스크린:
WebSocket이 수천 개의 실시간 연결을 유지합니다. - 중앙 콘솔:
Agent Runner가 AI(GPT-4)에게 끊임없이 명령을 내립니다. - 백그라운드:
Heartbeat가 시스템의 생명 반응을 주기적으로 체크합니다.
이 모든 것이 단 하나의 파일, server.impl.ts 안에서 조율됩니다.
복잡해 보이지만 걱정하지 마십시오. 우리는 이 거대한 우주선을 "단 5개의 버튼" 으로 분해해서 하나씩 조립할 것입니다. (이것이 앞에서 설명드린 추상화를 의미하는 것 기억 나시죠?)
준비되셨나요? 이제 사령관(startGatewayServer)의 의자에 앉아 볼 시간입니다.
"import가 70줄이나 되는데, 이걸 다 만들어야 하나요?" 네, 하지만 "내용"을 다 만들 필요는 없습니다. 껍데기(Stub)만 있으면 됩니다. 이 70줄은 크게 5가지 그룹으로 나뉩니다.
- 1. 외부 기관 (External Agencies):
../config/*: 설정 파일 등본을 떼오는 곳.../infra/*: 진단(Diagnostic), 심장 박동(Heartbeat), 재시작(Restart) 등 생명 유지 장치.../logging/*: 블랙박스 기록 장치.
- 2. 에이전트 본부 (Agent HQ):
../agents/*: AI 두뇌 관리소.
- 3. 내부 장기 (Internal Organs):
./server-*: HTTP, WS, Channel 등 서버의 실제 부품들.
Important
💡 [핵심 전략] "가짜로 채워라 (Fake it till you make it)"
70개 파일을 다 구현하다간 지쳐 떨어집니다. "빈 파일 전략(Stubbing)" 을 쓰세요.
- 빨간 줄 없애기 게임:
server.impl.ts를 복사해서 붙여넣으면 온통 빨간 줄(에러)입니다. - 빈 파일 생성: 에러가 나는 경로(예:
../infra/heartbeat.ts)에 파일을 만듭니다. - 가짜 수출(Export): 함수 내용을 비워두고
export만 하세요.// ../infra/heartbeat.ts // 껍데기만 만듭니다. 내용은 나중에 채웁니다. export function startHeartbeatRunner() { console.log("Heartbeat started (fake)"); }
- 컴파일 성공: 빨간 줄이 다 사라지면, 서버는 켜집니다. (물론 아무 기능도 안 하겠지만, 골격은 완성된 겁니다.)
[!QUESTION] Q. 사령관님, 질문 있습니다! "
server.impl.ts의 90~92번째 줄을 봤는데,GatewayServer타입이 너무 심플합니다. 고작close함수 하나밖에 없는데요?"export type GatewayServer = { close: (opts?: ...) => Promise<void>; };A. 전문가의 통찰력 (Expert's Insight) 아주 날카로운 질문입니다! 여기서 우리는 "추상화의 미학(The Art of Abstraction)" 을 배워야 합니다.
빙산의 일각 (Tip of the Iceberg):
- 빙산은 물 위에 10%만 보이고, 물 밑에 90%가 숨어 있죠.
GatewayServer는 물 위에 나온 10%입니다. 외부(CLI)에서는 서버를 "켜고(start)", "끄는(close)" 것 외에는 알 필요가 없게 만든 것입니다. 내부에서 100만 줄의 코드가 돌아가든 말든, 사용자는 "전원 버튼" 하나면 충분합니다.핵가방 (Nuclear Football):
- 대통령의 핵가방에는 복잡한 미사일 회로가 들어있지 않습니다. 오직 "발사 버튼" 과 "취소 버튼" 만 있습니다.
- 만약 리모컨에 버튼이 100개라면(예:
server.http,server.ws다 노출), 사용자는 헷갈려서 사고를 칠 겁니다.- "가장 훌륭한 인터페이스는 더 이상 뺄 것이 없는 상태다." 이 단순함이 바로 고수가 설계했다는 증거입니다.
각 함수의 역할과, 초보자가 이를 어떻게 복제(Cloning)해야 하는지 하나씩 뜯어보겠습니다.
- 역할:
config.yaml파일을 읽어서 자바스크립트 객체로 변환합니다. 그냥 읽는 게 아니라, "빠진 항목은 없는지, 타입은 맞는지" 검사(Validation)합니다. - 입력(Input):
CONFIG_PATH(파일 경로 문자열) - 출력(Output):
ClawdbotConfig(검증된 설정 객체) 또는 에러 폭발(Throw Error) - 코드 분석:
// 실제 로직 (Zod 라이브러리 사용) const raw = fs.readFileSync("config.yaml"); // 1. 파일 읽기 const parsed = yaml.parse(raw); // 2. 파싱 const config = ClawdbotSchema.parse(parsed); // 3. 검증 (여기서 틀리면 에러!) return config;
- 초보자 클론 전략 (Stub First):
- Stub Code:
import { narrate } from "../narrator"; export function loadConfig() { narrate({ who: "loadConfig", role: "보급관", action: "설정 파일 로딩 시늉" }); return { agents: { defaults: { model: "gpt-4" } } }; }
- Stub Code:
- 역할: 주기적으로(예: 1시간마다) 봇이 살아있는지 확인하고, 필요하면 자가 진단 메시지를 보냅니다. "활동 시간(Active Hours)"인지 체크해서 밤에는 조용히 시킵니다.
- 입력(Input):
Config(설정 객체) - 출력(Output):
HeartbeatRunner(멈춤 버튼stop()이 달린 handle) - 초보자 클론 전략 (Stub First):
- Stub Code:
import { narrate } from "../narrator"; export function startHeartbeatRunner() { narrate({ who: "startHeartbeatRunner", role: "의무병", action: "심장 박동기 가동" }); return { stop: () => { console.log("MEDIC: Stopped"); } }; }
- Stub Code:
- 역할: WhatsApp, Slack, Discord 등 서로 다른 언어를 쓰는 외부 채널들을 관리합니다. 각 채널을 켜고(
start), 끄고(stop), 상태를 확인하는 제어판입니다. - 입력(Input):
ChannelManagerOptions(로그, 설정 등) - 출력(Output):
ChannelManager(채널 제어 메서드 모음) - 초보자 클론 전략 (Stub First):
- Stub Code:
import { narrate } from "../narrator"; export function createChannelManager() { return { startChannels: async () => { narrate({ who: "startChannels", role: "외교관", action: "모든 대사관(WhatsApp, Slack) 연결" }); } }; }
- Stub Code:
- 역할: 외부에서 들어오는 요청(HTTP Request)을 받아서 적절한 담당자에게 토스합니다.
/v1/hooks->hooks.ts(웹훅 담당)/slack/events->slack.ts(슬랙 담당)/ui->control-ui.ts(관리자 화면 담당)
- 입력(Input):
handleHooksRequest,controlUiEnabled등 (각 부서의 연락처) - 출력(Output):
HttpServer(Node.js 서버 객체) - 초보자 클론 전략 (Stub First):
- Stub Code:
import { Hono } from "hono"; import { narrate } from "../narrator"; export function createHttpServer() { const app = new Hono(); app.get("/", (c) => { narrate({ who: "HttpHandler", role: "대변인", action: "기자(Client) 질문 응대" }); return c.text("SPOKESPERSON: I am ready."); }); return app; }
- Stub Code:
- 역할: 들어오는 소켓 연결(
connection)을 감지하고, "누구십니까(Auth)"를 물어보고, "연결되었습니다" 도장을 찍어주는 수문장입니다. - 입력(Input):
wss(웹소켓 서버),clients(접속자 명단) - 출력(Output):
void(돌려주는 것 없음. 그냥 전화선만 꽂아둠) - 초보자 클론 전략 (Stub First):
- Stub Code:
import { narrate } from "../narrator"; export function attachGatewayWsHandlers({ wss }) { wss.on("connection", (ws) => { narrate({ who: "WsHandler", role: "통신병", action: "새로운 전화 연결 수락" }); }); }
- Stub Code:
Tip
"Stubbing(껍데기 만들기)" 은 초보자의 꼼수가 아니라, 시니어 개발자가 복잡도를 관리하는 가장 프로페셔널한 기술입니다.
700줄을 한 번에 짜는 사람은 세상에 없습니다. 위에서 알려드린 5개의 껍데기 함수만 먼저 파일에 복붙하고, startGatewayServer 안에서 호출만 해보세요. 에러 없이 실행되는 기쁨을 먼저 맛보셔야 합니다.
"AI가 코드 다 짜주는데, 문법 공부를 해야 하나요?"
Important
네, 하지만 관점이 달라야 합니다. 직접 코딩하기 위해서가 아니라, AI에게 정확히 지시하기 위해서 배워야 합니다.
-
1. Interface/Type = "AI에게 내리는 스펙(Spec)"
- 과거: 내가 코딩하다 틀리면 컴파일러한테 혼나려고 씀.
- AI 시대: "야 AI야, 네가 짤 함수는 반드시 이 모양이어야 해!" 라고 규격(Spec)을 던져주는 용도.
- 전략: 구현(
function)보다 타입(interface)을 먼저 정의하고 AI에게 던지세요. 그러면 AI가 딴소리를 못 합니다.
-
2. Promise/Async = "AI야, 잠깐만 기다려"
- 과거: 비동기 처리가 어려워서 콜백 지옥에 빠짐.
- AI 시대: "이 작업은 오래 걸리니까(
async), 다 될 때까지 기다렸다가(await) 다음 줄로 넘어가" 라는 업무 순서 지시서입니다.
이 복잡한 파일을 혼자 짜지 마세요. 이렇게 지시하십시오.
Important
🚀 [미래 예보] 여러분의 역할은 '코더'가 아니라 '기획 개발자'입니다.
앞으로 여러분의 역할은 단순히 코드를 치는 코더가 아닙니다. AI 코딩 에이전트 (예: Google Antigravity, VS Code, KIMI, OpenAI Codex 등) 수많은 자동 개발 도구들을 능숙하게 다루기 위해서는, 개발을 아주 작은 단위로 쪼개고 그 단위가 '왜 여기에 놓여야 하는지' 고민하고 다시 배치하는 능력이 필수입니다. 이러한 판단과 기획, 조정을 직접 수행해야만 살아남을 수 있는 세상이 되었습니다.
방금 말씀드린 "코더의 종말" 이야기가 피부에 와닿지 않으신다고요? 역사를 보면 지금의 상황이 데자뷔처럼 느껴지실 겁니다.
- 1970-80년대의 풍경: 그 시절엔 모니터도, 키보드도 없었습니다. 개발자가 종이(코딩 시트)에 연필로 코드를 적어주면, **'펀치라이터(Punch Writer)'**라 불리는 전문직 분들이 **천공기(Keypunch Machine)**라는 기계로 종이 카드에 구멍을 뻥뻥 뚫었습니다.
- 카드 한 장 = 코드 한 줄: 수천 줄짜리 프로그램은 수천 장의 카드 덱이 되었고, 이 카드를 컴퓨터에 집어넣어야만 계산기가 돌아갔습니다.
- 그땐 그랬지: 당시 펀치라이터는 정확도와 속도를 갖춘, 당대 최고의 대우를 받던 **'최첨단 IT 전문직'**이었습니다. 프로그래밍의 결과물을 실제로 '만들어내는' 유일한 사람들이었으니까요.
- 지금은?: 키보드와 마우스, 그리고 저장 장치가 발달하며 '구멍을 뚫는 행위(Punching)' 자체는 완전히 사라졌습니다. 이제는 아무도 구멍을 뚫어 코드를 입력하지 않습니다.
Tip
시사하는 점: "구멍을 뚫는 사람"은 사라졌지만, "어디에 구멍을 뚫을지 기획한 사람"은 지금의 소프트웨어 엔지니어가 되었습니다. 이제 **"코드를 한 줄씩 치는 행위"**도 AI가 대신하는 천공기의 길을 걷고 있습니다. 여러분은 단순히 코드를 치는 사람이 아니라, **그 코드가 왜 그 자리에 있어야 하는지 결정하는 '기획 개발자'**가 되어야만 이 거대한 변화의 파도에서 살아남을 수 있습니다.
- 스텝 1: 빈 껍데기 요청 (Stubbing)
"server.impl.ts의
startGatewayServer함수 뼈대만 만들어줘. 내부는 비워두고, 리턴 타입인GatewayServer만 맞춰줘." - 스텝 2: 설정 로직 주입 (Dependency Injection)
"방금 만든 함수 맨 윗줄에
loadConfig를 호출해서 설정을 불러오는 로직만 추가해줘.loadConfig는 없는 함수니까 import 문만 적어놔." - 스텝 3: 서브시스템 연결 (Connectivity)
"이제
createHttpServer와createWebSocketServer를 호출하는 코드를 중간에 넣어줘."
Important
핵심: AI에게 한 번에 "이거 다 만들어"라고 하지 말고, "조립 설명서의 한 문장" 씩 던지세요. 그것이 진짜 엔지니어의 실력입니다.
사용자님이 질문하신 GatewayServer 타입 정의를 한 줄 한 줄 뜯어보겠습니다. 이것은 "서버 리모컨의 사용 설명서" 입니다.
export type GatewayServer = {
// [1] 기능 정의 (Property: Function)
// "close 버튼을 누르면 서버가 꺼집니다."
close: (
// [2] 선택적 옵션 (Optional Parameter)
// "옵션은 안 넣어도 됩니다(?). 넣는다면 '이유'와 '재시작 시간'을 적으세요."
opts?: { reason?: string; restartExpectedMs?: number | null }
)
// [3] 비동기 약속 (Async Return)
// "버튼을 누르면 즉시 꺼지지 않고, 완전히 꺼질 때까지 기다려야(Promise) 합니다."
=> Promise<void>;
};-
export type GatewayServer = { ... }:- 의미: "앞으로
GatewayServer라고 하면, 무조건 중괄호{}안에 있는 기능들을 가지고 있어야 한다"는 계약서(Contract) 입니다. - AI 활용법: AI에게 "서버 만들어줘"라고 하기 전에, "서버의 리모컨은 무조건 이렇게 생겨야 해" 라고 이 타입을 먼저 던져주세요. 그러면 AI가 엉뚱한 리모컨을 만들어오지 않습니다.
- 의미: "앞으로
-
opts?:(물음표의 마법):- 문법: Optional Parameter. 있어도 되고 없어도 되는 값.
- 은유: "선풍기 타이머". 타이머를 안 맞추고(
undefined) 그냥 꺼도 되고, 1시간 뒤에 꺼지라고 맞춰도 됩니다. - AI 활용법: "옵션이 너무 복잡해"라고 느끼면 AI에게 "모든 필드에
?를 붙여서 선택 사항으로 바꿔줘(Make everything optional)" 라고 지시하세요. 코딩이 훨씬 편해집니다.
-
=> Promise<void>:- 문법: 비동기 반환.
- 의미: "이 함수는
return값이 없습니다(void). 하지만 작업이 끝났다는 신호(Promise)는 나중에 보내줍니다." - 현실 비유: "배달 주문". 짜장면을 시키면(함수 호출), 짜장면이 바로 뽕 하고 나타나지 않습니다. "배달 중(Pending)" 상태였다가 나중에 "도착(Resolved)"합니다.
- AI 활용법: 요즘 서버 코드는 99%가 비동기입니다. AI에게 "모든 IO 작업은 무조건
async/await로 짜줘" 라고 미리 못 박으세요. 안 그러면 AI가 옛날 방식(동기식)으로 짜서 서버 전체를 멈추게(Blocking) 할 수도 있습니다.
// [핵심 함수] 이 함수가 실행되면 서버가 태어납니다.
export async function startGatewayServer(port = 18789, opts: GatewayServerOptions = {}) {
// 1. [준비 단계] 비행 전 점검 (Pre-flight Check)
// 환경 설정(Config)을 읽어오고, 로그 시스템을 켭니다.
const config = await loadConfig(CONFIG_PATH_CLAWDBOT);
const log = createSubsystemLogger("gateway");
// 2. [실행 단계] 심장 박동 시작 (Heartbeat)
// 서버가 살아있음을 알리는 심장 박동(Heartbeat) 시스템을 가동합니다.
const heartbeatHandle = startHeartbeatRunner({ /* ... */ });
// 3. [연결 단계] 각 부서(장기) 소집
// 입(HTTP), 귀(WebSocket), 팔다리(Channel) 담당자들을 불러옵니다.
// 3-1. 채널 매니저 (팔다리)
const channelManager = createChannelManager({ /* ... */ });
// 3-2. HTTP 서버 (입)
// Hono 앱을 만들고 라우팅을 설정합니다.
const { app, server } = await createHttpServer({ /* ... */ });
// 3-3. WebSocket 서버 (귀)
// 실시간 통신을 위한 소켓 서버를 엽니다.
const wss = await createWebSocketServer({ /* ... */ });
// 4. [최종 승인] 준비 완료
// 모든 시스템이 정상적으로 떴다는 로그를 남깁니다.
log.info(`Gateway running on port ${port}`);
return {
// 나중에 서버를 끄거나 조종할 수 있도록 리모컨(객체)을 반환합니다.
close: async () => { /* 종료 로직 */ }
};
}- 의존성 주입(Dependency Injection): 각 장기들은 서로가 필요합니다.
HTTP 서버는채널 매니저가 필요합니다. (사용자가 웹에서 "카톡 보내줘"라고 하면 채널 매니저에게 시켜야 하니까요)WebSocket 서버는에이전트가 필요합니다.
- 중앙 통제: 만약 각자 따로 켜지면, "HTTP는 켜졌는데 DB가 안 켜지는" 끔찍한 상황이 발생합니다.
server.impl.ts는 모든 장기가 완벽하게 준비될 때까지 기다렸다가(await), 동시에 출격 명령을 내립니다.
안내 데스크(server.ts)와 사령부(server.impl.ts)가 준비되었습니다. 하지만 아직 이 서버는 벙어리입니다. 외부에서 말을 걸 수도, 대답할 수도 없습니다. 이제 "입(Mouth)" 을 달아줄 차례입니다.
Note
🤔 "입(Mouth)"의 진짜 역할은?
단순한 출력기가 아닙니다! HTTP 서버는 양방향 통신 장치입니다:
-
입력 기능 (Request Listener):
- 외부에서 들어오는 모든 HTTP 요청(
GET,POST,PUT,DELETE)을 듣고(Listen) 받아들입니다. - 예: 웹훅(Slack, GitHub), API 호출(
/v1/chat/completions), 브라우저 접속(/ui)
- 외부에서 들어오는 모든 HTTP 요청(
-
출력 기능 (Response Sender):
- 요청을 처리한 후 **응답(Response)**을 돌려보냅니다.
- 예: JSON 데이터, HTML 페이지, 상태 코드(200 OK, 404 Not Found)
-
숨겨진 핵심 기능들:
- 라우팅(Routing): 요청 경로(
/health,/v1/hooks/agent)에 따라 적절한 핸들러로 분배합니다. - 인증(Authentication): 토큰 검증으로 허가된 요청만 처리합니다.
- 정적 파일 서빙: 관리자 UI의 HTML/CSS/JS 파일을 제공합니다.
- WebSocket 업그레이드: HTTP 연결을 실시간 WebSocket으로 전환시킵니다.
- 에러 처리: 잘못된 요청에 대해 적절한 에러 메시지를 반환합니다.
- 라우팅(Routing): 요청 경로(
비유: 입은 단순히 "말하는" 기관이 아니라, 듣고(귀), 맛보고(센서), 씹고(처리), 말하는(출력) 복합 기관입니다!
Note
🛠️ VS Code 실전 조립 절차
- 파일 생성:
src/gateway/server-http.ts파일을 생성합니다. - 라이브러리 불러오기: 상단에
import { Hono } from "hono";를 입력합니다. 만약 밑줄(Error)이 뜬다면 터미널에서pnpm add hono가 되어있는지 확인하세요. - 테스트: 서버를 켠 후 브라우저 주소창에
http://localhost:3000을 입력해 "Hello!"가 나오는지 확인하는 것이 최종 목표입니다.
이 파일은 Hono라는 초고속 혀(Language)를 사용합니다.
- 역할: 들어오는 모든 HTTP 요청(
GET,POST)을 듣고, 적절한 부서로 토스합니다. - 핵심 부품:
- Health Check: "살아있니?" (
/health) -> "응" (200 OK) - Webhooks: "슬랙에서 메시지 왔어!" (
/v1/hooks/...) -> 채널 매니저에게 전달 - UI Assets: "관리자 화면 보여줘" (
/ui) -> HTML 파일 서빙
- Health Check: "살아있니?" (
import { Hono } from "hono";
import { serveStatic } from "@hono/node-server/serve-static";
export async function createHttpServer(params: {
// 의존성 주입: 이 함수가 작동하기 위해 필요한 다른 부서 연락처들
handleHooksRequest: any; // 외부 훅 처리 담당자
controlUiEnabled: boolean; // UI 켤지 말지 여부
}) {
const app = new Hono();
// 1. [기초 대사] 생존 신고 (Ping/Pong)
// 가장 중요한 기능입니다. "나 살아있어요?"라고 물으면 "Pong" 해줍니다.
// 클라우드(AWS, Fly.io)가 서버가 죽었는지 살았는지 찌러볼 때 씁니다.
app.get("/health", (c) => c.text("ok"));
// 2. [목소리] 관리자 UI 서빙
// 사용자가 브라우저로 접속하면 이쁜 화면(HTML)을 보여줍니다.
if (params.controlUiEnabled) {
// "assets 폴더에 있는 파일 좀 보여줘" (정적 파일 서빙)
app.use("/ui/*", serveStatic({ root: "./assets" }));
}
// 3. [귀] 외부 훅 듣기 (Listening Hooks)
// Slack이나 다른 프로그램이 보내는 데이터를 받습니다.
app.all("/v1/hooks/*", async (c) => {
return params.handleHooksRequest(c); // 담당자에게 토스!
});
return {
app, // 완성된 입(Hono App)을 반환
};
}처음부터 완벽한 라우팅을 짤 필요는 없습니다. 일단 "숨을 쉬는지" 만 확인합시다.
src/gateway/server-http.ts파일을 만드세요.- 아래의 최소 생존 훈련 코드를 붙여넣으세요.
import { Hono } from "hono";
import { narrate } from "../narrator";
export async function createHttpServer(params: any) {
const app = new Hono();
// "심장이 뜁니다."
app.get("/", (c) => {
narrate({ who: "HTTP", role: "입", action: "숨쉬기(Health Check)" });
return c.text("OpenClaw Gateway: Breathing...");
});
return { app };
}- 이제 브라우저를 켜고
http://localhost:18789(포트는 설정에 따라 다름)에 접속했을 때, "OpenClaw Gateway: Breathing..." 이라는 글자가 보이면 성공입니다!
Tip
🎉 축하합니다!
메시지가 나온다면 성공입니다!! 축하드립니다. 여러분은 지금 핵심 게이트웨이 알고리즘을 완성하신 겁니다. 서버에 생명을 불어넣는 첫 번째 숨결을 성공적으로 만들어냈습니다!
다음 단계: 이제 핵심 신경망 조직인 귀 역할을 하는 WebSocket을 만들어보도록 하겠습니다.
HTTP가 "똑똑, 거기 누구 있나요?" 하고 묻는 단발성 대화라면, WebSocket은 전화선을 뽑지 않고 계속 들고 있는 실시간 통화입니다. 이것은 봇의 "신경망(Nervous System)" 이자 "귀(Ears)" 입니다.
Note
🔌 WebSocket이란?
WebSocket은 클라이언트(브라우저)와 서버 간의 양방향 실시간 통신 채널을 제공하는 프로토콜입니다.
| 특징 | HTTP | WebSocket |
|---|---|---|
| 연결 방식 | 요청-응답 후 연결 종료 | 한 번 연결하면 계속 유지 |
| 통신 방향 | 단방향 (클라이언트 → 서버) | 양방향 (서버 ↔ 클라이언트) |
| 실시간성 | 없음 (폴링 필요) | 즉각적 (푸시 가능) |
| 오버헤드 | 매 요청마다 헤더 전송 | 최초 1회만 핸드셰이크 |
| 비유 | 우편 배달 (편지 보내고 답장 기다림) | 전화 통화 (계속 연결 상태) |
- 채팅 앱: 메시지가 오면 즉시 화면에 표시
- 실시간 대시보드: 서버 상태, 로그가 실시간으로 업데이트
- 게임: 플레이어 위치, 점수 등을 지연 없이 동기화
- 주식 거래: 실시간 시세 변동 알림
관리자가 브라우저 대시보드에서 봇의 상태를 실시간으로 모니터링하고, 명령을 즉시 전달하기 위해 필수적입니다. HTTP만으로는 "1초마다 새로고침"하는 비효율적인 방법밖에 없습니다.
Note
🛠️ VS Code 실전 조립 절차
- 파일 생성:
src/gateway/server-ws-runtime.ts파일을 생성합니다. - 실시간 모니터링: 이 파일이 완성되면, 브라우저 관리자 페이지에서 실시간 로그가 올라오는 것을 볼 수 있습니다.
- 주의: WebSocket은 연결이 끊기면 다시 연결해주는 로직이 중요합니다. 나중에
truncateCloseReason같은 도구가 쓰이는 곳이 바로 여기입니다.
이 파일은 ws 라이브러리를 사용하여 실시간 통신을 처리합니다.
- 역할: 웹 대시보드와 서버 사이의 '핫라인' 을 개설합니다. 봇이 지금 무슨 생각을 하는지, 로그는 무엇인지 실시간으로 전송합니다.
- 핵심 부품:
- Heartbeat (생존 신호): "아직 전화 안 끊었지?" (주기적 신호 확인)
- Broadcast (방송): "자, 모든 대시보드 집중! 지금 봇이 답장을 보낸다!" (여러 접속자에게 동시 전송)
import { WebSocketServer } from "ws";
export function attachGatewayWsHandlers(wss: WebSocketServer) {
// 1. [연결 수립] 누군가 전화를 걸어오면(connection 이벤트)
wss.on("connection", (ws) => {
console.log("☎️ 신경망: 새로운 대시보드가 연결되었습니다.");
// 2. [말 듣기] 상대방이 메시지를 보내면
ws.on("message", (data) => {
// 메시지 처리 로직...
});
// 3. [전화 끊기]
ws.on("close", () => {
console.log("💀 신경망: 연결이 종료되었습니다.");
});
});
}여러분, 카톡을 하는데 내가 메시지를 보낼 때까지는 대화창이 안 바뀐다고 상상해 보세요. 상대방이 답장을 보냈는지 확인하려고 계속 '새로고침' 버튼을 눌러야 한다면? 그건 재앙입니다. WebSocket은 서버가 먼저 사용자에게 말을 걸 수 있게 해줍니다. 봇이 수다쟁이가 되려면 이 신경망이 반드시 필요합니다.
이제 입과 귀가 생겼으니, 실제로 외부와 소통할 "팔다리(Limbs)" 를 움직여야 합니다. WhatsApp, Slack 같은 다양한 메신저들을 총괄 관리하는 곳입니다.
Note
🛠️ VS Code 실전 조립 절차
- 파일 생성:
src/gateway/server-channels.ts파일을 생성합니다. - 채널 확장: 나중에 Slack이나 Telegram을 추가하고 싶을 때도 바로 이 파일에 "새로운 다리"를 다는 방식으로 확장하면 됩니다.
- 연결: 이곳에서 만들어진
manager가server.impl.ts로 전달되어 시스템 전체의 행동을 지시하게 됩니다.
- 역할: "카톡 담당자(WhatsApp)", "슬랙 담당자(Slack)"를 소집하고 관리합니다.
- 왜 Manager라고 부르나요?: 손가락 하나하나(채널 하나하나)를 직접 움직이는 게 아니라, "팔을 움직여!" 라고 명령을 내리면 내부에서 알아서 조율하기 때문입니다.
export function createChannelManager() {
const channels = new Map(); // 현재 연결된 팔다리 목록
return {
// 모든 팔다리를 출격(Start) 시킵니다.
startChannels: async (gateway) => {
console.log("🦾 팔다리 부착: WhatsApp 채널을 가동합니다.");
// 여기서 실제로 WhatsApp 채널 로직을 불러옵니다.
},
// 모든 팔다리를 정지(Stop) 시킵니다.
stopChannels: async () => {
console.log("🧘 팔다리 회수: 모든 연결을 안전하게 종료합니다.");
}
};
}Important
여기까지 오셨다면, 여러분은 서버의 '생물학적 구조'를 모두 완성하신 겁니다.
- 안내 데스크(Facacde)
- 내부 장기(Core Logic)
- 입(HTTP)
- 귀(WebSocket)
- 팔다리(Channel Manager)
이제 이 유령 서버에 진짜 영혼을 불어넣을 시간입니다. 다음 챕터에서 [진짜 WhatsApp] 을 연결해 봅시다!
봇이 세상을 인식하는 "눈과 귀" 입니다. 여기서는 WhatsApp을 예로 들어, 외부 시스템과 어떻게 대화하는지 파헤쳐 보겠습니다.
보통 기업들이 봇을 만들 때는 페이스북(Meta)에서 제공하는 공식 "WhatsApp Business API"를 씁니다. 하지만 이건 유료이고, 승인 절차도 까다롭습니다.
OpenClaw(Moltbot)이 선택한 Baileys는 다릅니다.
- 원리: 우리가 브라우저에서 'WhatsApp Web'을 켜서 QR 코드를 찍는 것과 완전히 똑같은 방식으로 동작합니다.
- 은유(Metaphor): "투명 인간 브라우저". Baileys는 화면이 없는(Headless) 브라우저가 되어, 사용자 대신 키보드를 치고 화면을 읽는 역할을 합니다.
이 코드는 단순한 통신 코드가 아니라, "이질적인 언어를 통역하는 통역사" 의 코드입니다.
import makeWASocket, { useMultiFileAuthState } from "@whiskeysockets/baileys";
export async function startWhatsAppChannel(gateway: any) {
// 1. [기억 장치] 세션 유지 (Session Persistence)
// 매번 QR 코드를 찍으면 귀찮겠죠? 로그인 정보를 파일에 저장해둡니다.
const { state, saveCreds } = await useMultiFileAuthState("auth_info_baileys");
// 2. [무전기 켜기] 소켓 연결
const sock = makeWASocket({
auth: state,
printQRInTerminal: true, // 터미널에 QR 코드를 띄웁니다.
});
// 3. [기억 업데이트] 로그인 정보가 바뀌면(새 토큰 등) 다시 저장합니다.
sock.ev.on("creds.update", saveCreds);
// 4. [경청하기] 새로운 메시지가 도착하면 (Upsert)
sock.ev.on("messages.upsert", async ({ messages }) => {
const msg = messages[0];
if (!msg.message) return; // 내용 없는 메시지는 무시
// [중요] 데이터 정규화 (Normalization)
// WhatsApp 특유의 복잡한 JSON을 Gateway가 이해할 수 있는 단순한 형태로 바꿉니다.
const normalizedMsg = {
from: msg.key.remoteJid, // 누가 보냈나?
text: msg.message.conversation || msg.message.extendedTextMessage?.text, // 뭐라고 했나?
timestamp: msg.messageTimestamp
};
// 5. [보고] Gateway 사령부에 알립니다.
// "대장님! 밖에서 이런 메시지가 들어왔습니다!"
gateway.emit("incoming_message", normalizedMsg);
});
}여러분, 지금은 WhatsApp만 연결하지만 나중에 슬랙(Slack) 이나 텔레그램(Telegram) 을 추가한다고 생각해 보세요. 만약 데이터 정규화를 안 하면, 봇의 뇌(AI)는 "이건 WhatsApp 말투네?", "이건 슬랙 말투네?" 하고 매번 다르게 공부해야 합니다. "어디서 오든 똑같은 형식으로 바꿔준다" 는 이 원칙 하나가 나중에 여러분의 노가다(?)를 100배 줄여줍니다.
- 터미널에
pnpm run dev(혹은 서버 실행 명령어)를 칩니다. - 터미널에 거대한 QR 코드가 나타날 때까지 기다리세요.
- 휴대폰에서 WhatsApp 앱을 켭니다.
설정(Settings)->연결된 기기(Linked Devices)->기기 연결을 누릅니다.- 터미널의 QR 코드를 스캔하세요.
- 이제 봇에게 아무 메시지나 보내보세요. 터미널에 여러분의 메시지가 찍힌다면? 축하합니다! 여러분의 봇은 드디어 눈을 떴습니다.
이 앱의 지능을 담당하는 "뇌(Brain)" 입니다. 단순히 앵무새처럼 대답하는 것이 아니라, 스스로 생각하고 도구를 쓰는 AI 에이전트(Agent) 로서의 핵심 로직을 파헤쳐 봅니다.
많은 분이 LLM(ChatGPT 등)을 사용하는 모든 봇을 에이전트라고 부르지만, 기술적으로는 큰 차이가 있습니다.
- 챗봇 (Chatbot): 사용자의 질문에 대해 지식(Knowledge)만으로 대답합니다. 팔다리가 없는 "천재 박사" 와 같습니다.
- 에이전트 (Agent): 질문을 이해한 후, 스스로 컴퓨터의 도구(파일 읽기, 명령어 실행 등)를 사용하여 문제를 해결합니다. 지식과 팔다리를 모두 가진 "해결사" 입니다.
- 핵심 철학: Re-Act (Reasoning + Acting). "생각하고, 행동하고, 결과를 관찰하고, 다시 생각하라."
Tip
🎓 [심화 수업] LLM(거대 언어 모델)의 작동 원리
"AI는 어떻게 사람처럼 말을 하나요? 정말 똑똑한 건가요?"
1. 핵심 원리: "다음에 올 말 맞추기 게임"
- LLM은 지능이 있는 인격체가 아닙니다. 엄밀히 말하면 '매우 정교한 자동 완성 시스템' 입니다.
- 수많은 데이터를 학습해서, "오늘 날씨가 참 [ ]"라는 문장 다음에 올 단어가 "좋네요"일 확률이 95%라는 것을 수학적으로 계산해냅니다.
2. 왜 "거대(Large)" 모델인가요?
- 데이터 양: 인터넷에 있는 거의 모든 텍스트를 읽었습니다.
- 매개변수(Parameter): 뇌세포 사이의 연결 고리가 수천억 개에 달합니다. 이 숫자가 커질수록 AI는 복잡한 문맥과 논리를 이해하게 됩니다.
3. 에이전트에서의 역할
- OpenClaw에서 LLM은 "추론 엔진" 입니다.
- 사용자: "내일 날씨 알려줘."
- LLM: (추론) "날씨를 알려면 '날씨 API'라는 도구를 써야겠군. 이 도구를 쓰겠다고 Gateway에게 알려주자."
요약: LLM은 **'지식의 창고'**이자 **'생각의 엔진'**입니다. 이 엔진에 '팔다리(Tool)'를 달아준 것이 바로 여러분이 만들고 있는 에이전트입니다.
이 코드는 LLM에게 "생각하는 시간" 과 "행동할 도구" 를 쥐어주는 지휘소입니다.
import { ChatOpenAI } from "@langchain/openai";
export async function runAgent(userInput: string, chatHistory: any[]) {
// 1. [정체성 부여] 시스템 프롬프트 (System Prompt)
// 봇에게 "너는 누구고, 어떤 가치관을 가졌는가"를 정의합니다.
const systemMessage = {
role: "system",
content: "너는 Moltbot이야. 리눅스 명령어를 실행하고 파일을 읽을 수 있는 능력이 있어."
};
// 2. [기억 소환] 맥락(Context) 구성
// 이전 대화 내용을 합쳐서 "우리가 무슨 대화를 하고 있었는지"를 AI에게 알려줍니다.
const allMessages = [systemMessage, ...chatHistory, { role: "user", content: userInput }];
// 3. [추론 시작] LLM 호출
const response = await llm.generate(allMessages);
// 4. [행동 판단] 도구 사용 여부 체크
// AI가 "파일을 읽어야겠다"라고 판단하면 특수한 메시지를 보냅니다.
if (response.tool_calls) {
// 실제로 컴퓨터의 파일을 읽거나 명령어를 실행합니다.
const toolResult = await executeTool(response.tool_calls);
// 결과를 다시 AI에게 알려주고 "자, 이제 어떻게 할래?"라고 다시 묻습니다. (재귀)
return runAgent(`도구 실행 결과: ${toolResult}`, [...allMessages, response]);
}
// 5. [최종 응답] 사용자에게 대답 전달
return response.content;
}에이전트에게 가장 어려운 것은 "기억력의 한계" 입니다.
- Context Window: AI는 한 번에 기억할 수 있는 양이 정해져 있습니다. (책 한 권 분량 등)
- Pruning (가지치기): 대화가 너무 길어지면, 가장 오래된 대화부터 삭제하거나 중요한 내용만 요약(Summarization)해서 압축해야 합니다.
- Why?: 이 작업을 안 하면 서버비(Token 비용)가 폭탄처럼 나오고, 결국 AI가 과부하로 대화를 포기하게 됩니다.
pi-embedded-runner.ts의 숨은 공로자는 바로 이 기억 관리 로직입니다.
AI 에이전트를 개발하다 보면 가끔 고집을 피우거나 이상한 소리를 할 때가 있습니다. (Hallucination) 그럴 때는 코드를 고치기 전에 시스템 프롬프트를 수정해 보세요. "친절하게 대답해" 보다는 "너는 5년 차 시니어 엔지니어야. 간결하고 정확하게 팩트 위주로 말해" 라고 구체적으로 역할을 지정하는 것만으로도 성능이 200% 올라갑니다.
관리자가 봇을 제어하고 대화 로그를 실시간으로 확인하는 "얼굴(Face)" 입니다.
보통 웹 앱 하면 React를 떠올리지만, OpenClaw(Moltbot)은 Google이 만든 Lit을 선택했습니다.
- 배경: React는 매우 강력하지만, 라이브러리 크기가 크고 학습 곡선이 있습니다.
- Lit의 특징: 브라우저 표준 기술인 웹 컴포넌트(Web Components) 를 극한으로 활용합니다.
- 장점:
- 가볍다: 별도의 복잡한 빌드 과정 없이도 브라우저에서 매우 빠르게 돌아갑니다.
- 표준이다: 10년 뒤에 React가 사라져도, Lit으로 만든 컴포넌트는 모든 브라우저에서 여전히 작동할 것입니다.
이 코드는 서버의 "귀(WebSocket)" 에서 들려오는 소리를 화면에 "그려주는(Rendering)" 역할을 합니다.
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
@customElement('moltbot-app') // 나만의 HTML 태그 만들기: <moltbot-app></moltbot-app>
export class MoltbotApp extends LitElement {
// 1. [상태 변수] 이 값이 바뀌면 화면이 자동으로 다시 그려집니다.
@state() messages: string[] = [];
// 2. [신경 연결] 컴포넌트가 화면에 나타날 때 호출됩니다.
connectedCallback() {
super.connectedCallback();
// 서버와 실시간 전화선(WebSocket)을 연결합니다.
const ws = new WebSocket(`ws://${location.host}`);
ws.onmessage = (event) => {
// 서버에서 새 메시지가 오면, 기존 목록에 추가합니다.
this.messages = [...this.messages, event.data];
};
}
// 3. [그리기] 실제 화면에 보여줄 HTML을 작성합니다.
render() {
return html`
<div class="chat-window">
<h1>Moltbot 사령부</h1>
<div class="log-list">
${this.messages.map(m => html`<p class="log-item">${m}</p>`)}
</div>
</div>
`;
}
}익숙하지 않은 분들에게는 Lit이 사용하는 Shadow DOM이 낯설 것입니다.
- 은유(Metaphor): "프라이버시 유리 커튼".
- 설명:
moltbot-app내부의 스타일(CSS)은 외부의 스타일과 완벽하게 격리됩니다. 만약 여러분이 실수로 바깥에서p { color: red; }라고 적어도, 이 컴포넌트 내부의 글자색에는 영향을 주지 않습니다. - Why?: 프로젝트가 커졌을 때 디자인이 꼬이는 것을 원천 봉쇄하기 위한 고급 기술입니다.
관리자 화면이 너무 투박해 보이나요? 걱정 마세요. Lit은 css라는 특수 함수를 통해 아름다운 스타일을 적용할 수 있습니다.
나중에 여러분이 프로젝트를 커스터마이징 하고 싶을 때, 이 app.ts 파일의 css 부분만 건드리면 다크 모드나 애니메이션도 손쉽게 넣을 수 있습니다.
지금까지 우리는 OpenClaw(Moltbot)의 거대한 설계도를 준비 -> 기초 -> 장기 -> 뇌 -> 얼굴 순으로 해부해 보았습니다. 이 거대한 코끼리를 한 번에 삼키는 것은 불가능합니다. 하지만 "부위별로 조립" 하면 누구나 자신만의 에이전트를 가질 수 있습니다.
-
[1주 차] 유령 서버 띄우기 (Scaffold):
pnpm init부터server.ts의 껍데기만 만드세요.- 브라우저에서
ok글자 하나 띄우는 것만으로도 여러분은 상위 10% 개발자입니다.
-
[2주 차] 가짜 채널과 대화하기 (Mocking):
- 진짜 WhatsApp을 연결하기 전에, 터미널(CLI)에 글을 치면 봇이 대답하게 만드세요.
- 핵심: 복잡한 통신 모듈(Baileys)을 배제하고 순수 로직만 테스트하는 기간입니다.
-
[3주 차] 뇌(AI) 이식하기 (Integration):
- LangChain이나 OpenAI API를 연결하세요.
- 이제 봇이 "알겠습니다" 대신 진짜 생각을 하기 시작합니다.
-
[4주 차] 진짜 세상으로 (Deployment):
- QR 코드를 찍어 WhatsApp에 연결하고, 라즈베리 파이나 클라우드에 서버를 배포하세요.
- 친구들에게 링크를 공유하고 봇이 대답하는 모습을 지켜보세요.
리버스 엔지니어링은 단순히 남의 코드를 훔쳐보는 것이 아닙니다. "이 사람은 왜 여기서 이 고민을 했을까?" 라고 거장과 대화하는 과정입니다.
어렵고 에러가 나더라도 포기하지 마세요. 에러 메시지는 여러분에게 "사령관님, 이 부분의 보급이 부족합니다!" 라고 외치는 전보와 같습니다. 하나씩 해결하다 보면 어느덧 여러분은 AI 시대를 이끌어갈 최고 수준의 시스템 아키텍트가 되어 있을 것입니다.
자, 이제 이론은 끝났습니다. 터미널을 켜고 첫 번째 명령어를 치십시오!

