rubyonrails.org에서 더 보기:

이 파일을 GITHUB에서 읽지 마세요. 가이드는 https://guides.rubyonrails.org 에서 제공됩니다.

Rails의 레이아웃과 렌더링

이 가이드는 Action Controller와 Action View의 기본적인 레이아웃 기능을 다룹니다.

이 가이드를 읽은 후에는 다음을 알 수 있습니다:

  • Rails에 내장된 다양한 렌더링 메서드를 사용하는 방법
  • 여러 콘텐츠 섹션이 있는 레이아웃을 만드는 방법
  • view를 DRY하게 만들기 위해 partial을 사용하는 방법
  • 중첩된 레이아웃(서브 템플릿)을 사용하는 방법

1 개요: 각 요소들이 어떻게 조화를 이루는가

이 가이드는 Model-View-Controller 삼각형에서 Controller와 View 간의 상호작용에 초점을 맞춥니다. 아시다시피 Controller는 Rails에서 요청을 처리하는 전체 프로세스를 조율하는 책임이 있지만, 일반적으로 복잡한 코드는 Model에 위임합니다. 하지만 사용자에게 응답을 보낼 때가 되면, Controller는 View에 작업을 넘깁니다. 이 가이드는 바로 이 작업 위임에 대한 내용을 다룹니다.

큰 틀에서 보면, 이는 어떤 응답을 보낼지 결정하고 그 응답을 생성하기 위한 적절한 메서드를 호출하는 것을 포함합니다. 만약 응답이 완전한 view라면, Rails는 view를 레이아웃으로 감싸고 필요한 경우 partial view들을 가져오는 추가 작업을 수행합니다. 이 모든 과정을 이 가이드에서 살펴볼 것입니다.

2 응답 생성하기

controller의 관점에서, HTTP 응답을 생성하는 방법에는 세 가지가 있습니다:

  • render를 호출하여 브라우저로 보낼 전체 응답을 생성
  • redirect_to를 호출하여 브라우저로 HTTP 리다이렉트 상태 코드를 전송
  • head를 호출하여 브라우저로 보낼 HTTP 헤더만으로 구성된 응답을 생성

2.1 기본 렌더링: Convention Over Configuration의 실제 예시

Rails가 "convention over configuration"을 지향한다는 말을 들어보셨을 것입니다. 기본 렌더링은 이것의 훌륭한 예시입니다. Rails에서 controller는 기본적으로 유효한 route에 해당하는 이름을 가진 view를 자동으로 렌더링합니다. 예를 들어, BooksController 클래스에 다음과 같은 코드가 있다면:

class BooksController < ApplicationController
end

routes 파일에 다음 내용을 작성하세요:

resources :books

그리고 app/views/books/index.html.erb view 파일이 있습니다:

<h1>도서들이 곧 출시됩니다!</h1>

Rails는 /books로 이동할 때 자동으로 app/views/books/index.html.erb를 렌더링하며 화면에 "Books are coming soon!"이 표시됩니다.

하지만 준비 중 화면은 최소한의 유용성만 있으므로, 곧 Book 모델을 생성하고 BooksController에 index action을 추가할 것입니다:

class BooksController < ApplicationController
  def index
    @books = Book.all
  end
end

index action의 마지막에 명시적인 render가 없는 것은 "convention over configuration" 원칙에 따른 것입니다. 컨트롤러 액션 마지막에 명시적으로 무언가를 렌더링하지 않으면, Rails는 자동으로 컨트롤러의 뷰 경로에서 action_name.html.erb 템플릿을 찾아서 렌더링하는 규칙이 있습니다. 따라서 이 경우 Rails는 app/views/books/index.html.erb 파일을 렌더링할 것입니다.

뷰에서 모든 책들의 속성을 표시하고 싶다면, 다음과 같은 ERB 템플릿을 사용할 수 있습니다:

<h1>도서 목록</h1>

<table>
  <thead>
    <tr>
      <th>제목</th>
      <th>내용</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @books.each do |book| %>
      <tr>
        <td><%= book.title %></td>
        <td><%= book.content %></td>
        <td><%= link_to "보기", book %></td>
        <td><%= link_to "수정", edit_book_path(book) %></td>
        <td><%= link_to "삭제", book, data: { turbo_method: :delete, turbo_confirm: "정말로 삭제하시겠습니까?" } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to "새 도서", new_book_path %>

실제 렌더링은 ActionView::Template::Handlers 모듈의 중첩 클래스들에 의해 수행됩니다. 이 가이드에서는 그 과정을 자세히 다루지는 않지만, view의 파일 확장자가 template handler의 선택을 제어한다는 것을 아는 것이 중요합니다.

2.2 render 사용하기

대부분의 경우, 컨트롤러의 render 메서드는 브라우저가 사용할 애플리케이션의 콘텐츠를 렌더링하는 무거운 작업을 수행합니다. render의 동작을 커스터마이징하는 다양한 방법이 있습니다. Rails 템플릿의 기본 뷰를 렌더링하거나, 특정 템플릿, 파일, 인라인 코드를 렌더링하거나, 아무것도 렌더링하지 않을 수 있습니다. 텍스트, JSON, XML을 렌더링할 수 있습니다. 렌더링된 응답의 content type이나 HTTP 상태도 지정할 수 있습니다.

render의 호출 결과를 브라우저에서 확인할 필요 없이 정확히 보고 싶다면, render_to_string을 호출할 수 있습니다. 이 메서드는 render와 정확히 같은 옵션을 받지만, 브라우저로 응답을 보내는 대신 문자열을 반환합니다.

2.2.1 액션의 뷰 렌더링하기

같은 컨트롤러 내의 다른 템플릿에 해당하는 뷰를 렌더링하고 싶다면, render에 뷰의 이름을 사용할 수 있습니다:

def update
  @book = Book.find(params[:id]) 
  if @book.update(book_params)
    redirect_to(@book)
  else
    render "edit"
  end
end

만약 update 호출이 실패하면, 이 컨트롤러의 update 액션을 호출할 때 동일한 컨트롤러에 속한 edit.html.erb 템플릿을 렌더링할 것입니다.

원한다면 렌더링할 액션을 지정할 때 문자열 대신 심볼을 사용할 수 있습니다:

def update
  @book = Book.find(params[:id]) 
  if @book.update(book_params)
    redirect_to(@book)
  else
    render :edit, status: :unprocessable_entity
  end
end

2.2.2 다른 Controller의 Action Template 렌더링하기

만약 action 코드가 있는 controller와는 완전히 다른 controller의 template을 렌더링하고 싶다면 어떻게 해야 할까요? template을 렌더링할 전체 경로(app/views 기준)를 받는 render를 사용하면 됩니다. 예를 들어, app/controllers/admin에 있는 AdminProductsController에서 코드를 실행할 때, action의 결과를 app/views/products의 template으로 다음과 같이 렌더링할 수 있습니다:

render "products/show"

Rails는 문자열에 포함된 슬래시 문자로 인해 이 view가 다른 controller에 속한다는 것을 알 수 있습니다. 명시적으로 표현하고 싶다면 :template 옵션을 사용할 수 있습니다(Rails 2.2 이전 버전에서는 필수였습니다):

render template: "products/show"

위처럼 template: 옵션을 사용하면, 현재 컨트롤러의 템플릿이 아닌 다른 템플릿을 렌더링할 수 있습니다.

2.2.3 정리하기

