Communitygithub.com

rails-patterns

Ruby on Rails 프레임워크 패턴으로, Rails 7.1+ 및 8.x 앱을 대상으로 합니다. 디렉터리 계약, 서비스 객체를 사용하는 슬림 컨트롤러, 폼 객체, 쿼리 객체, 관용적인 ActiveRecord, 백그라운드 작업, ViewComponent, Hotwire 및 Rails 8 Solid 스택을 다룹니다. Rails 앱, 컨트롤러, 모델, 서비스, 작업 또는 뷰를 빌드하거나 검토할 때 사용하세요.

rails-patterns란 무엇인가요?

이 스킬은 현대 Ruby on Rails 애플리케이션(Rails 7.1+ 및 8.x)을 위한 커뮤니티 표준 패턴 모음입니다. 50개 모델 이후에도 유지보수 가능한 앱을 위해 설계되었으며, 디렉터리 구조 계약, 슬림 컨트롤러, 서비스 객체, 폼 객체, 쿼리 객체, 관용적인 ActiveRecord, 백그라운드 작업, ViewComponent, Hotwire(Turbo + Stimulus) 및 Rails 8 Solid 스택(Solid Queue, Solid Cache, Solid Cable)을 다룹니다. 이 패턴들은 Rails 앱을 처음부터 구축하거나, 기존 PR을 검토할 때, 혹은 컨트롤러나 모델이 비대해질 때 활성화하는 것이 좋습니다. 특히 서비스 객체는 단일 책임 원칙을 따르고, 쿼리 객체는 재사용 가능한 범위를 제공하며, ViewComponent는 조건부 렌더링 로직을 명확하게 분리하는 데 유용합니다. 또한 N+1 쿼리 방지, 카운터 캐시, 멱등성 있는 백그라운드 작업 패턴 등 성능과 안정성을 위한 모범 사례를 포함하고 있습니다. 이 스킬은 '어떻게' 패턴을 적용할지에 초점을 맞추며, '무엇을' 그리고 '언제' 사용할지에 대한 의사 결정은 별도의 Ruby 패턴 규칙 파일에서 다룹니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/rails-patterns

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Rails 패턴

현대 Ruby on Rails 애플리케이션(Rails 7.1+ 및 8.x)을 위한 프레임워크 패턴입니다. Rails는 설계 상 의견이 강합니다. 이 패턴들은 50개 모델 이후에도 유지보수 가능한 앱을 위해 커뮤니티가 수렴한 것입니다. 이 스킬은 '방법(how)'에 대한 것입니다. '무엇'과 '언제'(어떤 패턴을 선택할지에 대한 결정)에 대해서는 Ruby 패턴 규칙(rules/ruby/patterns.md, 이 저장소에 설치된 rules/ecc/ruby/patterns.md)을 참조하세요.

활성화 시점

  • Rails 애플리케이션 구축 시 (풀스택, API 전용, 또는 하이브리드)
  • app/ 또는 config/를 건드리는 PR을 검토할 때
  • 모델, 컨트롤러, 서비스 또는 작업을 생성할 때
  • 컨트롤러 액션이 약 10줄을 넘어갈 때
  • 모델 파일이 약 200줄을 넘어갈 때
  • ActiveRecord 쿼리가 컨트롤러나 뷰에 나타나기 시작할 때

핵심 개념

디렉터리 계약

Rails 앱은 예측 가능한 구조를 따릅니다. 디렉터리는 의도적으로 추가해야 하며, 함부로 추가하지 마세요.

app/
  models/         ActiveRecord 모델. 데이터와 가까운 영속성 및 도메인 로직.
  controllers/    HTTP 요청 처리. 얇은 오케스트레이션만.
  views/          ERB 템플릿. 비즈니스 로직 없음.
  components/     ViewComponent 클래스. 테스트가 필요한 뷰 로직.
  services/       서비스 객체. 여러 단계의 비즈니스 연산.
  forms/          폼 객체. 여러 모델에 걸친 복잡한 폼 처리.
  queries/        쿼리 객체. 재사용 가능하고 조합 가능한 ActiveRecord 쿼리.
  jobs/           백그라운드 작업. Solid Queue, Sidekiq 또는 GoodJob을 통한 비동기 작업.
  mailers/        ActionMailer 클래스.
  helpers/        뷰 헬퍼. 아주 작은 표현 로직만.
  policies/       권한 부여 정책(Pundit 사용 시). 선택 사항.
  channels/       WebSocket 작업을 위한 ActionCable 채널.

