TRENDSHIFT · 2026.06.15

chatwoot 딥다이브
— 인터컴/젠데스크를 대체하는 오픈소스 옴니채널 고객지원 플랫폼의 설계

chatwoot웹 채팅·이메일·페이스북·인스타그램·왓츠앱·텔레그램·SMS 등 흩어진 모든 고객 문의를 하나의 통합 받은편지함(inbox)으로 모아 응대하는 오픈소스 자체호스팅(self-hosted) 고객지원 플랫폼이다. Intercom·Zendesk·Salesforce Service Cloud의 오픈소스 대안을 표방하며, Ruby on Rails 백엔드 + Vue 3 프론트엔드로 짜인 대형 모놀리식 애플리케이션이다. 최근에는 Captain이라는 AI 상담 에이전트(RAG·LLM)를 엔터프라이즈 모듈로 얹어 트렌딩에 다시 올랐다. (저장소: chatwoot/chatwoot · Rails 7.1 · Vue 3 · MIT 라이선스 · 버전 4.14.2 · GitHub 스타 25k+)

이 문서는 README 소개가 아니라 "수많은 외부 채널과 실시간 메시지를 어떻게 하나의 Rails 앱으로 묶어내는가"라는 아키텍처 관점에서, 채널 추상화·이벤트 시스템·실시간 통신·백그라운드 잡 처리·AI 통합을 코드 구조 기준으로 해부한다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 시스템 / 환경 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

이 레포가 무엇을 하는 물건인가.

핵심 메시지

"고객이 어느 창구로 말을 걸든,
상담원에게는 똑같은 모양의 대화 한 줄로 보이게 만드는 통합 안내데스크."

고객은 웹사이트 채팅 위젯에서, 누구는 인스타 DM에서, 누구는 왓츠앱에서, 누구는 이메일로 문의한다. chatwoot은 이 제각각인 입력 통로(채널)를 전부 받아서, 상담원에게는 "대화(conversation) → 메시지(message)"라는 통일된 모델로 보여 준다. 상담원이 답을 쓰면, chatwoot이 다시 알아서 그 고객이 쓰던 원래 채널의 형식으로 변환해 돌려보낸다.

즉 chatwoot의 본질은 "N개의 서로 다른 메시징 API를 하나의 받은편지함으로 정규화(normalize)하는 어댑터 계층 + 실시간 협업 대시보드"다. 자체호스팅이라 고객 데이터를 내 서버에 둔다는 점이 SaaS인 Intercom과 갈라지는 결정적 지점이다.

chatwoot옴니채널(omnichannel) 헬프데스크다. 라이브챗 위젯, 이메일, Facebook/Instagram, WhatsApp, Telegram, Line, SMS(Twilio), 보이스(Twilio Voice) 등 채널을 단일 인박스로 통합하고, 자동 배정·라벨·캔드응답(canned response)·자동화 규칙·매크로·CSAT 설문·헬프센터 포털·리포팅까지 고객지원에 필요한 거의 모든 기능을 갖췄다. 최근 버전은 Captain(AI 에이전트)으로 자주 묻는 질문을 자동 응대하고, Copilot으로 상담원을 보조한다.

용어
옴니채널 (Omnichannel)
고객이 어떤 채널(웹챗·이메일·SNS·전화)로 연락해도 하나의 연속된 대화 맥락으로 응대하는 방식. 멀티채널이 "여러 창구를 따로 운영"하는 것이라면, 옴니채널은 "모든 창구를 한 인박스로 합쳐 끊김 없이" 응대하는 것이 차이다.
용어
자체호스팅 (Self-hosted)
공급사 서버가 아니라 내(조직) 서버·클라우드 계정에 직접 설치해 운영하는 방식. 데이터 주권·규제 준수(개인정보가 국외로 나가지 않음)·비용 통제가 장점이고, 운영(업그레이드·백업·확장)을 직접 책임져야 하는 것이 대가다. chatwoot은 Docker·Helm으로 자체호스팅을 1급으로 지원한다.

2왜 주목받는가

트렌딩 이유 · Intercom/Zendesk 등 경쟁 대비 장점.

고객지원 SaaS(Intercom·Zendesk)는 강력하지만 좌석당 월 구독료가 비싸고, 고객 데이터가 외부 서버에 쌓인다. chatwoot이 꾸준히 별을 모으는 이유는 ① 자체호스팅으로 데이터 주권을 가져오면서기능은 상용에 근접하고 ③ 최근 Captain AI 에이전트로 "AI 고객지원" 흐름에 올라탔기 때문이다. MIT 라이선스 코어 + 엔터프라이즈 모듈(별도 라이선스)의 오픈코어 모델이다.