위의 두 가지 렌더링 방법(같은 controller의 다른 action의 template을 렌더링하는 것과, 다른 controller의 다른 action의 template을 렌더링하는 것)은 실제로 같은 동작의 변형입니다.

사실, BooksController 클래스에서 book이 성공적으로 업데이트되지 않았을 때 edit template을 렌더링하려는 update action 내에서, 다음의 모든 render 호출들은 모두 views/books 디렉토리의 edit.html.erb template을 렌더링할 것입니다:

render :edit
render action: :edit
render "edit"
render action: "edit"
render "books/edit"
render template: "books/edit"

위 모든 호출은 동일합니다 - 모두 템플릿 path를 찾기 위해 동일한 규칙을 사용합니다. 즉, edit 템플릿을 현재 controller의 뷰 path에서 찾거나, 마지막 두 방법처럼 명시적인 템플릿 참조를 사용합니다.

어떤 것을 사용할지는 스타일과 관례의 문제지만, 경험적 규칙은 작성하는 코드에 가장 잘 맞는 가장 단순한 것을 사용하는 것입니다.

2.2.4 render:inline 사용하기

render 메서드는 :inline 옵션을 사용해 ERB를 메서드 호출의 일부로 제공할 경우 view를 완전히 생략할 수 있습니다. 다음은 완벽하게 유효합니다:

render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"

이 옵션을 사용하는 것은 거의 좋은 이유가 없습니다. 컨트롤러에 ERB를 혼합하는 것은 Rails의 MVC 지향성을 해치며 다른 개발자들이 프로젝트의 로직을 따라가기 어렵게 만듭니다. 대신 별도의 erb view를 사용하세요.

기본적으로 inline rendering은 ERB를 사용합니다. :type 옵션을 사용하여 대신 Builder를 사용하도록 강제할 수 있습니다:

render inline: "xml.p {'끔찍한 코딩 관행!'}", type: :builder

2.2.5 Text 렌더링

render:plain 옵션을 사용하면 마크업이 전혀 없는 순수 텍스트를 브라우저에 보낼 수 있습니다:

render plain: "OK"

텍스트 본문을 "OK"로 렌더링

순수 텍스트를 렌더링하는 것은 적절한 HTML이 아닌 다른 것을 기대하는 Ajax나 웹 서비스 요청에 응답할 때 가장 유용합니다.

기본적으로 :plain 옵션을 사용하면 현재 layout을 사용하지 않고 텍스트가 렌더링됩니다. Rails가 텍스트를 현재 layout에 넣도록 하려면 layout: true 옵션을 추가하고 layout 파일에 .text.erb 확장자를 사용해야 합니다.

2.2.6 HTML 렌더링하기

render:html 옵션을 사용하여 HTML 문자열을 브라우저로 보낼 수 있습니다:

render html: helpers.tag.strong("찾을 수 없음")

HTML 코드의 작은 조각을 렌더링할 때 유용합니다. 하지만 마크업이 복잡한 경우에는 template 파일로 옮기는 것을 고려해보세요.

html: 옵션을 사용할 때, 문자열이 html_safe를 인식하는 API로 구성되지 않은 경우 HTML 엔티티는 이스케이프 처리됩니다.

2.2.7 JSON 렌더링하기

JSON은 많은 Ajax 라이브러리에서 사용되는 JavaScript 데이터 포맷입니다. Rails는 객체를 JSON으로 변환하고 그 JSON을 브라우저로 다시 렌더링하는 내장 지원 기능을 가지고 있습니다:

render json: @product

json: 키워드와 함께 render를 사용하면, 지정한 객체가 JSON 형식으로 변환됩니다.

render할 객체에 대해 to_json을 호출할 필요가 없습니다. :json 옵션을 사용하면 render가 자동으로 to_json을 호출합니다.

2.2.8 XML 렌더링

Rails는 객체를 XML로 변환하고 그 XML을 호출자에게 다시 렌더링하는 기능이 내장되어 있습니다:

render xml: @product

product를 XML 포맷으로 렌더링합니다.

렌더링하고자 하는 객체에 대해 to_xml을 직접 호출할 필요는 없습니다. :xml 옵션을 사용하면 render가 자동으로 to_xml을 호출합니다.

2.2.9 Vanilla JavaScript 렌더링하기

Rails는 vanilla JavaScript를 렌더링할 수 있습니다:

render js: "alert('Hello Rails');"

위 코드는 JavaScript 알림창을 통해 "Hello Rails"를 표시하도록 렌더링합니다.

브라우저에 문자열을 text/javascript MIME 타입으로 전송합니다.

2.2.10 Raw Body 렌더링

:body 옵션을 render에 사용하여 content type을 설정하지 않고 raw content를 브라우저로 전송할 수 있습니다:

render body: "raw"

render body 옵션은 body의 내용을 raw text로 보내줍니다.

이 옵션은 응답의 content type을 신경쓰지 않는 경우에만 사용해야 합니다. 대부분의 경우 :plain 또는 :html을 사용하는 것이 더 적절할 수 있습니다.

별도로 재정의하지 않는 한, 이 render 옵션에서 반환되는 응답은 text/plain이 됩니다. 이는 Action Dispatch 응답의 기본 content type이기 때문입니다.

2.2.11 Rendering Raw File

Rails는 절대 경로의 raw file을 렌더링할 수 있습니다. 이는 에러 페이지와 같은 정적 파일을 조건부로 렌더링할 때 유용합니다.

render file: "#{Rails.root}/public/404.html", layout: false

file: 옵션은 서버의 파일 시스템에 있는 템플릿 경로를 명시합니다. layout: false 옵션은 레이아웃을 사용하지 않도록 지정합니다.

이것은 raw 파일을 렌더링합니다(ERB나 다른 handler들을 지원하지 않습니다). 기본적으로 현재 layout 내에서 렌더링됩니다.

사용자 입력과 함께 :file 옵션을 사용하면 공격자가 파일 시스템의 보안에 민감한 파일에 접근할 수 있으므로 보안 문제가 발생할 수 있습니다.

layout이 필요하지 않은 경우 send_file이 더 빠르고 좋은 옵션이 될 수 있습니다.

2.2.12 객체 렌더링

Rails는 #render_in에 응답하는 객체를 렌더링할 수 있습니다. 객체에 #format을 정의하여 형식을 제어할 수 있습니다.

class Greeting
  def render_in(view_context)  
    view_context.render html: "Hello, World"
  end

  def format
    :html
  end
end

render Greeting.new  
# => "Hello World"

이것은 현재 view context로 제공된 객체에서 render_in을 호출합니다. render:renderable 옵션을 사용하여 객체를 제공할 수도 있습니다:

render renderable: Greeting.new
# => "Hello World"

2.2.13 render의 옵션들

render 메서드 호출은 일반적으로 다음 6가지 옵션을 받습니다:

  • :content_type
  • :layout
  • :location
  • :status
  • :formats
  • :variants
2.2.13.1 :content_type 옵션

기본적으로 Rails는 렌더링 결과를 text/html의 MIME content-type으로 제공합니다(또는 :json 옵션을 사용하면 application/json, :xml 옵션을 사용하면 application/xml). 이를 변경하고 싶을 때가 있을 수 있는데, :content_type 옵션을 설정하여 변경할 수 있습니다:

render template: "feed", content_type: "application/rss"
```를 사용하여 RSS feed를 렌더링합니다.

##### `:layout` 옵션

`render`의 대부분 옵션에서, 렌더링된 콘텐츠는 현재 layout의 일부로 표시됩니다. layout과 그 사용법에 대해서는 이 가이드의 뒷부분에서 자세히 설명합니다.

`:layout` 옵션을 사용하여 Rails에게 현재 action의 layout으로 사용할 특정 파일을 지정할 수 있습니다:

```ruby
render layout: "special_layout"