app/lib/, app/utils/, app/managers/는 피하세요. 어떤 것이 위 디렉터리에 맞지 않는다면, 일반적으로 새로운 디렉터리가 필요하다기보다는 설계를 다시 생각해야 합니다. 진정한 일반 코드는 lib/에 넣으세요.

슬림 컨트롤러

컨트롤러는 요청을 받고, 적절한 객체에 위임하며, 응답을 렌더링합니다. 비즈니스 로직은 다른 곳에 있습니다. (Ruby 패턴 규칙에 따라, 컨트롤러가 여러 책임을 지기 시작하면 서비스 객체로 추출하세요.)

서비스 객체

단일 모델 저장 이상을 다루는 비즈니스 연산의 기본입니다. 일관성을 유지하는 규칙:

  • 접미사(InvoiceCreator) 대신 도메인으로 네임스페이싱합니다(Invoices::Create).
  • 클래스 메서드 .call은 인스턴스 #call에 위임합니다.
  • 불리언이나 단순 레코드 대신 Result 객체를 반환하여 호출자가 성공, 오류 및 영향을 받은 레코드에 따라 분기할 수 있게 합니다.
  • 여러 레코드 쓰기는 트랜잭션으로 감쌉니다.
  • 각 서비스는 단일 목적을 유지합니다(Invoices::Create, Invoices::MarkPaid). 절대 Invoices::Manager는 안 됩니다.

폼 객체

폼이 여러 모델에 걸쳐 있거나 컬럼에 매핑되지 않는 필드가 있을 때는 중첩 속성이나 잘못된 모델의 가상 속성 대신 폼 객체를 사용하세요. 폼 객체는 뷰에 모델처럼 작동하며(form_with model: @form), 레코드를 깔끔하게 구성합니다.

쿼리 객체

컨트롤러나 서비스에서 재사용되거나 스코프로 처리하기에는 너무 복잡한 ActiveRecord 쿼리의 경우, 스코프를 입력으로 받아 조합 가능한 쿼리 객체를 추출하세요. 경험상, 세 개 이상의 체인 조건을 넘거나 매개변수를 받기 시작하는 스코프는 쿼리 객체가 되어야 합니다.

백그라운드 작업

느린 작업은 모두 오프로드하세요. (Ruby 패턴 규칙에 따라, 적당한 처리량의 새로운 Rails 8은 Solid Queue; 성숙한 관찰 가능성, 높은 처리량 또는 기존 Redis가 필요할 때는 Sidekiq.) 어댑터와 관계없이: ID가 아닌 레코드 ID를 전달하고, perform을 멱등성 있게 만들며, retry_on/discard_on을 명시적으로 설정하세요.

부분 템플릿 대신 ViewComponent

조건부 렌더링, 두 개 이상의 인자, 또는 세 곳 이상에서 재사용되는 뷰 로직의 경우 ViewComponent를 선호하세요. 컴포넌트는 격리되어 테스트 가능하고 인터페이스를 명시적으로 드러냅니다. 깊은 조건부 로직이 있는 부분 템플릿은 부채가 됩니다.

Hotwire: Turbo와 Stimulus

기본 Rails 프론트엔드 스택입니다. (Ruby 패턴 규칙에 따라, 서버 렌더링 앱에는 Hotwire를 선호하세요; 상호작용 복잡성이 클라이언트 표면을 정당화할 때만 React/Vue를 사용하세요.) Turbo Frame은 부분 페이지 업데이트, Turbo Stream은 서버 주도 업데이트, Stimulus는 마크업 옆의 작은 클라이언트 측 동작에 사용합니다.

Rails 8 Solid 스택