비교 항목Intercom / Zendesk (SaaS)chatwoot
호스팅벤더 클라우드 전용자체호스팅(Docker/Helm/Heroku/DO) 또는 클라우드
데이터 주권외부 서버에 저장내 서버에 저장 — GDPR·국내법 대응 용이
비용좌석당 월 구독(고가)코어 무료(MIT) · 인프라 비용만
채널다수 지원웹챗·이메일·FB·IG·WA·TG·Line·SMS·Voice 등 옴니채널
AIFin 등 자체 AICaptain(에이전트)·Copilot — 자체 RAG/LLM, 모델 교체 가능
확장제한된 API/앱오픈소스 코드 직접 수정·웹훅·대시보드 앱·에이전트 봇
커스터마이즈설정 범위 내Rails+Vue 코드 풀 액세스 · 자체 채널 추가 가능
기존 방식의 한계
"채널마다 따로" — 파편화된 응대와 데이터 종속

SNS 메시지는 SNS 앱에서, 이메일은 메일함에서, 채팅은 또 다른 도구에서 각각 응대하면 고객 한 명의 맥락이 흩어진다. 어느 상담원이 무슨 답을 했는지 추적이 안 되고, 같은 질문에 매번 새로 답한다. 게다가 상용 SaaS에 모든 대화를 맡기면 데이터가 벤더에 종속되고, 좌석이 늘수록 비용이 가파르게 오른다.

chatwoot의 해결
"채널 어댑터 + 통합 대화 모델 + 내 서버"

chatwoot은 모든 채널을 Inbox라는 추상 위로 정규화해 단일 대화 타임라인으로 만든다. 상담원은 한 화면에서 모든 채널을 응대하고, 자동 배정·라벨·자동화로 워크플로를 스케일한다. 그리고 이 모든 것을 내가 통제하는 서버에서 돌리므로 데이터 주권과 비용 통제를 동시에 얻는다. 여기에 Captain AI가 반복 질문을 흡수해 상담원은 복잡한 건만 다룬다.

3기술 스택 전체 지도

백엔드(Rails)·프론트엔드(Vue 3)·인프라(Postgres·Redis·Sidekiq) 각각.

① 백엔드 — Ruby on Rails 7.1 모놀리스

코어는 Ruby 3.4 + Rails 7.1로 짜인 모놀리식 애플리케이션이다. API(REST)는 Vue 대시보드와 위젯·외부 통합을 모두 먹여 살린다. 채널 연동·실시간·백그라운드 처리가 핵심이라 관련 젬이 두텁게 깔려 있다.

요소역할
Rails 7.1 / Ruby 3.4웹 프레임워크 본체. API 컨트롤러·모델·서비스 객체 중심.
devise + devise_token_auth + devise-two-factor인증. 세션·토큰 인증·2FA(OTP)·SAML(엔터프라이즈)까지.
pundit인가(authorization). 정책 객체(policy)로 "누가 무엇을 할 수 있나" 분리.
sidekiq + sidekiq-cron백그라운드 잡 처리(Redis 기반). 메시지 전송·웹훅·스케줄 잡.
wisper발행/구독(pub-sub) 이벤트 버스. 도메인 이벤트를 리스너로 분배.
ActionCable웹소켓 실시간 — 새 메시지·타이핑·상태를 대시보드로 push.
liquid템플릿 엔진 — 캔드응답·자동화 메시지에 변수 치환.
opensearch-ruby전문(full-text) 검색 — 대화·연락처·메시지 검색 인덱싱.
administrate슈퍼어드민(설치 관리자) 백오피스 UI 자동 생성.
Active Storage (S3/GCS/Azure)첨부파일 저장 — 클라우드 스토리지 어댑터 추상화.

② 프론트엔드 — Vue 3 SPA + 임베드 위젯

프론트는 하나가 아니다. 상담원 대시보드, 고객이 웹사이트에 다는 채팅 위젯, 헬프센터 포털, 설문(survey) 등 여러 진입점(entrypoint)이 같은 빌드 파이프라인(Vite) 위에 올라간다.