특정 layout을 지정하여 render

layout 없이 렌더링하도록 Rails에 지시할 수도 있습니다:

layout을 적용하지 않고 렌더링합니다
2.2.13.2 :location 옵션

:location 옵션을 사용하여 HTTP Location 헤더를 설정할 수 있습니다:

render xml: photo, location: photo_url(photo)

photo를 XML로 렌더링하고, location 옵션으로 photo_url(photo)을 전달합니다.

2.2.13.3 :status 옵션

Rails는 자동으로 올바른 HTTP status code로 응답을 생성합니다(대부분의 경우 200 OK입니다). :status 옵션을 사용하여 이를 변경할 수 있습니다:

render status: 500
render status: :forbidden

status를 명시적으로 지정할 수 있습니다. 정수나 기호 모두 가능합니다.

Rails는 숫자로 된 상태 코드와 아래 표시된 해당 symbol을 모두 이해합니다.

응답 클래스 HTTP Status Code Symbol
정보성 100 :continue
101 :switching_protocols
102 :processing
성공 200 :ok
201 :created
202 :accepted
203 :non_authoritative_information
204 :no_content
205 :reset_content
206 :partial_content
207 :multi_status
208 :already_reported
226 :im_used
리다이렉션 300 :multiple_choices
301 :moved_permanently
302 :found
303 :see_other
304 :not_modified
305 :use_proxy
307 :temporary_redirect
308 :permanent_redirect
클라이언트 오류 400 :bad_request
401 :unauthorized
402 :payment_required
403 :forbidden
404 :not_found
405 :method_not_allowed
406 :not_acceptable
407 :proxy_authentication_required
408 :request_timeout
409 :conflict
410 :gone
411 :length_required
412 :precondition_failed
413 :payload_too_large
414 :uri_too_long
415 :unsupported_media_type
416 :range_not_satisfiable
417 :expectation_failed
421 :misdirected_request
422 :unprocessable_entity
423 :locked
424 :failed_dependency
426 :upgrade_required
428 :precondition_required

| | 429 | :too_many_requests | | | 431 | :request_header_fields_too_large | | | 451 | :unavailable_for_legal_reasons | | Server Error | 500 | :internal_server_error | | | 501 | :not_implemented | | | 502 | :bad_gateway | | | 503 | :service_unavailable | | | 504 | :gateway_timeout | | | 505 | :http_version_not_supported | | | 506 | :variant_also_negotiates | | | 507 | :insufficient_storage | | | 508 | :loop_detected | | | 510 | :not_extended | | | 511 | :network_authentication_required |

content가 없는 status code(100-199, 204, 205, 또는 304)와 함께 content를 렌더링하려고 하면, 응답에서 제외될 것입니다.

2.2.13.4 :formats Option

Rails는 요청에 지정된 format(또는 기본값으로 :html)을 사용합니다. symbol이나 array를 포함하는 :formats 옵션을 전달하여 이를 변경할 수 있습니다.

render formats: :xml
render formats: [:json, :xml]

format으로 XML을 렌더링하도록 강제하거나, JSON 또는 XML format으로 렌더링하도록 강제합니다.

지정된 format의 템플릿이 존재하지 않으면 ActionView::MissingTemplate 에러가 발생합니다.

2.2.13.5 :variants 옵션

이것은 Rails에게 동일한 format의 템플릿 변형을 찾도록 지시합니다. symbol이나 배열과 함께 :variants 옵션을 전달하여 variants 목록을 지정할 수 있습니다.

다음은 사용 예시입니다.

# HomeController#index에서 호출됨
render variants: [:mobile, :desktop]

이러한 variants 설정으로 Rails는 다음 템플릿들을 순서대로 찾아 처음 발견된 것을 사용합니다.

  • app/views/home/index.html+mobile.erb
  • app/views/home/index.html+desktop.erb
  • app/views/home/index.html.erb

지정된 포맷의 템플릿이 존재하지 않으면 ActionView::MissingTemplate 에러가 발생합니다.

render 호출에서 variant를 설정하는 대신 컨트롤러 액션에서 request 객체에 variant를 설정할 수도 있습니다.

def index
  request.variant = determine_variant
end

private
  def determine_variant
    variant = nil
    # variant를 결정하는 코드
    variant = :mobile if session[:use_mobile] 

    variant
  end

2.2.14 Layout 찾기

현재 layout을 찾기 위해, Rails는 먼저 app/views/layouts에서 controller와 동일한 base name을 가진 파일을 찾습니다. 예를 들어, PhotosController 클래스의 action을 렌더링할 때는 app/views/layouts/photos.html.erb (또는 app/views/layouts/photos.builder)를 사용합니다. 만약 해당 controller에 특정된 layout이 없다면, Rails는 app/views/layouts/application.html.erb 또는 app/views/layouts/application.builder를 사용합니다. 만약 .erb layout이 없다면, Rails는 .builder layout이 존재하는 경우 이를 사용합니다. Rails는 또한 특정 layout을 개별 controller와 action에 더 정확하게 할당할 수 있는 여러 방법을 제공합니다.

2.2.14.1 Controller를 위한 Layout 지정하기

controller에서 layout 선언을 사용하여 기본 layout 규칙을 재정의할 수 있습니다. 예를 들면:

class ProductsController < ApplicationController
  layout "inventory"
  #...
end

이 선언으로, ProductsController에 의해 렌더링되는 모든 view들은 app/views/layouts/inventory.html.erb를 layout으로 사용하게 됩니다.

전체 애플리케이션에 특정 layout을 지정하려면, ApplicationController 클래스에서 layout 선언을 사용하세요:

class ApplicationController < ActionController::Base
  layout "main"
  #...
end

이 선언을 통해 전체 애플리케이션의 모든 view는 app/views/layouts/main.html.erb를 레이아웃으로 사용하게 됩니다.

2.2.14.2 Runtime에서 레이아웃 선택하기

심볼을 사용하여 요청이 처리될 때까지 레이아웃 선택을 연기할 수 있습니다:

class ProductsController < ApplicationController
  layout :products_layout

  def show
    @product = Product.find(params[:id]) 
  end

  private
    def products_layout
      @current_user.special? ? "special" : "products"
    end 
end

이제 현재 사용자가 특별한 사용자라면, product를 볼 때 특별한 layout을 보게 됩니다.

layout을 결정하기 위해 Proc과 같은 인라인 메서드도 사용할 수 있습니다. 예를 들어, Proc 객체를 전달하면 Proc에 제공하는 블록이 controller 인스턴스를 받게 되므로, 현재 request를 기반으로 layout을 결정할 수 있습니다:

class ProductsController < ApplicationController
  layout Proc.new { |controller| controller.request.xhr? ? "popup" : "application" }
                    # xhr(XMLHttpRequest)인 경우 "popup" layout을,
                    # 그렇지 않은 경우 "application" layout을 사용합니다
end
2.2.14.3 Conditional Layouts

컨트롤러 레벨에서 지정된 레이아웃은 :only:except 옵션을 지원합니다. 이 옵션들은 컨트롤러 내의 메서드 이름에 해당하는 하나의 메서드 이름이나 메서드 이름들의 배열을 사용할 수 있습니다:

class ProductsController < ApplicationController
  layout "product", except: [:index, :rss]