Rails 8은 이전에 Redis가 필요했던 데이터베이스 기반 기본값을 제공합니다: Solid Queue(작업), Solid Cache(캐시), Solid Cable(ActionCable). 트레이드오프는 더 많은 데이터베이스 부하를 감수하는 대신 하나의 인프라 구성 요소를 줄이는 것입니다. 적당한 처리량에 적합하며, 높은 규모에서는 Redis가 여전히 우수합니다. Kamal은 기본 Docker 기반 배포 도구입니다.

코드 예제

서비스 객체를 사용하는 슬림 컨트롤러

# Bad: 비즈니스 로직이 컨트롤러에 있음
class InvoicesController < ApplicationController
  def create
    @invoice = Invoice.new(invoice_params)
    @invoice.user = current_user
    @invoice.line_items.build(invoice_params[:line_items])
    @invoice.tax_total = TaxCalculator.new(@invoice).calculate
    @invoice.total = @invoice.line_items.sum(&:amount) + @invoice.tax_total

    if @invoice.save
      InvoiceMailer.created(@invoice).deliver_later
      AccountingExportJob.perform_later(@invoice.id)
      redirect_to @invoice, notice: "인보이스가 생성되었습니다"
    else
      render :new
    end
  end
end

# Good: 컨트롤러는 오케스트레이션, 서비스가 작업 수행
class InvoicesController < ApplicationController
  def create
    result = Invoices::Create.call(params: invoice_params, user: current_user)

    if result.success?
      redirect_to result.invoice, notice: "인보이스가 생성되었습니다"
    else
      @invoice = result.invoice
      render :new, status: :unprocessable_entity
    end
  end
end

서비스 객체

# app/services/invoices/create.rb
module Invoices
  class Create
    # Struct는 Rails 7.1이 지원하는 모든 Ruby에서 실행 가능하게 유지합니다.
    # Ruby 3.2+에서는 `Data.define(:success?, :invoice, :errors)`가 더 간결한
    # 불변 대안입니다.
    Result = Struct.new(:success, :invoice, :errors, keyword_init: true) do
      def success?
        success
      end
    end

    def self.call(params:, user:)
      new(params: params, user: user).call
    end

    def initialize(params:, user:)
      @params = params
      @user = user
    end

    def call
      invoice = build_invoice
      ApplicationRecord.transaction do
        invoice.save!
      end
      begin
        send_notifications(invoice)
      rescue StandardError => e
        Rails.logger.error("인보이스 #{invoice.id}에 대한 알림 전송 실패: #{e.message}")
      end
      Result.new(success: true, invoice: invoice, errors: nil)
    rescue ActiveRecord::RecordInvalid => e
      Result.new(success: false, invoice: e.record, errors: e.record.errors)
    end

    private

    attr_reader :params, :user

    def build_invoice
      invoice = user.invoices.new(params.except(:line_items))
      invoice.line_items.build(params[:line_items])
      invoice.tax_total = TaxCalculator.call(invoice)
      invoice.total = invoice.line_items.sum(&:amount) + invoice.tax_total
      invoice
    end

    def send_notifications(invoice)
      InvoiceMailer.created(invoice).deliver_later
      AccountingExportJob.perform_later(invoice.id)
    end
  end
end

폼 객체

# app/forms/signup_form.rb
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :email, :string
  attribute :password, :string
  attribute :company_name, :string
  attribute :terms_accepted, :boolean

  validates :email, presence: true, format: URI::MailTo::EMAIL_REGEXP
  validates :password, presence: true, length: { minimum: 12 }
  validates :company_name, presence: true
  validates :terms_accepted, acceptance: true

  attr_reader :user, :company

  def save
    return false unless valid?

    ApplicationRecord.transaction do
      @company = Company.create!(name: company_name)
      @user = @company.users.create!(email: email, password: password, role: :owner)
    end
    true
  rescue ActiveRecord::RecordInvalid => e
    errors.merge!(e.record.errors)
    false
  end
end

쿼리 객체