요소역할
Vue 3 (Composition API)전체 UI 프레임워크. app/javascript/dashboard 등.
pinia상태 관리(Vuex 후계). 대화·연락처·UI 상태 스토어.
vue-router대시보드 SPA 라우팅.
vue-i18n + Crowdin다국어. 50+ 언어를 Crowdin으로 번역 관리.
Tailwind CSS유틸리티 우선 스타일. 디자인 시스템(design-system).
ProseMirror (@chatwoot/prosemirror-schema)메시지 입력기 — 리치텍스트·멘션·서식.
Chart.js (vue-chartjs)리포팅 대시보드 차트.
ActionCable JS / @rails/actioncable웹소켓 클라이언트 — 실시간 수신.
@chatwoot/ninja-keys (커맨드바)키보드 단축 커맨드 팔레트.
SDK (app/javascript/sdk/sdk.js)고객 사이트에 삽입하는 위젯 로더(iframe 임베드).

③ 인프라 · 데이터 · 배포

요소역할
PostgreSQL주 데이터베이스. 계정·대화·메시지·연락처. pg 젬.
RedisSidekiq 큐·ActionCable 펍섭·캐시. redis-namespace.
OpenSearch검색 인덱스(선택). 대규모 대화/연락처 검색.
Docker / docker-compose웹·worker(sidekiq)·postgres·redis 컨테이너 구성.
Helm 차트Kubernetes 배포(ArtifactHub 공식 차트).
Procfile (web / worker)웹 서버 + Sidekiq 워커 프로세스 분리 실행.
Vite프론트 번들러. 위젯·대시보드·SDK 각각 빌드 설정.
Sentry / New Relic에러·성능 모니터링(선택).
외부 APITwilio(SMS/Voice)·Meta Graph(FB/IG)·WhatsApp·Telegram·Slack·OpenAI/LLM·Firecrawl(크롤링).
용어
백그라운드 잡 (Background Job) / Sidekiq
웹 요청 안에서 처리하면 너무 느린 일(외부 API로 메시지 전송, 이메일 발송, 웹훅 호출, 리포트 집계)을 큐에 넣어 별도 워커 프로세스가 나중에 처리하는 방식. Sidekiq은 Redis를 큐 저장소로 쓰는 Ruby의 표준 백그라운드 처리 엔진이다. chatwoot은 채널 송수신 대부분을 잡으로 돌린다.

4아키텍처 심화 분석

채널 어댑터 → 통합 대화 모델 → 이벤트 버스 → 실시간/백그라운드.

전체 흐름 한눈에

외부 채널(왓츠앱·인스타·웹챗 등)에서 들어온 메시지는 채널별 Inbox를 거쳐 공통 Conversation / Message 모델로 정규화된다. 메시지가 생성되면 wisper 이벤트가 발행되고, 여러 리스너가 이를 받아 자동화 규칙 실행·웹훅·알림·실시간 브로드캐스트(ActionCable)·검색 인덱싱 등을 각자 비동기로 처리한다. 상담원이 답을 쓰면 역방향으로 채널 어댑터가 원래 플랫폼 API 형식으로 변환해 Sidekiq 잡으로 송신한다.

외부 채널 (들어오는 방향, INBOUND) WhatsApp │ Instagram │ FB │ Telegram │ 웹챗위젯 │ 이메일 │ SMS/Voice(Twilio) │ │ │ │ │ │ │ ▼ (Webhook / IMAP / WebSocket) ▼ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ 채널별 컨트롤러 / 콜백 (app/controllers/.../callbacks) │ │ 서명검증 · 페이로드 파싱 · 채널 매칭 │ └───────────────────────────────┬──────────────────────────────────┘ │ Builder 패턴 ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Inbox ──(channel: WhatsApp/Email/WebWidget/...) │ │ ContactInbox 로 발신자(Contact) 식별/생성 │ │ ↓ 정규화 │ │ Conversation ──> Message (단일 통합 모델) │ └───────────────────────────────┬──────────────────────────────────┘ │ after_create → wisper 이벤트 발행 ══════════════ wisper 이벤트 버스 (pub/sub) ══════════════ "conversation.created" · "message.created" · "assignee.changed" ... ┌───────────┬───────────┬───────────┬───────────┬──────────────┐ ▼ ▼ ▼ ▼ ▼ ▼ 자동화규칙 웹훅 발송 상담원 알림 실시간 푸시 검색 인덱싱 리포팅 이벤트 (AutomationRule)(webhook) (notification)(ActionCable)(OpenSearch) (reporting_event) │ │ │ │ └─ 무거운 작업은 전부 Sidekiq 잡으로 위임 (Redis 큐) ─┘ │ ───────────────────────────── 나가는 방향 (OUTBOUND) ────────────── 상담원/봇/Captain이 답장 작성 │ ▼ SendReplyJob → 채널별 Send-on-Channel 서비스 ┌──────────────────────────────────────────────────────────────────┐ │ app/services/{whatsapp,facebook,telegram,...} │ │ 통합 Message → 각 플랫폼 API 페이로드로 역변환 후 전송 │ └──────────────────────────────────────────────────────────────────┘ │ ▼ WhatsApp / Instagram / 웹챗(ActionCable) / 이메일(SMTP) 로 도착

