ChatbotArchitecturepnpm WorkspacesVue 3FirebaseSaaSMultitenant
CongKong Chatbot 아키텍처 — pnpm 모노레포와 멀티테넌트 설계
2026. 4. 15.1분 읽기
무엇을 만들었나
SaaS 챗봇 플랫폼. 고객사가 1줄 script 태그를 붙이면 자체 브랜딩의 챗봇이 즉시 동작한다.
왜 pnpm Workspaces(모노레포)를 선택했나
이 프로젝트는 3개의 독립된 결과물이 있다.
- widget: 고객사 사이트에 임베드되는 JS 번들
- admin: 어드민 대시보드 (Vue 3 SPA)
- functions: Firebase Cloud Functions (서버 로직)
멀티레포로 나누면 타입 정의를 세 곳에 각각 유지해야 하고, widget과 functions 사이에 인터페이스가 맞지 않는 문제가 생긴다.
pnpm Workspaces로 하나의 레포에서 관리하면:
packages/shared에 공통 타입 한 번만 정의- widget ↔ functions 인터페이스가 컴파일 타임에 검증됨
- 한 번의
pnpm install로 전체 의존성 설치
멀티테넌트 Firestore 설계
고객사별 데이터 격리가 핵심이다.
Firestore 구조:
/tenants/{siteId}/sessions/{sessionId}/messages/{messageId}
/tenants/{siteId}/settings
/tenants/{siteId}/agents/{agentId}
siteId를 Firestore path의 최상위에 두면:
- Security Rules에서
siteId하나로 모든 접근 제어 가능 - 코드 수정 없이 신규 고객사 추가 (Firestore에 siteId 문서 생성만 하면 됨)
- 고객사 데이터가 물리적으로 격리됨
widget.js 임베드 설계
<!-- 고객사 사이트에 이것만 추가 -->
<script src="https://cdn.congkong.net/widget.js" data-site-id="acme"></script>
widget.js는 Shadow DOM으로 스타일을 격리한다. 고객사 CSS가 챗봇에 영향을 주거나, 챗봇 스타일이 고객사 페이지를 오염시키지 않는다.
// Shadow DOM 격리
const shadow = this.attachShadow({ mode: 'closed' });
const style = document.createElement('style');
style.textContent = WIDGET_CSS; // 번들된 CSS
shadow.appendChild(style);
배운 것
SaaS 설계에서 가장 중요한 결정은 테넌트 격리 방법이다. Firestore path 설계를 처음부터 /tenants/{siteId}/...로 잡으면 나중에 고객사를 추가할 때 코드를 전혀 바꾸지 않아도 된다. 이 결정을 나중에 바꾸려면 전체 데이터 마이그레이션이 필요하다.