end

ProductsController는 index와 rss action을 제외한 모든 action에 대해 "product" layout을 적용합니다.

이 선언으로 product 레이아웃은 rssindex 메서드를 제외한 모든 곳에서 사용됩니다.

2.2.14.4 Layout 상속

Layout 선언은 계층 구조에서 아래로 전파되며, 더 구체적인 layout 선언이 항상 일반적인 선언을 오버라이드합니다. 예를 들어:

  • application_controller.rb

    class ApplicationController < ActionController::Base
      layout "main"
    end
    
  • articles_controller.rb

    class ArticlesController < ApplicationController
    end
    
  • special_articles_controller.rb

    class SpecialArticlesController < ArticlesController
      layout "special"
    end
    
  • old_articles_controller.rb

    class OldArticlesController < SpecialArticlesController
      layout false
    
      def show
        @article = Article.find(params[:id])
      end
    
      def index
        @old_articles = Article.older
        render layout: "old"
      end
      # ...
    end
    

이 애플리케이션에서:

  • 일반적으로 view는 main layout에서 렌더링됩니다
  • ArticlesController#indexmain layout을 사용합니다
  • SpecialArticlesController#indexspecial layout을 사용합니다
  • OldArticlesController#show는 layout을 전혀 사용하지 않습니다
  • OldArticlesController#indexold layout을 사용합니다
2.2.14.5 Template Inheritance

Layout Inheritance 로직과 유사하게, template이나 partial이 일반적인 경로에서 찾을 수 없는 경우 controller는 상속 체인에서 렌더링할 template이나 partial을 찾습니다. 예를 들어:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
end
# app/controllers/admin_controller.rb
class AdminController < ApplicationController
end
# app/controllers/admin/products_controller.rb
class Admin::ProductsController < AdminController
  def index
  end
end

admin/products#index action에 대한 template 검색 순서는 다음과 같습니다:

  • app/views/admin/products/
  • app/views/admin/
  • app/views/application/

이는 app/views/application/을 공유 partial들을 위한 훌륭한 장소로 만들어주며, 이 partial들은 ERB에서 다음과 같이 렌더링될 수 있습니다:

<%# app/views/admin/products/index.html.erb %>
<%= render @products || "empty_list" %>

<%# app/views/application/_empty_list.html.erb %>
이 목록에는 아직 항목이 <em>없습니다</em>.

2.2.15 Double Render Error 피하기

대부분의 Rails 개발자들은 언젠가는 "Can only render or redirect once per action" 에러 메시지를 보게 될 것입니다. 이는 귀찮은 일이지만 비교적 쉽게 해결할 수 있습니다. 보통 이런 에러는 render가 작동하는 방식에 대한 근본적인 오해로 인해 발생합니다.

예를 들어, 다음의 코드는 이 에러를 발생시킬 것입니다:

def show
  @book = Book.find(params[:id])
  if @book.special?
    render action: "special_show"
  end
  render action: "regular_show" 
end

이 코드는 문제가 있습니다. special? 메서드가 true를 반환하면 special_show template을 렌더링하려고 시도한 다음 즉시 regular_show template도 렌더링하려고 시도할 것입니다. 실제로는 DoubleRenderError가 발생할 것입니다.

@book.special?true로 평가되면, Rails는 @book 변수를 special_show 뷰에 덤프하는 렌더링 프로세스를 시작할 것입니다. 하지만 이것은 show 액션의 나머지 코드의 실행을 중단하지 않을 것이며, Rails가 액션의 끝에 도달하면 regular_show 뷰를 렌더링하기 시작하고 에러를 발생시킬 것입니다. 해결책은 간단합니다: 하나의 코드 경로에서 renderredirect를 한 번만 호출하도록 해야 합니다. return이 도움이 될 수 있습니다. 다음은 수정된 메서드 버전입니다:

def show
  @book = Book.find(params[:id])
  if @book.special?
    render action: "special_show"
    return 
  end
  render action: "regular_show" 
end

ActionController에 의해 수행되는 암시적 render는 render가 호출되었는지 감지하므로, 다음과 같은 코드는 오류 없이 동작합니다:

def show
  @book = Book.find(params[:id]) 
  if @book.special?
    render action: "special_show"
  end 
end

위 코드는 특수 케이스를 적절히 처리하지만, 일반적인 케이스에서는 암시적인 render를 수행합니다. 메서드 마지막에 있는 action view의 이름을 기반으로 show.html.erb를 자동으로 렌더링하게 됩니다.

이것은 special?인 책들은 special_show 템플릿으로 렌더링하고, 다른 책들은 기본 show 템플릿으로 렌더링할 것입니다.

2.3 redirect_to 사용하기

HTTP 요청에 대한 응답을 처리하는 또 다른 방법은 redirect_to를 사용하는 것입니다. 앞서 보았듯이 render는 Rails에게 응답을 구성하는데 어떤 view(또는 다른 asset)를 사용할지 알려줍니다. redirect_to 메서드는 완전히 다른 일을 수행합니다: 브라우저에게 다른 URL에 대한 새로운 요청을 보내도록 지시합니다. 예를 들어, 코드의 어느 위치에서든 애플리케이션의 photos 인덱스로 다음과 같이 리다이렉트할 수 있습니다:

redirect_to photos_url

redirect_back를 사용하여 사용자를 이전 페이지로 되돌릴 수 있습니다. 이 위치는 HTTP_REFERER 헤더에서 가져오는데, 브라우저에서 반드시 설정된다는 보장이 없으므로 이런 경우를 대비해 fallback_location을 반드시 제공해야 합니다.

redirect_back(fallback_location: root_path)

전 페이지로 리다이렉트합니다. fallback_location 파라미터는 HTTP_REFERER 헤더가 없을 때 리다이렉트할 위치를 지정하는데 필수입니다.

redirect_toredirect_back은 메서드 실행을 즉시 중단하고 반환하지 않으며, 단순히 HTTP 응답만 설정합니다. 이들 메서드 이후에 오는 구문들도 계속 실행됩니다. 필요한 경우 명시적인 return 또는 다른 중단 메커니즘을 사용하여 실행을 중단할 수 있습니다.

2.3.1 다른 리다이렉트 상태 코드 사용하기

redirect_to를 호출할 때 Rails는 임시 리다이렉트인 HTTP 상태 코드 302를 사용합니다. 영구 리다이렉트인 301과 같은 다른 상태 코드를 사용하고 싶다면, :status 옵션을 사용할 수 있습니다:

redirect_to photos_path, status: 301

301 status code로 photos_path로 redirect합니다.

:render:status 옵션과 마찬가지로, redirect_to:status도 숫자와 심볼 형태의 헤더 지정을 모두 허용합니다.

2.3.2 renderredirect_to의 차이점

때로는 경험이 부족한 개발자들이 redirect_to를 Rails 코드의 한 위치에서 다른 위치로 실행을 이동시키는 일종의 goto 명령어처럼 생각하기도 합니다. 이는 잘못된 것입니다.

현재 액션은 완료되어 브라우저에 응답을 반환합니다. 이후 코드 실행이 중단되고 새로운 요청을 기다리게 되는데, HTTP 302 상태 코드를 반환함으로써 브라우저에게 다음에 어떤 요청을 해야 하는지 알려준 것뿐입니다.

그 차이를 이해하기 위해 다음 액션들을 살펴보세요:

def index
  @books = Book.all
end

def show
  @book = Book.find_by(id: params[:id]) 
  if @book.nil?
    render action: "index"
  end
