Writebook (37signals)
ONCE 브랜드의 출판 도구 — delegated_type과 편집 이력 관리의 교과서
GitHub: projectcues/writebook (ONCE 공식 코드)
Writebook은 37시그널스의 ONCE 브랜드로 출시된 웹 기반 출판 도구입니다. 책을 작성하고, 챕터를 관리하고, 온라인으로 출판할 수 있습니다.
앱 디렉토리 구조
app/
├── controllers/
│ ├── books_controller.rb # 책 CRUD
│ ├── books/
│ │ ├── publications_controller.rb # 출판 = CRUD!
│ │ ├── bookmarks_controller.rb # 북마크
│ │ └── leaves/
│ │ └── moves_controller.rb # 페이지 이동 = CRUD!
│ ├── pages_controller.rb
│ ├── pages/
│ │ └── edits_controller.rb # 편집 이력 조회
│ ├── sections_controller.rb
│ ├── pictures_controller.rb
│ ├── sessions_controller.rb # Devise 없는 인증
│ ├── sessions/
│ │ └── transfers_controller.rb # 세션 이전 = CRUD!
│ ├── users_controller.rb
│ ├── users/
│ │ └── profiles_controller.rb
│ ├── first_runs_controller.rb # 초기 설정
│ ├── accounts/
│ │ ├── join_codes_controller.rb
│ │ └── custom_styles_controller.rb
│ └── concerns/
│ └── authentication.rb # Campfire와 동일 패턴!
├── models/
│ ├── book.rb # 책
│ ├── book/
│ │ ├── accessable.rb # 접근 제어 concern
│ │ └── sluggable.rb # URL slug concern
│ ├── leaf.rb # 중간 테이블 (delegated_type)
│ ├── leaf/
│ │ ├── editable.rb # 편집 이력 concern
│ │ └── positionable.rb # 순서 정렬
│ ├── leafable.rb # delegated_type 인터페이스
│ ├── page.rb # 페이지 (Leafable)
│ ├── section.rb # 섹션 (Leafable)
│ ├── picture.rb # 이미지 (Leafable)
│ ├── access.rb # 접근 권한 = 레코드!
│ ├── edit.rb # 편집 이력 = 레코드!
│ ├── session.rb # 세션 = 레코드!
│ ├── user.rb # has_secure_password
│ ├── user/
│ │ ├── role.rb
│ │ └── transferable.rb
│ └── current.rb # Current 패턴
└── mailers/
└── application_mailer.rb
핵심 패턴 분석
1. delegated_type — 다형성 콘텐츠 관리
Writebook의 가장 주목할 패턴입니다. 책의 콘텐츠(Page, Section, Picture)를 delegated_type으로 관리합니다.
# leaf.rb — 중간 레이어
class Leaf < ApplicationRecord
belongs_to :book, touch: true
delegated_type :leafable, types: %w[Page Section Picture]
enum :status, %w[active trashed].index_by(&:itself)
end
# leafable.rb — 인터페이스 모듈
module Leafable
TYPES = %w[Page Section Picture]
included do
has_one :leaf, as: :leafable
has_one :book, through: :leaf
delegate :title, to: :leaf
end
end
# page.rb, section.rb, picture.rb
class Page < ApplicationRecord
include Leafable
end
STI(단일 테이블 상속)의 한계를 해결하는 Rails 공식 패턴입니다. 각 타입이 자기만의 테이블과 컬럼을 가지면서, Leaf라는 공통 인터페이스로 묶입니다.
2. Edit — 편집 이력을 레코드로 관리
class Edit < ApplicationRecord
belongs_to :leaf
delegated_type :leafable, types: Leafable::TYPES
enum :action, %w[revision trash].index_by(&:itself)
def previous
leaf.edits.before(self).last
end
def next
leaf.edits.after(self).first
end
end
수정할 때마다 Edit 레코드가 생성되어, 전체 편집 이력을 자연스럽게 추적합니다. previous/next로 이력 탐색도 가능합니다.
3. Access — 접근 권한을 레코드로 관리
class Access < ApplicationRecord
enum :level, %w[reader editor].index_by(&:itself)
belongs_to :user
belongs_to :book
end
is_admin: boolean 대신 User와 Book 사이의 Access 레코드로 권한을 관리합니다. reader/editor 레벨로 세분화되어 있습니다.
4. Session — 자체 세션 모델
class Session < ApplicationRecord
has_secure_token
belongs_to :user
def self.start!(user_agent:, ip_address:)
create! user_agent: user_agent, ip_address: ip_address
end
def resume(user_agent:, ip_address:)
if last_active_at.before?(1.hour.ago)
update! user_agent: user_agent, ip_address: ip_address, last_active_at: Time.now
end
end
end
Devise의 current_sign_in_at, last_sign_in_ip 같은 기능을 DB 세션 레코드로 직접 구현합니다.
5. Authentication concern — Campfire와 동일 패턴
Campfire와 완전히 같은 구조의 Authentication concern을 사용합니다:
require_authentication/allow_unauthenticated_access쿠키 기반 세션 복원 (
find_session_by_cookie)Current.user설정
37시그널스의 인증 패턴이 프로젝트 간에 재사용되고 있음을 확인할 수 있습니다.
6. CRUD 매핑 패턴
# 출판 = Publication 리소스의 update
resource :publication, controller: 'books/publications'
# 북마크 = Bookmark 리소스의 show
resource :bookmark, controller: 'books/bookmarks'
# 페이지 이동 = Move 리소스의 create
namespace :leaves do
resources :moves, only: :create
end
Campfire vs Writebook 비교
| Campfire | Writebook | |
|---|---|---|
| 용도 | 실시간 채팅 | 웹 출판 |
| 인증 | Authentication concern | 동일 |
| 다형성 | STI (Room) | delegated_type (Leaf) |
| 이력 관리 | 없음 | Edit 레코드 |
| 접근 제어 | Membership | Access (reader/editor) |
| 실시간 | Action Cable 6채널 | 없음 |
| 잡 큐 | Solid Queue | Resque + Redis |
| 주목 패턴 | CRUD 매핑 | delegated_type |
구조 다이어그램
delegated_type 구조 (ERD)
Leaf가 공통 인터페이스 역할 (title, position, status)
상태 = 레코드 (Edit / Access / Session)
previous / next로 이력 탐색
Devise 없이 세션 관리
인증 흐름 (Campfire와 동일 패턴)
Campfire vs Writebook
핵심 포인트
GitHub에서 projectcues/writebook 저장소 열기
config/routes.rb → 리소스 구조와 네스팅 패턴 확인
app/models/leaf.rb → delegated_type 패턴 이해
app/models/leafable.rb → 다형성 인터페이스 모듈 분석
app/models/edit.rb → 편집 이력 관리 패턴 확인
app/models/access.rb → 접근 권한 레코드 패턴 확인
app/models/session.rb → DB 기반 세션 관리 확인
app/controllers/concerns/authentication.rb → Campfire와 동일 패턴 비교
app/controllers/books/publications_controller.rb → CRUD 매핑 확인
장점
- ✓ delegated_type 패턴의 실전 사용 사례 학습
- ✓ 편집 이력 관리를 레코드로 구현하는 패턴 확인
- ✓ Campfire와 비교하여 37시그널스의 일관된 패턴 파악
- ✓ 접근 제어를 레코드로 구현하는 패턴 (Pundit 없이)
- ✓ DB 기반 세션 관리의 실전 구현
- ✓ slug 기반 URL 라우팅의 우아한 구현
단점
- ✗ Resque+Redis 사용 — Solid Queue가 아닌 구버전 스택
- ✗ Action Cable 없음 — 실시간 기능 학습에는 Campfire가 적합
- ✗ Writebook은 비교적 단순한 앱 (CRUD 중심)
- ✗ 비공개 ONCE 라이선스 — 포크/수정 제한