핵심 설계 패턴 1 — 채널 다형성(polymorphism)

chatwoot의 심장부는 Inbox 모델이 다형적 연관(polymorphic association)으로 실제 채널을 가리킨다는 점이다. Inboxchannel_type + channel_id를 들고, 그 채널은 Channel::WebWidget·Channel::Whatsapp·Channel::Email·Channel::FacebookPage 등 11종 중 하나다. 상위 로직(대화·메시지)은 채널 종류를 몰라도 되고, 채널별 차이는 각 Channel 모델과 대응 서비스에 캡슐화된다. 새 채널을 추가해도 Conversation/Message 코드는 손대지 않는다 — 이것이 옴니채널 확장성의 비결이다.

채널 모델 목록 (app/models/channel/)
Channel::WebWidget      # 웹사이트 임베드 채팅
Channel::Email          # IMAP/SMTP 이메일
Channel::FacebookPage   # 페이스북 페이지 메신저
Channel::Instagram      # 인스타그램 DM
Channel::Whatsapp       # WhatsApp Cloud/온프레미스
Channel::Telegram       # 텔레그램 봇
Channel::Line           # LINE
Channel::Sms / TwilioSms# SMS
Channel::Api            # 커스텀(직접 API로 메시지 주입)
Channel::TwitterProfile # X/트위터
Channel::Tiktok         # 틱톡

각 채널은 has_one :inbox, as: :channel 관계다. Inbox 입장에서는 belongs_to :channel, polymorphic: true — 무엇이 붙든 똑같이 다룬다.

핵심 설계 패턴 2 — 이벤트 기반(wisper) + 리스너

"메시지가 생기면 해야 할 일"이 너무 많다(알림·웹훅·자동화·실시간·검색·리포팅). 이걸 모델 콜백에 다 욱여넣으면 결합이 폭발한다. chatwoot은 wisper pub/sub 이벤트 버스로 분리했다. 도메인 동작이 일어나면 이벤트를 broadcast하고, app/listeners/의 각 리스너가 관심 있는 이벤트만 구독해 처리한다. 발신자는 누가 듣는지 모르고, 수신자는 누가 보냈는지 신경 쓰지 않는다 — 느슨한 결합(loose coupling)의 교과서다.

패턴chatwoot에서의 구현
다형적 채널Inbox belongs_to :channel, polymorphic — 채널 11종을 균일 취급.
Builder 객체app/builders/ — 인바운드 메시지를 대화/메시지로 조립하는 책임 분리.
Service 객체app/services/{facebook,whatsapp,...} — 채널별 외부 API 송수신 캡슐화.
pub/sub 이벤트wisper + app/listeners/ + app/dispatchers/ — 도메인 이벤트 분배.
백그라운드 잡app/jobs/ + Sidekiq — 외부 송신·웹훅·집계를 비동기로.
정책(policy) 인가app/policies/ + Pundit — 멀티테넌트 권한 분리.
멀티테넌시Account 단위로 모든 데이터 스코핑 — 한 인스턴스에 여러 조직.
오픈코어enterprise/ 디렉터리에 유료 기능(Captain·SLA·SAML 등) 격리.
비유 — 이벤트 버스

chatwoot의 이벤트 시스템을 "회사 사내 방송(공지 방송)"으로 생각하자. "3번 창구에 새 고객 도착!"이라고 한 번 방송(broadcast)하면, 그 방송을 듣고 경비팀(알림)·기록팀(리포팅)·안내데스크(실시간 화면 갱신)·외부협력사(웹훅)가 각자 알아서 움직인다. 방송하는 사람은 누가 듣는지 일일이 호출할 필요가 없고, 새 부서(리스너)가 생겨도 방송 로직은 그대로다.

핵심 설계 패턴 3 — 실시간 통신(ActionCable)