end

위 코드는 모든 책 목록을 보여주는 index action과 특정 책의 세부 정보를 보여주는 show action입니다. show action에서 해당 ID의 책을 찾지 못하면 index view로 이동합니다.

이런 형태의 코드에서는 @book 변수가 nil일 경우 문제가 발생할 수 있습니다. render :action은 대상 action의 어떤 코드도 실행하지 않으므로 index 뷰에서 필요로 하는 @books 변수가 설정되지 않는다는 점을 기억하세요. 이를 해결하는 한 가지 방법은 렌더링 대신 redirect를 사용하는 것입니다:

def index
  @books = Book.all
end

def show
  @book = Book.find_by(id: params[:id])
  if @book.nil?
    redirect_to action: :index
  end
end

index 메서드는 모든 Book 레코드를 조회합니다.

show 메서드는 params[:id]에 해당하는 Book 레코드를 찾습니다. 만약 해당 레코드가 없으면(nil인 경우) index 액션으로 리디렉션됩니다.

이 코드에서는, 브라우저가 index 페이지에 대한 새로운 요청을 만들고, index 메서드의 코드가 실행되어 모든 것이 정상적으로 작동할 것입니다.

이 코드의 유일한 단점은 브라우저와의 왕복이 필요하다는 점입니다: 브라우저가 /books/1로 show action을 요청하고 컨트롤러는 books가 없다는 것을 발견하면, 컨트롤러는 브라우저에게 /books/로 이동하라고 지시하는 302 redirect 응답을 보냅니다. 브라우저는 이를 따르고 컨트롤러에 index action을 요청하는 새로운 요청을 다시 보내고, 컨트롤러는 데이터베이스의 모든 books를 가져와 index 템플릿을 렌더링하여 브라우저로 다시 보내면 브라우저가 이를 화면에 표시합니다.

작은 애플리케이션에서는 이러한 추가 지연이 문제가 되지 않을 수 있지만, 응답 시간이 중요한 경우에는 고려해야 할 사항입니다. 다음과 같은 인위적인 예제로 이를 처리하는 한 가지 방법을 보여드리겠습니다:

def index
  @books = Book.all
end

def show
  @book = Book.find_by(id: params[:id]) 
  if @book.nil?
    @books = Book.all
    flash.now[:alert] = "찾으시는 책이 존재하지 않습니다"
    render "index"
  end
end

이는 지정된 ID를 가진 books가 없음을 감지하고, model의 모든 books로 @books instance variable을 채운 다음, index.html.erb template을 직접 렌더링하여 사용자에게 무슨 일이 일어났는지 알려주는 flash alert message와 함께 브라우저에 반환합니다.

2.4 head를 사용하여 헤더만 있는 응답 만들기

head 메서드는 브라우저에 헤더만 있는 응답을 보내는 데 사용할 수 있습니다. head 메서드는 HTTP 상태 코드를 나타내는 숫자나 심볼을 인자로 받습니다 (참조 표 참고). options 인자는 헤더 이름과 값의 hash로 해석됩니다. 예를 들어, 오류 헤더만 반환할 수 있습니다:

head :bad_request

이것은 HTTP status를 400 Bad Request로 응답하지만, body는 비어있습니다.

이는 다음과 같은 header를 생성할 것입니다:

HTTP/1.1 400 잘못된 요청
Connection: close
Date: Sun, 24 Jan 2010 12:15:53 GMT
Transfer-Encoding: chunked 
Content-Type: text/html; charset=utf-8
X-Runtime: 0.013483
Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
Cache-Control: no-cache

또는 다른 정보를 전달하기 위해 다른 HTTP header를 사용할 수 있습니다.

head :created, location: photo_path(@photo)

Response의 body 없이 헤더만 반환합니다. location 헤더를 photo_path로 설정하고 HTTP 상태를 created로 설정하는 것입니다.

위는 다음과 같은 결과를 생성할 것입니다:

HTTP/1.1 201 Created
Connection: close
Date: Sun, 24 Jan 2010 12:16:44 GMT 
Transfer-Encoding: chunked
Location: /photos/1
Content-Type: text/html; charset=utf-8
X-Runtime: 0.083496
Set-Cookie: _blog_session=...생략...; path=/; HttpOnly
Cache-Control: no-cache

3 레이아웃 구조화하기

Rails가 응답으로 view를 렌더링할 때, 이 가이드의 앞부분에서 다룬 현재 레이아웃을 찾는 규칙을 사용하여 view를 현재 레이아웃과 결합합니다. 레이아웃 내에서 전체 응답을 형성하기 위해 서로 다른 출력들을 결합하는 세 가지 도구를 사용할 수 있습니다:

3.1 Asset Tag Helpers

Asset tag helper는 view를 feeds, JavaScript, stylesheets, images, videos, audios에 연결하는 HTML을 생성하는 메소드를 제공합니다. Rails에는 여섯 가지 asset tag helper가 있습니다:

이러한 태그들은 layout이나 다른 view에서 사용할 수 있지만, auto_discovery_link_tag, javascript_include_tag, stylesheet_link_tag는 주로 layout의 <head> 섹션에서 사용됩니다.

asset tag helper는 지정된 위치에 asset이 존재하는지 확인하지 않습니다. 단순히 사용자가 알고 있다고 가정하고 링크를 생성합니다.

auto_discovery_link_tag helper는 대부분의 브라우저와 feed reader가 RSS, Atom 또는 JSON feed의 존재를 감지하는데 사용할 수 있는 HTML을 생성합니다. 이는 링크의 타입(:rss, :atom, 또는 :json), url_for에 전달되는 옵션의 해시, 그리고 태그의 옵션 해시를 인자로 받습니다.

<%= auto_discovery_link_tag(:rss, {action: "feed"},
  {title: "RSS 피드"}) %>

auto_discovery_link_tag에 사용할 수 있는 세 가지 태그 옵션이 있습니다:

  • :rel은 링크에서 rel 값을 지정합니다. 기본값은 "alternate"입니다.
  • :type은 명시적 MIME type을 지정합니다. Rails는 자동으로 적절한 MIME type을 생성합니다.
  • :title은 링크의 제목을 지정합니다. 기본값은 대문자로 된 :type 값입니다(예: "ATOM" 또는 "RSS").

3.1.2 javascript_include_tag로 JavaScript 파일 연결하기

javascript_include_tag 헬퍼는 제공된 각 소스에 대해 HTML script 태그를 반환합니다.

Asset Pipeline이 활성화된 Rails를 사용하는 경우, 이 헬퍼는 이전 Rails 버전에서 사용되던 public/javascripts 대신 /assets/javascripts/에 대한 링크를 생성합니다. 이 링크는 asset pipeline에 의해 제공됩니다.

Rails 애플리케이션이나 Rails 엔진 내의 JavaScript 파일은 app/assets, lib/assets 또는 vendor/assets 세 위치 중 하나에 위치합니다. 이러한 위치들은 Asset Pipeline 가이드의 Asset Organization 섹션에서 자세히 설명됩니다.

원하는 경우 문서 루트를 기준으로 한 전체 경로나 URL을 지정할 수 있습니다. 예를 들어, app/assets/javascripts, lib/assets/javascripts 또는 vendor/assets/javascripts 중 하나에 있는 main.js JavaScript 파일에 링크하려면 다음과 같이 합니다:

<%= javascript_include_tag "main" %>

Rails는 다음과 같은 script 태그를 출력할 것입니다:

