📖

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)

📚 Book
title, theme, published, slug
has_many :leaves
🍃 Leaf delegated_type :leafable
book_id, title, position, status, leafable_type, leafable_id
delegated_type :leafable, types: [Page, Section, Picture]
핵심: <strong>STI</strong>(단일 테이블)와 달리, 각 타입이 <strong>자기만의 테이블</strong>을 가짐
Leaf가 공통 인터페이스 역할 (title, position, status)

상태 = 레코드 (Edit / Access / Session)

✏️ Edit
leaf_id
action (revision|trash)
leafable_type, leafable_id
created_at
편집할 때마다 레코드 생성
previous / next로 이력 탐색
🔑 Access
user_id
book_id
level (reader|editor)
존재 = 접근 가능
Pundit 없이 권한 관리
🔐 Session
user_id
token (secure_token)
user_agent, ip_address
last_active_at
세션도 DB 레코드
Devise 없이 세션 관리

인증 흐름 (Campfire와 동일 패턴)

🌐 브라우저 요청
Authentication concern
before_action :require_authentication
authentication.rb ↗
쿠키에 session_token 있음
쿠키 없음
restore_authentication
Session.find_by(token:)
→ session.resume
→ Current.user = session.user
request_authentication
redirect_to new_session_url
핵심: Campfire와 <strong>완전히 동일한 패턴</strong> — 37시그널스 내부에서 재사용되는 인증 구조

Campfire vs Writebook

용도
실시간 채팅
웹 출판
인증
Authentication
동일
다형성
STI (Room)
delegated_type
이력
없음
Edit 레코드
접근 제어
Membership
Access (reader/editor)
실시간
Action Cable 6채널
없음
잡 큐
Solid Queue
Resque + Redis

핵심 포인트

1

GitHub에서 projectcues/writebook 저장소 열기

2

config/routes.rb → 리소스 구조와 네스팅 패턴 확인

3

app/models/leaf.rb → delegated_type 패턴 이해

4

app/models/leafable.rb → 다형성 인터페이스 모듈 분석

5

app/models/edit.rb → 편집 이력 관리 패턴 확인

6

app/models/access.rb → 접근 권한 레코드 패턴 확인

7

app/models/session.rb → DB 기반 세션 관리 확인

8

app/controllers/concerns/authentication.rb → Campfire와 동일 패턴 비교

9

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 라이선스 — 포크/수정 제한

사용 사례

delegated_type: Leaf → Page/Section/Picture (다형성 콘텐츠) Edit 레코드: 편집 이력을 레코드로 추적 (previous/next 탐색) Access 레코드: User-Book 간 권한 관리 (reader/editor) Session 레코드: DB 기반 세션 관리 (user_agent, ip 추적) Publication CRUD: 출판 = Publication 리소스의 update Move CRUD: 페이지 순서 변경 = Move 리소스의 create Authentication concern: Campfire와 동일한 인증 패턴 재사용