본문으로 건너뛰기
김신건의 로그

[Rails] Routes: resources, nested, scope, constraints

· 수정 · 📖 약 1분 · 402자/단어 #rails #routing #restful
rails routes, rails routing, resources, resourceful routes, nested routes, rails namespace

정의

Rails 라우팅은 URL을 controller#action으로 매핑. RESTful resources 매크로가 7개 표준 액션을 한 줄로 생성. config/routes.rb에 정의. rails routes로 전체 확인.

resources

# config/routes.rb
Rails.application.routes.draw do
  resources :posts
end

생성된 7개 라우트:

HTTPURLController#Action헬퍼
GET/postsposts#indexposts_path
GET/posts/newposts#newnew_post_path
POST/postsposts#createposts_path
GET/posts/:idposts#showpost_path(p)
GET/posts/:id/editposts#editedit_post_path(p)
PATCH/PUT/posts/:idposts#updatepost_path(p)
DELETE/posts/:idposts#destroypost_path(p)

rails routes -c posts로 확인.

일부만 / 제외

resources :posts, only: [:index, :show]
resources :posts, except: [:destroy]

resource (단수, singleton)

resource :profile    # 단수 (현재 사용자 1개)
HTTPURLAction
GET/profile/newnew
POST/profilecreate
GET/profileshow
GET/profile/editedit
PATCH/profileupdate
DELETE/profiledestroy

index 없음. URL에 :id 없음.

Nested resources

resources :posts do
  resources :comments
end
/posts/:post_id/comments       # comments#index
/posts/:post_id/comments/:id   # comments#show

깊이 1단계 권장. 2단계 이상은 별도 라우트.

shallow

resources :posts do
  resources :comments, shallow: true
end
GET    /posts/:post_id/comments       # index
POST   /posts/:post_id/comments       # create
GET    /posts/:post_id/comments/new   # new
GET    /comments/:id                  # show
GET    /comments/:id/edit             # edit
PATCH  /comments/:id                  # update
DELETE /comments/:id                  # destroy

collection은 nested, member는 평탄. URL 단순 + 부모 컨텍스트 보존.

namespace, scope, module

# namespace: URL prefix + controller module + 라우트 이름 prefix
namespace :admin do
  resources :posts    # /admin/posts → Admin::PostsController, admin_posts_path
end

# scope: URL prefix만
scope "/v1" do
  resources :posts    # /v1/posts → PostsController, posts_path
end

# scope module: controller module만
scope module: :admin do
  resources :posts    # /posts → Admin::PostsController
end

# scope path: URL prefix만
scope "/api" do
  scope module: :api do
    resources :posts
  end
end

API versioning

namespace :api do
  namespace :v1 do
    resources :posts
    resources :users
  end
  namespace :v2 do
    resources :posts
  end
end

/api/v1/posts, Api::V1::PostsController.

constraints

# 정규식
get "/posts/:id", to: "posts#show", constraints: { id: /\d+/ }

# subdomain
constraints subdomain: "api" do
  resources :posts
end

# 메서드 객체
class AdminConstraint
  def matches?(request)
    User.find_by(id: request.session[:user_id])&.admin?
  end
end

constraints AdminConstraint.new do
  mount Sidekiq::Web => "/admin/sidekiq"
end

추가 액션: member, collection

resources :posts do
  member do
    post :publish        # /posts/:id/publish
    delete :archive
  end

  collection do
    get :featured        # /posts/featured
    get :search
  end
end

get on: :member 단축형:

resources :posts do
  get :publish, on: :member
end

기본 라우트

# 루트
root "posts#index"        # GET / → PostsController#index

# 정적
get "/about", to: "pages#about"
get "/contact", to: "pages#contact", as: :contact    # 헬퍼 이름 명시

# match (여러 HTTP 메서드)
match "/search", to: "search#query", via: [:get, :post]

URL 헬퍼

posts_path              # "/posts"
post_path(post)         # "/posts/123"
new_post_path           # "/posts/new"
edit_post_path(post)
post_url(post)          # "http://example.com/posts/123" (호스트 포함)

# 옵션
posts_path(format: :json)        # "/posts.json"
posts_path(page: 2)               # "/posts?page=2"
post_path(post, anchor: "comments")  # "/posts/123#comments"

view, controller, mailer에서 모두 사용.

redirect, mount

# 다른 URL로 리다이렉트
get "/old-blog/:id", to: redirect("/posts/%{id}")
get "/old", to: redirect("https://example.com")

# Rack 앱 마운트
mount Sidekiq::Web => "/sidekiq"
mount Lookbook::Engine, at: "/lookbook" if Rails.env.development?

concern (라우트 공유)

concern :commentable do
  resources :comments
end

resources :posts, concerns: :commentable
resources :photos, concerns: :commentable

DRY.

direct, resolve

특정 URL 생성 규칙.

direct(:homepage) { "https://example.com" }
homepage_url    # "https://example.com"

resolve("Post") { |post| post_path(post) }
# url_for(@post) → /posts/:id

globbing

get "/docs/*path", to: "docs#show"    # /docs/a/b/c → params[:path] = "a/b/c"

format 제한

resources :posts, defaults: { format: :json }
get "/api/posts", to: "api/posts#index", constraints: { format: :json }

동적 segment

get ":controller/:action/:id"    # legacy, 권장 안 함

7-action 외 액션 추가

resources :posts do
  resources :likes, only: [:create, :destroy]
  collection { get :search }
  member { post :duplicate }
end

라우트 디버깅

rails routes                          # 전부
rails routes -c posts                  # 특정 controller
rails routes -g admin                  # 패턴 검색
rails routes --expanded                # 상세

브라우저 /rails/info/routes (dev mode).

API only 라우팅

namespace :api, defaults: { format: :json } do
  namespace :v1 do
    resources :posts, only: [:index, :show, :create, :update, :destroy]
    resources :users, only: [:show]
  end
end

자주 보는 패턴

Catch-all (404)

# 가장 마지막
match "*path", to: "errors#not_found", via: :all

다국어

scope "(:locale)", locale: /en|ko/ do
  resources :posts
end
# /posts /ko/posts /en/posts

인증 분기

authenticate :user, ->(u) { u.admin? } do
  mount Sidekiq::Web => "/sidekiq"
end

devise가 제공하는 헬퍼.

Engine 마운트

mount MyEngine::Engine => "/my"

함정

1. 깊은 nesting

resources :a do
  resources :b do
    resources :c    # /a/:a_id/b/:b_id/c/:id (URL 깊고 헬퍼 복잡)
  end
end

shallow 또는 평탄화.

2. 라우트 순서

get "/posts/featured", to: "posts#featured"
resources :posts    # /posts/:id가 /posts/featured를 가로챌 수도

# 더 구체적 라우트 먼저

3. PATCH vs PUT

기본 update는 PATCH + PUT 둘 다 수락. REST 엄격하면 PATCH만:

resources :posts, only: [:update], via: :patch

4. 헬퍼 이름 변경

resources :posts, as: :articles    # post_path → article_path

URL은 그대로, 헬퍼만.

5. params에 ?[] (Rails 5+ deprecated)

?tags[]=1&tags[]=2

Rails는 자동 배열 변환. 다른 백엔드와 호환성 확인.

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기