<script src='/assets/main.js'></script>

이 asset에 대한 요청은 Sprockets gem에 의해 처리됩니다.

app/assets/javascripts/main.jsapp/assets/javascripts/columns.js와 같은 여러 파일을 동시에 포함하려면:

<%= javascript_include_tag "main", "columns" %>

app/assets/javascripts/main.jsapp/assets/javascripts/photos/columns.js를 포함하려면:

<%= javascript_include_tag "main", "/photos/columns" %>

http://example.com/main.js를 포함하려면:

<%= javascript_include_tag "http://example.com/main.js" %>

stylesheet_link_tag 헬퍼는 제공된 각 소스에 대해 HTML <link> 태그를 반환합니다.

"Asset Pipeline"이 활성화된 Rails를 사용하는 경우, 이 헬퍼는 /assets/stylesheets/로 연결되는 링크를 생성합니다. 이 링크는 Sprockets gem에 의해 처리됩니다. 스타일시트 파일은 app/assets, lib/assets, 또는 vendor/assets 중 하나의 위치에 저장될 수 있습니다.

문서 루트를 기준으로 한 전체 경로나 URL을 지정할 수 있습니다. 예를 들어, app/assets, lib/assets, 또는 vendor/assets 중 하나의 내부에 있는 stylesheets 디렉토리 안에 있는 스타일시트 파일에 링크하려면 다음과 같이 합니다:

<%= stylesheet_link_tag "main" %>

app/assets/stylesheets/main.cssapp/assets/stylesheets/columns.css를 포함하려면:

<%= stylesheet_link_tag "main", "columns" %>

app/assets/stylesheets/main.cssapp/assets/stylesheets/photos/columns.css를 포함하려면:

<%= stylesheet_link_tag "main", "photos/columns" %>

http://example.com/main.css를 포함하려면:

<%= stylesheet_link_tag "http://example.com/main.css" %>

기본적으로 stylesheet_link_tagrel="stylesheet"와 함께 link를 생성합니다. 이 기본값은 적절한 옵션(:rel)을 지정하여 재정의할 수 있습니다.

<%= stylesheet_link_tag "main_print", media: "print" %>

3.1.4 image_tag로 이미지 링크하기

image_tag 헬퍼는 지정된 파일에 대한 HTML <img /> 태그를 생성합니다. 기본적으로 파일들은 public/images에서 로드됩니다.

이미지의 확장자를 반드시 지정해야 한다는 점에 주의하세요.

<%= image_tag "header.png" %>

원하는 경우 이미지 path를 제공할 수 있습니다:

<%= image_tag "icons/delete.gif" %>

추가적인 HTML 옵션들의 hash를 제공할 수 있습니다:

<%= image_tag "icons/delete.gif", {height: 45} %>

사용자가 브라우저에서 이미지를 비활성화한 경우 사용될 이미지의 대체 텍스트를 제공할 수 있습니다. alt 텍스트를 명시적으로 지정하지 않으면 파일명에서 확장자를 제외하고 대문자로 변환한 것이 기본값이 됩니다. 예를 들어 다음의 두 이미지 태그는 동일한 코드를 반환합니다:

<%= image_tag "home.gif" %>
<%= image_tag "home.gif", alt: "홈" %>

포맷이 "{width}x{height}"인 특별한 size 태그도 지정할 수 있습니다:

<%= image_tag "home.gif", size: "50x20" %>

위의 특수 태그들 외에도 :class, :id, :name과 같은 표준 HTML 옵션들의 hash를 마지막에 전달할 수 있습니다:

<%= image_tag "home.gif", alt: "홈으로 가기",
                          id: "HomeImage", 
                          class: "nav_bar" %>

3.1.5 video_tag를 사용하여 동영상 링크하기

video_tag 헬퍼는 지정된 파일에 대한 HTML5 <video> 태그를 생성합니다. 기본적으로 파일들은 public/videos에서 로드됩니다.

<%= video_tag "movie.ogg" %>

Produces는 Accepts와 반대입니다. 주어진 format이나 MIME type의 response를 생성하도록 route를 제한합니다.

get "/articles", to: "articles#index", as: :articles
defaults format: :json
end

위의 route에서 GET /articles는 format: 'json'으로 response content type이 application/json인 경우에만 매칭됩니다.

다음과 같이 더 많은 format을 다루는 방법도 있습니다:

constraints format: /json|xml/ do
  get "/articles", to: "articles#index"
end 

route를 format이나 MIME type으로 제한하려면 constraintsformat: 또는 ":format" => 키를 사용할 수 있습니다.

예를 들어:

get "articles/:id", to: "articles#show", constraints: { format: 'json' }
get "articles/:id", to: "articles#show", constraints: { format: /json|xml/ }
<video src="/videos/movie.ogg" />

image_tag처럼 절대 경로나 public/videos 디렉토리에 대한 상대 경로를 제공할 수 있습니다. 추가로 image_tag와 마찬가지로 size: "#{width}x#{height}" 옵션을 지정할 수 있습니다. Video tag도 마지막에 HTML 옵션(id, class 등)을 지정할 수 있습니다.

video tag는 HTML 옵션 해시를 통해 다음을 포함한 모든 <video> HTML 옵션을 지원합니다:

  • poster: "image_name.png", 비디오가 재생되기 전에 표시할 이미지를 제공합니다.
  • autoplay: true, 페이지 로드시 비디오 재생을 시작합니다.
  • loop: true, 비디오가 끝나면 반복 재생합니다.
  • controls: true, 사용자가 비디오를 제어할 수 있는 브라우저 제공 컨트롤을 제공합니다.
  • autobuffer: true, 페이지 로드시 사용자를 위해 파일을 미리 로드합니다.

video_tag에 비디오 배열을 전달하여 여러 비디오를 재생하도록 지정할 수도 있습니다:

<%= video_tag ["trailer.ogg", "movie.ogg"] %>

다음과 같이 출력될 것입니다:

<video>
  <source src="/videos/trailer.ogg">
  <source src="/videos/movie.ogg">
</video>

3.1.6 audio_tag를 사용하여 오디오 파일에 링크하기

audio_tag 헬퍼는 지정된 파일에 대한 HTML5 <audio> 태그를 생성합니다. 기본적으로 파일들은 public/audios에서 로드됩니다.

<%= audio_tag "music.mp3" %>

원하는 경우 audio 파일의 경로를 제공할 수 있습니다:

<%= audio_tag "music/first_song.mp3" %>

audio_tag helper를 사용하여 오디오 파일을 출력합니다.

ID, class 등과 같은 추가 옵션을 hash로 전달할 수도 있습니다.

video_tag와 마찬가지로 audio_tag도 특별한 옵션들이 있습니다:

  • autoplay: true, 페이지 로드시 오디오 재생을 시작합니다
  • controls: true, 사용자가 오디오와 상호작용할 수 있는 브라우저 기본 컨트롤을 제공합니다
  • autobuffer: true, 페이지 로드시 사용자를 위해 파일을 미리 로드합니다

3.2 yield 이해하기

레이아웃의 맥락에서 yield는 view의 내용이 삽입되어야 하는 섹션을 식별합니다. 이를 사용하는 가장 간단한 방법은 단일 yield를 사용하는 것이며, 현재 렌더링되는 view의 모든 내용이 여기에 삽입됩니다:

<html>
  <head>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

여러 개의 yielding region을 가지는 layout을 생성할 수도 있습니다:

