🔥

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 매핑 패턴

일반적인 방식 (커스텀 액션)
RoomsController
def index ... end
def show ... end
def create ... end
def close ... end
def reopen ... end
def archive ... end
def ban_user ... end
7+ 액션 = Fat Controller
Campfire 방식 (CRUD 분리)
RoomsController index / show / create
Rooms::ClosedsController create / destroy
Messages::BoostsController create / destroy
Users::BansController create / destroy
각 컨트롤러가 CRUD만 = Skinny Controllers

상태 = 레코드 (ERD)

👤 User has_secure_password
id, email_address, password_digest, name, role
has_many
has_many
has_many
🚫
user_id
administrator_id
created_at
존재 = 차단됨
message_id
user_id
content
존재 = 추천됨
🔗
room_id
user_id
created_at
존재 = 참여 중
핵심: <strong>is_banned: boolean</strong> 대신 <strong>Ban 레코드 존재 여부</strong>로 상태 판단
누가, 언제 변경했는지 자연스럽게 기록됨

Concern 분리 구조

👤 User 모델 concerns/ ↗
include User::Bannable 차단
include User::Mentionable @멘션
include User::Role 역할
include User::Transferable 이전
💬 Message 모델 concerns/ ↗
include Message::Broadcasts Cable
include Message::Pagination 페이지
include Message::Searchable 검색

Action Cable 채널 구조

핵심 포인트

1

GitHub에서 basecamp/once-campfire 저장소 열기

2

config/routes.rb → 리소스 네스팅과 CRUD 매핑 패턴 확인

3

app/controllers/rooms/ → closeds, opens, directs 컨트롤러 분석

4

app/models/user.rb → has_secure_password 인증 확인 (Devise 없음)

5

app/models/user/*.rb → Concern 분리 패턴 학습

6

app/models/ban.rb, boost.rb → 상태를 레코드로 관리하는 패턴 확인

7

app/channels/ → Action Cable 6개 채널 구조 분석

8

app/javascript/controllers/ → Stimulus 컨트롤러 패턴 확인

9

자기 프로젝트에 패턴 하나씩 적용해보기

장점

  • DHH 스타일의 가장 완성된 실전 예시
  • CRUD 매핑 패턴을 실제 코드로 확인 가능
  • Concern 분리의 적절한 규모감 파악
  • Action Cable 실전 구현 6개 채널 참고
  • ONCE 철학의 단순한 아키텍처 체감
  • Stimulus 컨트롤러 ~30개의 실전 패턴

단점

  • 단일 테넌트라 멀티테넌트 앱에 직접 적용 어려움
  • 소규모 채팅 앱이라 대규모 서비스와 다름
  • Minitest + Fixtures — RSpec 사용자에게 낯설 수 있음
  • 네이티브 CSS 사용 — Tailwind 트렌드와 다름
  • ONCE 라이선스로 포크/수정 제한

사용 사례

CRUD 매핑: rooms/closeds_controller.rb — 방 닫기 = create, 열기 = destroy CRUD 매핑: messages/boosts_controller.rb — 부스트 = create/destroy CRUD 매핑: users/bans_controller.rb — 차단 = create/destroy 레코드 상태: Ban, Boost, Membership — 불리언 대신 레코드 Concern 분리: User::Bannable, Message::Broadcasts 등 Current 패턴: 요청 범위 전역 상태 관리 Devise 없는 인증: has_secure_password + Authentication concern