# app/queries/invoices/overdue.rb
module Invoices
  class Overdue
    def self.call(scope: Invoice.all, as_of: Time.current)
      new(scope: scope, as_of: as_of).call
    end

    def initialize(scope:, as_of:)
      @scope = scope
      @as_of = as_of
    end

    def call
      scope
        .where(status: :sent)
        .where(due_date: ..as_of)
        .where.not(id: paid_invoice_ids)
        .includes(:customer, :line_items)
    end

    private

    attr_reader :scope, :as_of

    def paid_invoice_ids
      Payment.where(created_at: ..as_of).pluck(:invoice_id)
    end
  end
end

쿼리 객체는 스코프를 받아들이므로 조합 가능합니다: Invoices::Overdue.call(scope: current_user.invoices).

N+1 방지

# Bad: 뷰에서 post.author.name을 호출할 때 N+1 발생
@posts = Post.published

# Good: 미리 로드
@posts = Post.published.includes(:author)

includes는 Rails가 preload와 eager_load 중에서 선택하게 합니다. 연관 관계로 필터링할 때는 별도의 쿼리를 위해 preload를, JOIN을 위해 eager_load를 강제하세요. Rails 6.1부터 strict_loading은 우발적인 지연 로드 시 예외를 발생시킵니다.

카운터 캐시

class Comment < ApplicationRecord
  belongs_to :post, counter_cache: true
end
add_column :posts, :comments_count, :integer, default: 0, null: false

post.comments_count는 COUNT(*) 대신 컬럼 읽기가 됩니다. 이 예제는 새 테이블을 가정합니다. 이미 행이 있는 테이블에 카운터 캐시를 추가하려면 백필이 필요하며, 이 범위를 벗어납니다.

백그라운드 작업 형태

레코드가 아닌 레코드 ID를 전달하세요. 재시도는 전달을 최소 한 번(at-least-once)으로 만들므로, 외부 서비스를 호출하는 모든 작업은 멱등성을 가져야 합니다. 그렇지 않으면 원격 호출 성공 후 일시적인 실패가 다음 시도에서 효과를 중복시킵니다.

class AccountingExportJob < ApplicationJob
  queue_as :exports

  retry_on AccountingApi::TransientError, wait: :polynomially_longer, attempts: 5
  discard_on AccountingApi::PermanentError

  def perform(invoice_id)
    invoice = Invoice.find(invoice_id)
    export = AccountingExport.create_or_find_by!(
      invoice: invoice,
      idempotency_key: "invoice-export-#{invoice.id}-#{invoice.updated_at.to_i}"
    )
    return if export.completed_at?

    receipt = AccountingApi.export(invoice, idempotency_key: export.idempotency_key)
    export.update!(completed_at: Time.current, external_id: receipt.id)
  end
end
add_index :accounting_exports, :idempotency_key, unique: true

고유 인덱스가 이것을 안전하게 만듭니다. 두 시도가 경쟁할 때, 데이터베이스는 두 번째 삽입을 거부하고 Active Record는 호출 내에서 충돌을 해결하여 기존 행을 반환합니다. 이는 작업 수준 재시도 없이 발생합니다. 위의 retry_on은 AccountingApi::TransientError만 다룹니다. 가드는 원격 호출 전의 기간을 다루고, idempotency_key를 API에 전달하는 것은 그 이후의 기간을 다루므로, API 호출과 update! 사이에 충돌이 발생해도 단일 내보내기로 해결됩니다.

ViewComponent

# app/components/invoice_status_badge_component.rb
class InvoiceStatusBadgeComponent < ViewComponent::Base
  STATUS_CLASSES = {
    draft: "bg-gray-100 text-gray-800",
    sent: "bg-blue-100 text-blue-800",
    paid: "bg-green-100 text-green-800",
    overdue: "bg-red-100 text-red-800"
  }.freeze

  def initialize(invoice:)
    @invoice = invoice
  end

  def call
    tag.span(@invoice.status.humanize, class: "rounded-full px-2 py-1 text-sm #{status_class}")
  end

  private

  def status_class
    STATUS_CLASSES.fetch(@invoice.status.to_sym, "bg-gray-100")
  end
end
<%= render InvoiceStatusBadgeComponent.new(invoice: @invoice) %>

Hotwire