<html>
  <head>
    <%= yield :head %> <!-- head 섹션에 사용할 컨텐츠 yield -->
  </head>
  <body>
    <%= yield %> <!-- body 섹션에 사용할 메인 컨텐츠 yield -->
  </body>
</html>

뷰의 메인 본문은 항상 이름 없는 yield로 렌더링됩니다. 이름이 있는 yield에 콘텐츠를 렌더링하려면, 이름이 있는 yield와 동일한 인자를 사용하여 content_for 메서드를 호출하세요.

새로 생성된 애플리케이션은 app/views/layouts/application.html.erb 템플릿의 <head> 요소 내에 <%= yield :head %>를 포함하고 있습니다.

3.3 content_for 메서드 사용하기

content_for 메서드를 사용하면 레이아웃의 이름이 있는 yield 블록에 컨텐츠를 삽입할 수 있습니다. 예를 들어, 다음 뷰는 방금 본 레이아웃과 함께 동작할 수 있습니다:

<% content_for :head do %>
  <title>간단한 페이지</title>
<% end %>

<p>Hello, Rails!</p>

이 페이지를 제공된 layout에 렌더링한 결과는 다음과 같은 HTML이 됩니다:

<html>
  <head>
    <title>간단한 페이지</title>
  </head>
  <body>
    <p>안녕하세요, Rails!</p>
  </body>
</html>

content_for 메소드는 레이아웃이 사이드바나 푸터와 같이 각각의 콘텐츠 블록이 삽입되어야 하는 구분된 영역을 포함할 때 매우 유용합니다. 또한 페이지별 JavaScript <script> 요소, CSS <link> 요소, 컨텍스트별 <meta> 요소 또는 기타 요소들을 일반적인 레이아웃의 <head>에 삽입할 때도 유용합니다.

3.4 Partial 사용하기

Partial 템플릿 - 보통 "partial"이라고 불리는 - 은 렌더링 프로세스를 더 관리하기 쉬운 조각으로 나누는 또 다른 도구입니다. Partial을 사용하면 응답의 특정 부분을 렌더링하는 코드를 자체 파일로 이동할 수 있습니다.

3.4.1 Partial 명명하기

뷰의 일부로 partial을 렌더링하려면 뷰 내에서 render 메서드를 사용합니다:

<%= render "menu" %>

이는 렌더링되는 view 내의 해당 지점에서 _menu.html.erb라는 이름의 파일을 렌더링할 것입니다. 맨 앞의 밑줄 문자에 주목하세요: partial은 일반 view와 구분하기 위해 앞에 밑줄을 붙여서 이름을 짓습니다. 하지만 실제로 참조할 때는 밑줄 없이 사용합니다. 이는 다른 폴더에서 partial을 가져올 때도 마찬가지입니다:

<%= render "application/menu" %>

view partial은 템플릿과 레이아웃과 동일한 템플릿 상속에 의존하기 때문에, 코드는 app/views/application/_menu.html.erb에서 partial을 가져올 것입니다.

3.4.2 View를 단순화하기 위해 Partial 사용하기

partial을 사용하는 한 가지 방법은 서브루틴처럼 다루는 것입니다: 무슨 일이 일어나는지 더 쉽게 파악할 수 있도록 view에서 세부 사항을 빼내는 방법입니다. 예를 들어, 다음과 같은 view가 있을 수 있습니다:

<%= render "application/ad_banner" %>

<h1>제품</h1>

<p>다음은 우리의 주요 제품들입니다:</p>
<%# ... %>

<%= render "application/footer" %>

여기서 _ad_banner.html.erb_footer.html.erb partial들은 애플리케이션의 많은 페이지에서 공유되는 콘텐츠를 포함할 수 있습니다. 특정 페이지에 집중할 때 이러한 섹션의 세부 사항을 볼 필요는 없습니다.

이 가이드의 이전 섹션에서 보았듯이, yield는 layout을 정리하는 데 매우 강력한 도구입니다. 이는 순수 Ruby이므로 거의 모든 곳에서 사용할 수 있다는 점을 기억하세요. 예를 들어, 여러 유사한 리소스에 대한 form layout 정의를 DRY하게 만드는 데 사용할 수 있습니다:

  • users/index.html.erb

    <%= render "application/search_filters", search: @q do |form| %>
      <p>
        Name contains: <%= form.text_field :name_contains %>
      </p>
    <% end %>
    
  • roles/index.html.erb

    <%= render "application/search_filters", search: @q do |form| %>
      <p>
        Title contains: <%= form.text_field :title_contains %>
      </p>
    <% end %>
    
  • application/_search_filters.html.erb

    <%= form_with model: search do |form| %>
      <h1>Search form:</h1>
      <fieldset>
        <%= yield form %>
      </fieldset>
      <p>
        <%= form.submit "Search" %>
      </p>
    <% end %>
    

애플리케이션의 모든 페이지에서 공유되는 콘텐츠의 경우, layout에서 직접 partial을 사용할 수 있습니다.

3.4.3 Partial Layouts

partial은 view가 layout을 사용할 수 있는 것처럼 자체 layout 파일을 사용할 수 있습니다. 예를 들어, 다음과 같이 partial을 호출할 수 있습니다:

<%= render partial: "link_area", layout: "graybar" %>

_link_area.html.erb라는 이름의 partial을 찾아 _graybar.html.erb 레이아웃을 사용해 렌더링합니다. partial용 레이아웃은 일반 partial과 마찬가지로 앞에 밑줄이 붙는 명명 규칙을 따르며, 메인 layouts 폴더가 아닌 해당 partial이 속한 폴더와 같은 곳에 위치합니다.

또한 :layout과 같은 추가 옵션을 전달할 때는 :partial을 명시적으로 지정해야 합니다.

3.4.4 Local 변수 전달하기

partial에 local 변수를 전달할 수도 있어서 더욱 강력하고 유연하게 사용할 수 있습니다. 예를 들어, 이 기법을 사용하여 new와 edit 페이지 간의 중복을 줄이면서도 일부 고유한 내용을 유지할 수 있습니다:

  • new.html.erb

    <h1>New zone</h1>
    <%= render partial: "form", locals: {zone: @zone} %>
    
  • edit.html.erb

    <h1>Editing zone</h1>
    <%= render partial: "form", locals: {zone: @zone} %>
    
  • _form.html.erb

    <%= form_with model: zone do |form| %>
      <p>
        <b>Zone name</b><br>
        <%= form.text_field :name %>
      </p>
      <p>
        <%= form.submit %>
      </p>
    <% end %>
    

두 view에서 동일한 partial이 렌더링되지만, Action View의 submit helper는 new 액션에서는 "Create Zone"을, edit 액션에서는 "Update Zone"을 반환합니다.

특정 경우에만 partial에 local 변수를 전달하려면 local_assigns를 사용하세요.

  • index.html.erb

    <%= render user.articles %>
    
  • show.html.erb

