Campfire (37signals)
DHH 스타일의 교과서 — Rails 기본 기능만으로 만든 실시간 채팅 앱
GitHub: basecamp/once-campfire
Campfire는 37시그널스의 ONCE 브랜드로 출시된 설치형 채팅 앱입니다. Rails 기본 기능만으로 실시간 채팅을 구현하여, DHH 코딩 스타일의 가장 완성된 실전 예시입니다.
앱 디렉토리 구조
app/
├── controllers/
│ ├── rooms/
│ │ ├── closeds_controller.rb # 방 닫기 (CRUD 매핑!)
│ │ ├── opens_controller.rb # 방 열기
│ │ ├── directs_controller.rb # 1:1 채팅
│ │ └── involvements_controller.rb
│ ├── messages/
│ │ └── boosts_controller.rb # 메시지 부스트 (CRUD!)
│ ├── users/
│ │ └── bans_controller.rb # 사용자 차단 (레코드로 상태!)
│ ├── sessions_controller.rb # 인증 (Devise 없음!)
│ └── first_runs_controller.rb # 초기 설정
├── models/
│ ├── room.rb
│ ├── rooms/
│ │ ├── closed.rb # STI / 네임스페이스
│ │ ├── direct.rb
│ │ └── open.rb
│ ├── user.rb # has_secure_password
│ ├── user/
│ │ ├── bannable.rb # Concern
│ │ ├── mentionable.rb
│ │ ├── role.rb
│ │ └── transferable.rb
│ ├── message.rb
│ ├── message/
│ │ ├── broadcasts.rb # Action Cable 브로드캐스트
│ │ ├── pagination.rb
│ │ └── searchable.rb
│ ├── ban.rb # 차단 = 레코드!
│ ├── boost.rb # 부스트 = 레코드!
│ ├── membership.rb # 멤버십 = 레코드!
│ └── current.rb # Current 패턴
├── channels/ # Action Cable (Solid Cable)
│ ├── room_channel.rb
│ ├── presence_channel.rb
│ ├── typing_notifications_channel.rb
│ ├── heartbeat_channel.rb
│ ├── read_rooms_channel.rb
│ └── unread_rooms_channel.rb
├── javascript/controllers/ # Stimulus (~30개)
│ ├── composer_controller.js
│ ├── presence_controller.js
│ └── typing_notifications_controller.js
└── jobs/
├── bot/webhook_job.rb
├── remove_banned_content_job.rb
└── room/push_message_job.rb
핵심 패턴 분석
1. CRUD 컨트롤러 — 커스텀 액션 제로
"방 닫기"를 RoomsController#close로 만들지 않고, Rooms::ClosedsController#create로 매핑합니다.
# rooms/closeds_controller.rb
class Rooms::ClosedsController < ApplicationController
def create # 방 닫기 = Closed 레코드 생성
end
def destroy # 방 다시 열기 = Closed 레코드 삭제
end
end
# messages/boosts_controller.rb
class Messages::BoostsController < ApplicationController
def create # 부스트 = Boost 레코드 생성
end
def destroy # 부스트 취소 = Boost 레코드 삭제
end
end
모든 컨트롤러가 표준 CRUD 7개 액션(index/show/new/create/edit/update/destroy)만 사용합니다.
2. 상태 = 레코드 (불리언 컬럼 최소화)
# ban.rb — 차단을 레코드로 관리
class Ban < ApplicationRecord
belongs_to :user # 누가 차단되었는지
belongs_to :administrator, class_name: 'User'
# created_at = 언제 차단되었는지 (자동)
end
# boost.rb — 추천을 레코드로 관리
class Boost < ApplicationRecord
belongs_to :message
belongs_to :user
end
# membership.rb — 방 참여를 레코드로 관리
class Membership < ApplicationRecord
belongs_to :room
belongs_to :user
end
is_banned: boolean 대신 Ban 레코드를 생성하면, 누가/언제 차단했는지 자연스럽게 기록됩니다.
3. Devise 없는 인증
# app/models/user.rb
class User < ApplicationRecord
has_secure_password # Rails 내장
end
# app/controllers/concerns/authentication.rb
module Authentication
extend ActiveSupport::Concern
# 세션 기반 인증 로직
end
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create # 로그인
end
def destroy # 로그아웃
end
end
4. Concern 분리 — 모델 비대화 방지
User 모델:
User::Bannable— 차단 관련 로직User::Mentionable— @멘션 관련User::Role— 역할(admin) 관련User::Transferable— 소유권 이전
Message 모델:
Message::Broadcasts— Action Cable 브로드캐스트Message::Pagination— 페이지네이션Message::Searchable— 검색
5. Current 패턴
class Current < ActiveSupport::CurrentAttributes
attribute :user
# Current.user로 어디서든 현재 사용자 접근
end
6. Action Cable — 6개 전용 채널
| 채널 | 역할 |
|---|---|
RoomChannel |
채팅 메시지 실시간 전송 |
PresenceChannel |
접속 사용자 상태 표시 |
TypingNotificationsChannel |
"입력 중..." 표시 |
HeartbeatChannel |
연결 상태 모니터링 |
ReadRoomsChannel |
읽은 방 표시 |
UnreadRoomsChannel |
안 읽은 방 표시 |
기술 스택 비교
| 카테고리 | Campfire (37signals) | 일반적 Rails 선택 |
|---|---|---|
| 인증 | has_secure_password |
Devise |
| 백그라운드 잡 | Solid Queue | Sidekiq + Redis |
| 캐시 | Solid Cache (DB) | Redis |
| WebSocket | Solid Cable (DB) | Redis adapter |
| 테스트 | Minitest + Fixtures | RSpec + FactoryBot |
| CSS | 네이티브 CSS | Tailwind CSS |
| JS | Stimulus + Turbo | React/Vue |
| 배포 | Kamal + Docker | Heroku/AWS |
ONCE 철학
Campfire는 ONCE 브랜드로 출시되었습니다:
한 번 구매, 영원히 사용 — SaaS 구독 모델 거부
자체 서버 실행 — 데이터 주권 보장
외부 의존성 최소화 — Redis 없이 SQLite + Solid 시리즈
단일 테넌트 — 멀티테넌트 복잡성 제거
이 철학이 코드에 직접 반영됩니다: SQLite 기본, Solid Queue/Cache/Cable, OAuth 없이 이메일/비밀번호, Kamal 단일 서버 배포.
구조 다이어그램
CRUD 매핑 패턴
상태 = 레코드 (ERD)
누가, 언제 변경했는지 자연스럽게 기록됨
Concern 분리 구조
Action Cable 채널 구조
핵심 포인트
GitHub에서 basecamp/once-campfire 저장소 열기
config/routes.rb → 리소스 네스팅과 CRUD 매핑 패턴 확인
app/controllers/rooms/ → closeds, opens, directs 컨트롤러 분석
app/models/user.rb → has_secure_password 인증 확인 (Devise 없음)
app/models/user/*.rb → Concern 분리 패턴 학습
app/models/ban.rb, boost.rb → 상태를 레코드로 관리하는 패턴 확인
app/channels/ → Action Cable 6개 채널 구조 분석
app/javascript/controllers/ → Stimulus 컨트롤러 패턴 확인
자기 프로젝트에 패턴 하나씩 적용해보기
장점
- ✓ DHH 스타일의 가장 완성된 실전 예시
- ✓ CRUD 매핑 패턴을 실제 코드로 확인 가능
- ✓ Concern 분리의 적절한 규모감 파악
- ✓ Action Cable 실전 구현 6개 채널 참고
- ✓ ONCE 철학의 단순한 아키텍처 체감
- ✓ Stimulus 컨트롤러 ~30개의 실전 패턴
단점
- ✗ 단일 테넌트라 멀티테넌트 앱에 직접 적용 어려움
- ✗ 소규모 채팅 앱이라 대규모 서비스와 다름
- ✗ Minitest + Fixtures — RSpec 사용자에게 낯설 수 있음
- ✗ 네이티브 CSS 사용 — Tailwind 트렌드와 다름
- ✗ ONCE 라이선스로 포크/수정 제한