고객이 위젯에서 타이핑하는 순간 상담원 화면에 점이 깜빡이고, 새 메시지가 즉시 뜬다. 이건 ActionCable 웹소켓으로 처리한다. app/channels/room_channel.rb가 클라이언트 구독을 받고, 서버는 ActionCableBroadcastJob으로 해당 계정/대화 구독자에게만 이벤트를 push한다. Redis가 펍섭 백엔드라 웹 서버를 여러 대로 늘려도 실시간이 깨지지 않는다(수평 확장 가능).

용어
멀티테넌시 (Multi-tenancy)
하나의 애플리케이션 인스턴스로 여러 독립 조직(tenant)을 동시에 서비스하는 구조. chatwoot은 모든 데이터를 Account로 스코핑해, A회사의 상담원이 B회사 대화를 절대 못 보게 격리한다. Pundit 정책과 쿼리 스코프가 이 경계를 강제한다.

핵심 설계 패턴 4 — Captain: RAG 기반 AI 에이전트

최근 트렌딩의 동력인 Captainenterprise/app/services/captain/에 격리돼 있다. 헬프센터 문서·과거 대화를 임베딩(embedding)해 벡터로 저장(ArticleEmbedding 모델)하고, 고객 질문이 오면 유사 문서를 검색(RAG) → LLM에 컨텍스트로 주입 → 답변 생성한다. tool_registry_service로 LLM이 호출할 수 있는 도구(예: 대화 이관, 정보 조회)를 등록하는 툴 호출(function calling) 구조도 갖췄다. Copilot은 같은 엔진을 상담원 보조(답변 초안 제안)로 쓴다. 모델은 OpenAI 호환이라 교체 가능하다.

용어
RAG (Retrieval-Augmented Generation)
LLM이 답하기 전에 관련 문서를 먼저 검색해 프롬프트에 끼워 넣는 기법. "모델이 학습 안 한 우리 회사 FAQ"를 답하게 만드는 표준 방법으로, 환각(hallucination)을 줄인다. Captain은 헬프센터 글을 임베딩→벡터검색으로 끌어와 근거 있는 답을 만든다.

5디렉토리 구조 해부

전형적 Rails 레이아웃 + 옴니채널 특화 디렉터리 + enterprise 분리.

chatwoot/ ├── app/ ★ Rails 애플리케이션 본체 │ ├── models/ 도메인 모델 (Account·Conversation·Message·Contact·Inbox...) │ │ ├── channel/ ★ 11종 채널 모델 (다형적) — whatsapp/email/web_widget... │ │ ├── concerns/ 공유 모듈(mixin) │ │ └── integrations/ 외부 통합(슬랙·다이얼로그플로우 등) 모델 │ ├── controllers/ ★ API 컨트롤러 + 채널 webhook 콜백 │ │ └── api/v1/ 대시보드/위젯/플랫폼 REST API │ ├── builders/ ★ 인바운드 메시지 → 대화/메시지 조립 │ ├── services/ ★★ 채널별 송수신 로직 (facebook/whatsapp/telegram...) │ ├── listeners/ ★ wisper 이벤트 구독자 (알림·자동화·웹훅 트리거) │ ├── dispatchers/ 이벤트 디스패처(동기/비동기 분배) │ ├── jobs/ ★ Sidekiq 백그라운드 잡 (SendReplyJob·WebhookJob...) │ ├── channels/ ★ ActionCable (room_channel.rb — 실시간 웹소켓) │ ├── policies/ Pundit 인가 정책 (멀티테넌트 권한) │ ├── finders/ 복잡한 조회 쿼리 객체 │ ├── mailboxes/ ActionMailbox — 수신 이메일 라우팅 │ ├── mailers/ 발신 이메일 │ ├── drops/ Liquid 템플릿 변수 노출 객체 │ ├── views/ 서버 렌더 뷰(메일·위젯 셸·관리자) │ └── javascript/ ★★ 프론트엔드 (Vue 3) │ ├── dashboard/ 상담원 대시보드 SPA │ ├── widget/ 고객용 채팅 위젯 │ ├── sdk/ 위젯 임베드 SDK (sdk.js) │ ├── portal/ 헬프센터 포털 │ ├── survey/ CSAT 설문 페이지 │ ├── design-system/ 공통 UI 컴포넌트 │ └── shared / v3 공유 유틸 · 차세대 UI ├── enterprise/ ★★ 오픈코어 유료 모듈 (별도 라이선스) │ └── app/services/captain/ ★ AI 에이전트(RAG·LLM·copilot·tools) │ └── ... sla / voice / firecrawl / saml ├── config/ Rails 설정 (routes·sidekiq·cable·환경별) ├── db/ 마이그레이션 · 스키마 ├── lib/ 범용 라이브러리·태스크 ├── swagger/ API 문서(OpenAPI) ├── spec/ RSpec 테스트 ├── docker/ · docker-compose.yaml · deployment/ 배포 구성 ├── Gemfile / Gemfile.lock Ruby 의존성 ├── package.json / pnpm-lock JS 의존성 ├── Procfile(.dev) web + worker(sidekiq) 프로세스 └── tailwind.config.js / vite.config.ts 프론트 빌드
읽는 순서 추천