<%= render article, full: true %> ```

  • _article.html.erb
<h2><%= article.title %></h2>

<% if local_assigns[:full] %>
  <%= simple_format article.body %>
<% else %>
  <%= truncate article.body %>
<% end %>

이렇게 하면 모든 local 변수를 선언하지 않고도 partial을 사용할 수 있습니다.

모든 partial은 partial과 동일한 이름을 가진 local 변수(맨 앞의 underscore 제외)를 가지고 있습니다. :object 옵션을 통해 이 local 변수에 object를 전달할 수 있습니다:

<%= render partial: "customer", object: @new_customer %>

customer partial 내에서 customer 변수는 부모 뷰의 @new_customer를 참조합니다.

만약 partial에 렌더링할 모델의 인스턴스가 있다면 다음과 같은 축약 문법을 사용할 수 있습니다:

<%= render @customer %>

@customer instance 변수가 Customer 모델의 인스턴스를 포함한다고 가정하면, 이는 _customer.html.erb를 사용하여 렌더링하고 부모 뷰의 @customer instance 변수를 참조할 local 변수 customer를 partial로 전달합니다.

3.4.5 Collection 렌더링하기

Partial은 collection을 렌더링할 때 매우 유용합니다. :collection 옵션을 통해 collection을 partial로 전달하면, partial은 collection의 각 멤버마다 한 번씩 삽입됩니다:

  • index.html.erb

    <h1>Products</h1>
    <%= render partial: "product", collection: @products %>
    
  • _product.html.erb

    <p>Product Name: <%= product.name %></p>
    

Partial이 복수형 collection과 함께 호출될 때, partial의 개별 인스턴스들은 partial의 이름을 따서 명명된 변수를 통해 렌더링되는 collection의 멤버에 접근할 수 있습니다. 이 경우 partial은 _product이며, _product partial 내에서 렌더링되는 인스턴스를 얻기 위해 product를 참조할 수 있습니다.

이에 대한 축약형도 있습니다. @productsProduct 인스턴스의 collection이라고 가정하면, 동일한 결과를 얻기 위해 index.html.erb에서 다음과 같이 간단히 작성할 수 있습니다:

<h1>Products</h1>
<%= render @products %>

Rails는 컬렉션의 모델 이름을 보고 사용할 partial의 이름을 결정합니다. 실제로 이종 컬렉션을 만들어서 이런 방식으로 렌더링할 수도 있으며, Rails는 컬렉션의 각 멤버에 대해 적절한 partial을 선택합니다:

  • index.html.erb

    <h1>Contacts</h1>
    <%= render [customer1, employee1, customer2, employee2] %>
    
  • customers/_customer.html.erb

    <p>Customer: <%= customer.name %></p>
    
  • employees/_employee.html.erb

    <p>Employee: <%= employee.name %></p>
    

이 경우 Rails는 컬렉션의 각 멤버에 대해 적절하게 customer 또는 employee partial을 사용합니다.

컬렉션이 비어있는 경우, render는 nil을 반환하므로 대체 콘텐츠를 제공하는 것이 매우 간단합니다.

<h1>Products</h1>
<%= render(@products) || "사용 가능한 product가 없습니다." %>

3.4.6 로컬 변수

partial 내에서 커스텀 로컬 변수 이름을 사용하려면, partial을 호출할 때 :as 옵션을 지정하세요:

<%= render partial: "product", collection: @products, as: :item %>

위 코드는 @products collection을 product partial로 렌더링하고, 개별 product 객체를 :item이라는 로컬 변수로 전달합니다.

이 변경 사항으로, partial 내에서 item 로컬 변수를 통해 @products 컬렉션의 인스턴스에 접근할 수 있습니다.

또한 locals: {} 옵션을 사용하여 렌더링하는 partial에 임의의 로컬 변수를 전달할 수 있습니다:

<%= render partial: "product", collection: @products,
           as: :item, locals: {title: "상품 페이지"} %>

이 경우, partial은 "Products Page"라는 값을 가진 로컬 변수 title에 접근할 수 있습니다.

3.4.7 Counter Variables

Rails는 또한 collection에 의해 호출된 partial 내에서 counter 변수를 사용할 수 있게 합니다. 이 변수는 partial의 제목 뒤에 _counter를 붙여서 이름이 지정됩니다. 예를 들어, @products collection을 렌더링할 때 _product.html.erb partial은 product_counter 변수에 접근할 수 있습니다. 이 변수는 첫 번째 렌더링에서 0값으로 시작하여 해당 view 내에서 partial이 렌더링된 횟수를 인덱싱합니다.

# index.html.erb
<%= render partial: "product", collection: @products %>
# _product.html.erb
<%= product_counter %> # 첫 번째 product는 0, 두 번째 product는 1...

이것은 as: 옵션을 사용하여 지역 변수 이름이 변경된 경우에도 작동합니다. 따라서 as: :item을 사용한 경우, counter 변수는 item_counter가 됩니다.

3.4.8 Spacer Templates

:spacer_template 옵션을 사용하여 주요 partial 인스턴스 사이에 렌더링될 두 번째 partial을 지정할 수도 있습니다:

<%= render partial: @products, spacer_template: "product_ruler" %>

이 템플릿은 각각의 partial 렌더링 사이에 지정된 spacer 템플릿을 삽입할 것입니다.

Rails는 각 _product partial 쌍 사이에 _product_ruler partial을 렌더링합니다(어떤 데이터도 전달되지 않음).

3.4.9 Collection Partial 레이아웃

collection을 렌더링할 때 :layout 옵션을 사용할 수도 있습니다:

<%= render partial: "product", collection: @products, layout: "special_layout" %>

collection을 사용할 때도 layout을 지정할 수 있습니다.

collection의 각 아이템에 대한 partial과 함께 layout이 렌더링될 것입니다. current object와 object_counter 변수는 partial 내에서와 동일한 방식으로 layout 내에서도 사용할 수 있습니다.

3.5 Nested Layouts 사용하기

애플리케이션에서 특정 컨트롤러를 지원하기 위해 일반적인 애플리케이션 레이아웃과 약간 다른 레이아웃이 필요할 수 있습니다. 메인 레이아웃을 반복하고 수정하는 대신, nested layouts(서브 템플릿이라고도 함)을 사용하여 이를 구현할 수 있습니다. 다음은 예시입니다:

다음과 같은 ApplicationController 레이아웃이 있다고 가정해봅시다:

  • app/views/layouts/application.html.erb

    <html>
    <head>
      <title><%= @page_title or "Page Title" %></title>
      <%= stylesheet_link_tag "layout" %>
      <%= yield :head %>
    </head>
    <body>
      <div id="top_menu">Top menu items here</div>
      <div id="menu">Menu items here</div>
      <div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div>
    </body>
    </html>
    

NewsController에 의해 생성된 페이지에서 상단 메뉴를 숨기고 우측 메뉴를 추가하고 싶다면:

  • app/views/layouts/news.html.erb

    <% content_for :head do %>
      <style>
        #top_menu {display: none}
        #right_menu {float: right; background-color: yellow; color: black}
      </style>
    <% end %>
    <% content_for :content do %>
      <div id="right_menu">Right menu items here</div>
      <%= content_for?(:news_content) ? yield(:news_content) : yield %>
    <% end %>
    <%= render template: "layouts/application" %>
    

이게 전부입니다. News 뷰는 새로운 레이아웃을 사용하여 상단 메뉴를 숨기고 "content" div 내부에 새로운 우측 메뉴를 추가할 것입니다.

이 기술을 사용하여 다양한 서브 템플릿 구성으로 비슷한 결과를 얻을 수 있는 여러 방법이 있습니다. 중첩 레벨에는 제한이 없다는 점에 주목하세요. ActionView::render 메소드를 render template: 'layouts/news'를 통해 사용하여 News 레이아웃을 기반으로 새로운 레이아웃을 만들 수 있습니다. News 레이아웃을 서브 템플릿으로 사용하지 않을 것이 확실하다면, content_for?(:news_content) ? yield(:news_content) : yield를 단순히 yield로 대체할 수 있습니다.



맨 위로