<%# Turbo Frame: Edit 클릭 시 이 프레임만 교체됨 %>
<%= turbo_frame_tag "invoice_#{@invoice.id}" do %>
  <div class="invoice">
    <%= link_to "Edit", edit_invoice_path(@invoice) %>
  </div>
<% end %>
<%# Turbo Stream: app/views/comments/create.turbo_stream.erb %>
<%= turbo_stream.append "comments", @comment %>
<%= turbo_stream.update "comment_form", partial: "form", locals: { comment: Comment.new } %>
// app/javascript/controllers/copy_to_clipboard_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["source"]

  copy() {
    navigator.clipboard.writeText(this.sourceTarget.value)
  }
}

허용 가능한 콜백과 허용 불가능한 콜백

# 허용 가능: 순수 데이터 정규화
class User < ApplicationRecord
  before_validation :normalize_email

  private

  def normalize_email
    self.email = email.to_s.downcase.strip
  end
end

# 서비스로 이동 대신: 콜백에 숨겨진 부작용
# class User < ApplicationRecord
#   after_create :send_welcome_email  # 선택 해제가 어렵고 테스트하기 어려움
# end

좋은 concern과 나쁜 concern

# 좋음: 진정으로 교차 관심사이며, 관련 없는 모델에서 재사용 가능
# app/models/concerns/soft_deletable.rb
module SoftDeletable
  extend ActiveSupport::Concern

  included do
    scope :active, -> { where(deleted_at: nil) }
    scope :deleted, -> { where.not(deleted_at: nil) }
  end

  def soft_delete! = update!(deleted_at: Time.current)
  def restore! = update!(deleted_at: nil)
end

# 나쁨: 정확히 하나의 모델이 사용하는 "concern"으로, 그 모델에 속하는 로직을 담고 있음
# app/models/concerns/invoice_calculations.rb
module InvoiceCalculations
  extend ActiveSupport::Concern

  def calculate_total
    line_items.sum(&:amount) + tax_total
  end
end
# 오직 Invoice만 이 모듈을 include합니다. 이는 교차 관심사가 아닙니다. "슬림" 모델의 모양을 위해
# 모듈에 숨겨진 Invoice 자체의 로직일 뿐입니다. Invoice로 다시 옮기세요.

하나의 클래스만 사용하는 concern은 단지 코드를 이동하는 것일 뿐입니다. 해당 클래스에 속합니다. concern은 여러 클래스에서 실제로 재사용되는 교차 관심사 로직을 위해 예약되어야 합니다.

Rails 7.1+ / 8.x 마이그레이션 노트

Rails 8.0

  • Rails.application.config.load_defaults 8.0은 production.rb에서 config.active_job.queue_adapter = :solid_queue 및 config.cache_store = :solid_cache_store를 활성화합니다. 새 앱은 기본적으로 Redis 없이 제공됩니다.
  • config.active_record.raise_on_assign_to_nil은 기본적으로 true이며, nil이 할당될 때 belongs_to 연관 관계에서 ActiveRecord::NotNullViolation을 발생시킵니다.
  • bin/rails app:update를 실행하여 기존 앱을 업데이트하고 새 기본값을 검토하세요.

Rails 7.1

  • config.active_support.cache_format_version = 7.1을 사용하면 Rails 7.0 이전 버전과 호환되지 않는 더 나은 캐시 직렬화가 가능합니다.
  • config.active_record.run_after_transaction_callbacks_in_order_defined = true는 Rails 7.0에서 도입된 after_commit 콜백의 중첩 트랜잭션 동작을 유지합니다.
  • config.active_record.query_log_tags_enabled =true는 ActiveRecord::Base.logger에 대한 쿼리를 SQL 주석과 함께 태그합니다.

관련 리소스

  • Ruby 패턴 규칙 — 이 저장소의 rules/ruby/patterns.md에 설치됨. 여기에는 언제 각 패턴을 적용해야 하는지, 그리고 rails-patterns를 보완하는 Ruby 특정 규칙이 포함되어 있습니다.
  • Rails 가이드 — Rails 공식 가이드.
  • ViewComponent 문서 — 공식 ViewComponent 문서.
  • Hotwire 핸드북 — Turbo와 Stimulus에 대한 공식 문서.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

관련 스킬