app/models/inbox.rb + app/models/channel/로 "채널 다형성"의 실물 → ② app/models/conversation.rb·message.rb로 통합 대화 모델 → ③ app/builders/로 인바운드 조립 → ④ app/listeners/ + wisper로 이벤트 분배 → ⑤ app/services/whatsapp(또는 facebook)로 채널별 송수신 → ⑥ app/javascript/dashboard로 프론트. 옴니채널의 핵심 통찰은 ①~⑤에 거의 다 있다.

6학습 포인트 (기술별)

대형 Rails 모놀리스 + 옴니채널 + 실시간에서 무엇을 배우나.

A. 다형적 추상화로 N개 외부 시스템 통합하기

chatwoot 최대의 교훈은 "서로 다른 11개 메시징 API를 하나의 모델로 정규화하는 법"이다. 핵심은 ① 공통 인터페이스(Inbox/Conversation/Message)를 먼저 정의하고, ② 채널별 차이를 Channel::* 모델 + 대응 Service로 캡슐화하며, ③ 다형적 연관으로 상위 로직을 채널 무지(channel-agnostic) 상태로 유지하는 것. 새 채널 추가가 "코어 수정"이 아니라 "어댑터 하나 추가"가 되도록 설계하는 패턴은 결제·알림·스토리지 등 모든 통합에 그대로 적용된다.

실습: Channel::WebWidgetChannel::Whatsapp이 각각 어떤 칼럼/메서드를 갖고, Inbox가 둘을 어떻게 균일하게 다루는지 코드로 추적하라. "공통" vs "채널 고유"의 경계선을 정리해 본다.

B. 이벤트 기반 아키텍처와 느슨한 결합

모델 콜백에 모든 부수효과를 넣는 대신, wisper로 도메인 이벤트를 발행하고 리스너가 구독하는 구조를 배운다. app/listeners/를 읽으면 "메시지 생성 → 알림/웹훅/자동화/실시간"이 어떻게 독립적으로 갈라지는지 보인다. 이 패턴은 마이크로서비스로 쪼개지 않고도 모놀리스 안에서 책임을 분리하는 강력한 도구다.

실습: message.created 이벤트를 구독하는 리스너를 전부 찾아 표로 정리하라(이벤트명 → 리스너 → 무슨 잡). 새 리스너 하나(예: "긴 메시지면 슬랙 알림")를 추가하는 코드를 머릿속으로 설계해 본다.

C. 실시간 + 백그라운드: 응답성과 처리량 분리

웹 요청은 빨라야 하고(실시간 push), 무거운 일은 미뤄야 한다(백그라운드). chatwoot은 ActionCable(즉시 push) + Sidekiq(지연 처리) 두 축으로 이를 분리한다. "사용자에게 즉시 보여줄 것"과 "나중에 처리해도 될 것"을 가르는 기준을 코드로 체득할 수 있다. Redis가 양쪽(펍섭 백엔드 + 잡 큐)을 받치는 구조도 핵심.

실습: 메시지 전송이 (1) DB 저장 (2) 실시간 브로드캐스트 (3) 외부 채널 송신 (4) 웹훅으로 어떻게 나뉘는지 추적하고, 각각이 "동기"인지 "Sidekiq 잡"인지 분류하라. 왜 그렇게 나눴는지 설명해 본다.

D. 멀티테넌시와 인가(Pundit)

한 인스턴스에 여러 조직이 공존하는 SaaS의 기본기를 배운다. Account 스코핑으로 데이터를 격리하고, Pundit 정책 객체로 "이 사용자가 이 리소스에 무엇을 할 수 있나"를 컨트롤러 밖으로 분리한다. app/policies/를 읽으면 권한 로직이 모델·컨트롤러에 흩어지지 않고 한곳에 모이는 이점이 보인다.

