Communitygithub.com

rails-patterns

Ruby on Rails 7.1+ 및 8.x 앱을 위한 프레임워크 패턴 모음입니다. 디렉터리 규칙, 얇은 컨트롤러와 서비스 객체, 폼 객체, 쿼리 객체, 관용적인 ActiveRecord, 백그라운드 잡, ViewComponent, Hotwire, Rails 8 Solid 스택을 다룹니다. Rails 앱, 컨트롤러, 모델, 서비스, 잡, 뷰를 구축하거나 검토할 때 사용하세요.

rails-patterns란 무엇인가요?

이 스킬은 50개 이상의 모델을 가진 Rails 애플리케이션도 유지보수 가능하게 유지하기 위해 커뮤니티에서 합의된 패턴을 제공합니다. 주로 Rails 7.1+와 8.x를 대상으로 하며, 디렉터리 구조, 얇은 컨트롤러, 서비스 객체(Single Responsibility), 폼 객체(Form Object), 쿼리 객체(Query Object), 관용적인 ActiveRecord(N+1 방지, 카운터 캐시, 적절한 콜백), 백그라운드 잡(멱등성, ID 전달), ViewComponent(부분 템플릿 대체), Hotwire(Turbo & Stimulus), Rails 8의 Solid Stack(Solid Queue, Solid Cache, Solid Cable, Kamal 배포) 등을 포괄합니다. 또한 Concern의 적절한 사용과 남용을 구분하고, '무엇'과 '언제' 패턴을 선택해야 하는지는 별도의 Ruby 패턴 규칙 파일에 위임합니다. 이 스킬은 Claude Code, Cursor, Codex 등 AI 에이전트에서 Rails 관련 작업을 수행할 때 자동으로 활성화되어 코드 생성을 안내하고 리뷰를 지원합니다.

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

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

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

문서

Rails 패턴

최신 Ruby on Rails 애플리케이션(Rails 7.1+ 및 8.x)을 위한 프레임워크 패턴입니다. Rails는 설계상 독선적(opinionated)입니다. 여기 제시된 패턴은 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 패턴 규칙에 따라, 컨트롤러가 여러 책임을 가지기 시작하면 서비스 객체로 추출하세요.)

서비스 객체

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

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

폼 객체

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

쿼리 객체

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

백그라운드 잡

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

부분 템플릿보다 ViewComponent

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

Hotwire: Turbo와 Stimulus

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

Rails 8 Solid 스택

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

코드 예제

서비스 객체를 사용한 얇은 컨트롤러

# 나쁨: 컨트롤러에 비즈니스 로직이 있음
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: "Invoice created"
    else
      render :new
    end
  end
end

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

    if result.success?
      redirect_to result.invoice, notice: "Invoice created"
    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("Notification dispatch failed for invoice #{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 방지

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

# 좋음: 미리 로딩 (eager load)
@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(*) 대신 컬럼 읽기가 됩니다. 이 예제는 새 테이블을 가정합니다. 이미 행이 있는 테이블에 카운터 캐시를 추가하려면 백필(backfill)이 필요하며, 이 범위를 벗어납니다.

백그라운드 잡 형태

레코드 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만 다룹니다. 가드는 원격 호출 전의 윈도우를 다루고, API에 idempotency_key를 전달하는 것은 이후의 윈도우를 다루므로, 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)
  }
}

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

# 허용 가능: 순수 데이터 정규화
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 vs 나쁜 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만이 이를 포함합니다. 횡단 관심사가 아닙니다. Invoice 자체의 로직이
# "얇은" 모델인 것처럼 보이기 위해 모듈에 숨겨져 있습니다. Invoice로 다시 옮기세요.

하나의 클래스에서만 사용되는 concern은 단지 코드를 이동하는 것에 불과합니다. 그 클래스에 속해야 합니다. Concern은 관련 없는 클래스들에서 재사용되거나 논란의 여지가 있는 다중 상속을 해결하는 횡단 관심사일 때만 사용하세요.

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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

관련 스킬