실습: ConversationPolicy(또는 유사 정책)를 읽고, 관리자/상담원/봇이 각각 무엇을 할 수 있는지 권한 매트릭스를 그려라. 멀티테넌트 누수(다른 Account 데이터 접근)를 막는 코드 지점을 찾아본다.

E. 오픈코어 모듈 분리와 AI(RAG) 통합

enterprise/ 디렉터리로 유료 기능을 코어와 물리적으로 분리하는 오픈코어 운영 방식, 그리고 captain/RAG 파이프라인(임베딩 → 벡터검색 → LLM → 툴 호출)을 실서비스 코드로 학습한다. "AI 기능을 기존 대형 앱에 어떻게 깔끔히 얹는가"의 좋은 레퍼런스다.

실습: enterprise/app/services/captain/에서 "문서 임베딩 → 질문 시 검색 → LLM 호출"의 흐름을 함수 단위로 따라가라. tool_registry_service가 LLM에 어떤 도구를 노출하는지 목록화해 본다.

7시스템 / 환경 요구사항

단일 바이너리가 아니라 "여러 프로세스 + 데이터 저장소"의 조합이다.

항목요구사항
런타임Ruby 3.4.x + Node 24.x(빌드용). 프로덕션은 보통 Docker 이미지 사용.
데이터베이스PostgreSQL(필수). 메시지·대화·연락처 등 핵심 데이터.
캐시·큐Redis(필수) — Sidekiq 큐 + ActionCable 펍섭.
검색(선택)OpenSearch — 대규모 검색 인덱싱. 없으면 DB 검색으로 폴백.
프로세스 구성web(Puma) + worker(Sidekiq) 최소 2종 프로세스. 실시간 위해 ActionCable.
스토리지로컬 또는 S3/GCS/Azure(첨부파일). 프로덕션은 클라우드 스토리지 권장.
외부 자격증명채널별 API 키(Twilio·Meta·WhatsApp·Telegram), 메일 SMTP/IMAP, (선택) OpenAI 키.
배포 옵션docker-compose(소규모) · Helm/Kubernetes(대규모) · Heroku/DigitalOcean 원클릭 · 매니지드 클라우드(Chatwoot Cloud).
리버스 프록시웹소켓(ActionCable) 통과를 위한 Nginx/Caddy 설정 필요(WS upgrade).
실전 팁
"환경변수가 곧 설정" — .env.example을 먼저 읽어라

chatwoot 설치의 90%는 .env 채우기다. FRONTEND_URL·POSTGRES_*·REDIS_URL·메일 설정·채널 API 키가 전부 환경변수다(저장소 .env.example 약 270줄). 처음엔 docker-compose로 web+worker+postgres+redis를 한 번에 띄우는 게 가장 빠르며, 채널은 하나씩(웹챗→이메일→SNS) 붙여 가며 검증하는 것을 권장한다.

8직접 해볼 수 있는 실습 과제

난이도별 5단계 — 로컬 docker-compose로 띄워 두고 진행.

과제 1 난이도 ★☆☆☆☆

로컬에 띄우고 웹챗 위젯 붙이기

docker-compose로 chatwoot을 띄운 뒤, 새 계정을 만들고 웹사이트 라이브챗 인박스를 생성한다. 발급된 위젯 스크립트(sdk.js 임베드)를 빈 HTML 페이지에 붙여, 그 페이지에서 채팅을 보내고 대시보드에서 받아 응대해 본다. 옴니채널의 가장 단순한 한 채널을 끝까지 경험하는 것이 목적.

과제 2 난이도 ★★☆☆☆

이벤트 → 리스너 흐름 추적

메시지를 하나 보내고, 코드에서 message.created 류 이벤트가 발행되는 지점과 그것을 구독하는 app/listeners/ 리스너들을 모두 찾아 "이벤트 → 리스너 → 잡" 매핑 표를 만든다. Sidekiq 웹 UI(/sidekiq)로 실제 어떤 잡이 큐에 쌓이는지 눈으로 확인한다.

과제 3 난이도 ★★★☆☆

자동화 규칙 + 캔드응답 만들기

"새 대화에 특정 키워드가 있으면 자동으로 라벨 붙이고 팀에 배정"하는 자동화 규칙(AutomationRule)을 UI로 만들고, 코드(app/models/automation_rule.rb + 관련 서비스)에서 조건·액션이 어떻게 평가·실행되는지 따라간다. Liquid 변수가 들어간 캔드응답도 하나 작성해 본다.

과제 4 난이도 ★★★★☆

API 채널로 커스텀 채널 만들기

Channel::Api(커스텀 채널)를 이용해, 외부에서 REST API로 메시지를 주입하고 웹훅으로 답장을 받아 처리하는 나만의 채널 어댑터(예: 사내 챗봇 연동)를 작은 스크립트로 구현한다. 다형적 채널 추상화가 "코어 수정 없이 새 채널"을 어떻게 가능케 하는지 체감한다.

과제 5 난이도 ★★★★★

Captain(RAG) 흐름 분석 + 미니 RAG 재현

enterprise/app/services/captain/를 정독해 "헬프센터 글 임베딩 → 질문 시 벡터검색 → LLM 컨텍스트 주입 → 답변"의 파이프라인을 함수 단위로 정리한다. 그다음 별도 작은 스크립트로 "문서 몇 개를 임베딩해 저장하고, 질문에 유사 문서를 붙여 LLM에 보내는" 미니 RAG를 직접 재현해 본다.

9관련 기술 심화 학습 로드맵

6주 코스 — Rails 도메인 설계부터 실시간·AI 통합까지.

주차주제실습 · 참고
1주차Rails 도메인 모델링 — Account·Inbox·Conversation·Message 관계app/models/ · ERD 직접 그리기 · 다형적 연관 학습
2주차채널 다형성 & 어댑터 패턴 — 외부 API 통합 정규화app/models/channel/ · app/services/{facebook,whatsapp}
3주차이벤트 기반 설계 — wisper pub/sub, 리스너/디스패처app/listeners/ · app/dispatchers/ · 느슨한 결합 원리
4주차백그라운드 처리 — Sidekiq 잡·큐·재시도·스케줄(cron)app/jobs/ · Sidekiq 웹 UI · Redis 모니터링
5주차실시간 & 멀티테넌시 — ActionCable·Pundit 인가·Account 스코핑app/channels/ · app/policies/ · WS 수평 확장
6주차AI 통합(RAG) & 프론트(Vue 3) — Captain·Copilot·Pinia 스토어enterprise/.../captain/ · app/javascript/dashboard/

10핵심 키워드 사전

본문·소스에 나온 용어 빠른 참조.

용어의미
옴니채널웹챗·이메일·SNS·전화 등 모든 창구를 하나의 연속 대화로 응대.
Inboxchatwoot의 채널 추상 단위. 다형적으로 실제 채널(WebWidget/Whatsapp...)을 가리킴.
Conversation / Message채널 무관 통합 대화·메시지 모델. 모든 채널이 이리로 정규화됨.
Contact / ContactInbox고객(연락처)과 그가 어느 채널로 들어왔는지 매핑.
Account멀티테넌시 단위(조직). 모든 데이터가 이 스코프로 격리.
다형적 연관한 모델이 여러 종류의 다른 모델을 균일하게 가리키는 Rails 패턴.
wisperpub/sub 이벤트 버스 젬. 도메인 이벤트를 리스너로 분배(느슨한 결합).
Listener / Dispatcher이벤트 구독자 / 이벤트 분배기(app/listeners, app/dispatchers).
Builder인바운드 페이로드를 대화/메시지로 조립하는 객체(app/builders).
SidekiqRedis 기반 백그라운드 잡 처리 엔진. 외부 송신·웹훅·집계 담당.
ActionCableRails 웹소켓. 새 메시지·타이핑을 실시간 push(Redis 펍섭).
Pundit정책 객체 기반 인가 라이브러리. 멀티테넌트 권한 분리.
AutomationRule / Macro조건-액션 자동화 / 일괄 작업 매크로.
Canned Response자주 쓰는 답변 템플릿(Liquid 변수 치환 가능).
CSAT고객 만족도 설문(대화 종료 후 평가).
Captain / CopilotRAG 기반 AI 상담 에이전트 / 상담원 보조(엔터프라이즈).
RAG검색으로 끌어온 문서를 LLM 프롬프트에 주입해 근거 있는 답 생성.
오픈코어코어는 오픈소스(MIT), 일부 고급 기능(enterprise/)은 별도 라이선스.
자체호스팅벤더 서버가 아닌 내 서버에 직접 설치·운영(데이터 주권).

11참고 링크