fields_for 사용기 2016년 업데이트

2012년 04월 14일 “fields_for 사용기”란 제목의 블로그 글을 작성한 적이 있다. 무려 4년전의 일이다. 세월이 많이 흘렀다. 레일스는 최근에 5.0.0.beta1 버전을 릴리스했고 현재 루비의 최신 버전은 2.3.0 이다. 이 글은 이전 글에 대한 2016년도 업데이트라고 생각하면 된다.

form_for 란 무엇인가?

레일스 API를 참고하여 내용을 정리해 보자. 레일스에서는 리소스의 생성(create)와 갱신(update) 작업을 손쉽게 할 수 있도록 헬퍼 메소드를 제공한다.

'form_for' 여기서 리소스라 함은, 물론 문맥에 따라 약간 다른 의미를 가질 수 있지만, 이 글에서는 편의상 '데이터베이스 테이블 레코드'를 의미한다고 생각하면 쉽게 이해가 될 것이다.

1. 샘플 애플리케이션 생성 설명을 위해서

'fields_for_r5'라는 프로젝트를 생성하고,

$ rails new fields_for_r5

노트 : 루비 2.3.0, 레일스 5.0.0.beta1 버전을 이용하여 프로젝트를 생성하였다.

2. 모델 리소스 생성

아래와 같이 레일스의 'scaffold' 제너레이터를 이용하여 'Post' 모델에 대한 리소스를 생성하자.

$ rails g scaffold Post title content:text 

3. 폼 Partial 템플릿 설명 생성된 파일 중에서 뷰 관련 파일목록은 아래와 같다.

$ tree app/views/posts 
app/views/posts 
├── _form.html.erb 
├── edit.html.erb 
├── index.html.erb 
├── index.json.jbuilder 
├── new.html.erb 
├── show.html.erb 
└── show.son.jbuilder 

이제, 새로운 'post'를 생성하기 위해서 브라우저에서 http://localhost:3000/posts/new 로 접속하면 서버단에서는 'posts#new' 액션이 호출되고 호출결과로 'new' 뷰 템플릿 파일('app/views/posts/new.html.erb')이 렌더링된다. 이 뷰 파일의 내용은 아래와 같다.

<h1>New Post</h1> 
<%= render 'form', post: @post %> 
<%= link_to 'Back', posts_path %> 

위에서 3번 코드라인을 주목하자.

<%= render 'form', post: @post %> 

레일스에서는 뷰 템플릿 파일을 여러 개의 조각('partials')으로 나누고 필요에 따라 재조합해서 사용할 수 있도록 지원하는데, 뷰 파일 내에서 이와 같은 'partial' 템플릿 파일을 불러오기 위해서 위와 같이 'render' 메소드에 'partial' 파일을 넘겨 준다. 알고 있는 바와 같이 실제 'partial' 파일명은 이름 앞에 '_'를 붙인다는 것을 기억하자. 따라서 위의 코드라인은 실제로 'app/views/posts/_form.html.erb' 파일을 불러오게 되는 것이다.

'scaffold' 제너레이터에 의해서 생성된 '_form.html.erb' 파일의 내용은 아래와 같다.

<%= form_for(post) do |f| %> 
  <% if post.errors.any? %> 
    <div id="error_explanation"> 
      <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2> 
      <ul> 
        <% post.errors.full_messages.each do |message| %> 
          <li><%= message %></li> 
        <% end %>
      </ul> 
    </div> 
  <% end %> 
  <div class="field"> 
    <%= f.label :title %> 
    <%= f.text_field :title %>
  </div> 
  <div class="field"> 
    <%= f.label :content %> 
    <%= f.text_area :content %>
  </div> 
  <div class="actions"> 
    <%= f.submit %>
  </div> 
<% end %> 

위의 'posts#new' 액션이 호출된 후 뷰 파일이 렌더링된 후 응답으로 보내지는 'html' 문서는 아래와 같다.

<form class="new_post" id="new_post" action="/posts" accept-charset="UTF-8" method="post"> 
  <input name="utf8" type="hidden" value="✓">
  <input type="hidden" name="authenticity_token" value="3TTNyi29MUuoeiz3nEDfQu1atUffixmshCQ2KC8+Y/QZPy8awyanTJTo/+oaEUBLkJNC2R/KfW+yLIlqGl+wEQ=="> 
  <div class="field"> 
    <label for="post_title">Title</label> 
    <input type="text" name="post[title]" id="post_title">
  </div> 
  <div class="field"> 
    <label for="post_content">Content</label> 
    <textarea name="post[content]" id="post_content"></textarea>
  </div> 
  <div class="actions"> 
    <input type="submit" name="commit" value="Create Post" data-disable-with="Create Post">
  </div> 
</form> 

즉, 리소스에 대한 폼 작업에 필요한 복잡한 'html' 코드 작업을 레일스의 폼 헬퍼 메소드 'form_for'를 이용하면 손쉽게 구현할 수 있게 된다.

fields_for 란 무엇인가?

이와 관련된 메소드로 'fields_for'가 있다. 이 메소드는 'form_for' 메소드와 함께 사용하여 하나의 폼에서 연결된 두개의 모델을 생성하게나 갱신할 수 있게 해 준다.

일대일 관계선언 예 설명을 위해서 새로운 샘플 애플리케이션을 작성해 보자.

'Person' 모델은 하나의 'Address' 모델을 가지는 것으로 가정한다. 즉 아래와 같은 모델 관계 설정을 구현한다

# Person 모델 클래스 
class Person < ActiveRecord::Base 
  has_one :address 
end 

# Address 모델 클래스 
class Address < ActiveRecord::Base 
  belongs_to :person 
end 

1. 샘플 리소스 생성과 관계선언

새로운 레일스 애플리케이션을 생성한 후 아래와 같이 'Person' 리소스를 생성한다.

$ rails generate scaffold Person name sex age

그리고 'Address' 리소스 모델을 생성한다. 이 때 주의해야 할 것은 모델 속성으로 'person:references' 파라메터를 추가해 주어야 한다는 것이다. 이로써 'Person' 모델 클래스와의 관계 선언시에 필요한 'foreign key'가 추가되고 'Address' 모델 클래스에 'belongs_to :person' 코드라인이 자동으로 추가된다.

$ rails generate model Address person:references street zip_code

'Person' 모델 클래스 파일에는 직접 아래와 같이 관계설정을 해 주어야 한다.

# app/models/person.rb 
class Person < ActiveRecord::Base 
  has_one :address 
end

참고로 'app/models/address.rb' 파일의 내용은 아래와 같다.

class Address < ActiveRecord::Base 
  belongs_to :person 
end

주의 : 'Rails 5'에서는 'belongs_to'에 추가 옵션이 필요하다. 'optional: true' http://blog.michelada.io/whats-new-in-rails-5

class Address < ActiveRecord::Base 
  belongs_to :person, optional: true 
end

우리가 구현하고자 하는 것은 'Person' 리소스를 폼으로 생성하거나 갱신할 때 'Address' 리소스도 함께 입력하도록 하는 것이다.

2. accepts_nested_attributes_for 메소드

바로 여기서 'fields_for' 메소드를 사용하면 된다. 가정 먼저 추가해야 할 것을 'Person' 모델 클래스에 중첩 폼을 구현하기 위한 매크로를 사용해야 한다. 아래의 코드를 보자.

class Person < ActiveRecord::Base 
  has_one :address 
  accepts_nested_attributes_for :address 
end

두 개의 모델이 관계 선언이 이미 되어 있을 경우 'accepts_nested_attribute_for' 메소드는 아래와 같은 가상 속성(virtual attributes)을 생성해 준다.

class Person 
  def address 
    @address 
  end 
  def address_attributes=(attributes) 
    # Process the attributes hash 
  end 
end

3. 폼(form) 뷰(view) 파일과 fields_for 메소드 그리고 아래와 같이 코드를 작성한다.

<%= form_for @person do |person_form| %> 
  <!--생략--> 
  <%= person_form.fields_for :address do |address_fields| %> 
    Street: <%= address_fields.text_field :street %> 
    Zip code: <%= address_fields.text_field :zip_code %> 
  <% end %> 
  ... 
<% end %> 

4. Strong parameter 추가

마지막으로 잊어서는 안되는 것은 'people' 컨트롤러에 'Address' 리소스에 대한 속성을 'Strong parameter'로 등록해 주어야 한다.

# app/controllers/people_controller.rb 

private 

def person_params 
  params.require(:person).permit(:name, :sex, :age, address_attributes: [:street, :zip_code]) 
end 

대략적인 코드구현이 완료되었다. 지금까지 작업한 내용을 정리하면,

  1. 두 개의 모델에 관계 선언을 한다.
  2. 'parent' 모델 클래스에 'child' 모델 객체에 대한 가상 속성을 추가하기 위해 'accepts_nested_attributes_for' 매크로를 추가한다.
  3. 폼 뷰 'partial' 템플릿 파일 내의 'form_for' 메소드 내에 'fields_for' 메소드를 사용하여 'child' 모델 객체 속성을 추가한다.
  4. 'parent' 모델 컨트롤러에 'child' 모델 객체에 대한 가상 속성을 'Strong parameter'로 등록해 준다. 실제로 코드를 작성하여 브라우저에서 확인해 보면 몇가지 문제점을 발견할 수 있게 된다. 이번에는 생성한 데이터를 수정해 보자.

'edit' 링크를 클릭하면 동일한 폼이 나타나는데, 이 때는 'address' 필드가 보이지 않게 된다. 이유는 'address' 모델 객체가 생성되지 않았기 때문이다. 따라서 'edit' 액션에서 '@person.build_address' 코드라인을 추가하여 'address' 모델 객체를 생성하면 해결된다. 결과적으로 'new''edit' 액션에서 동일한 코드를 중복해서 작성된다. 코드 'refactoring'를 위해서 중복되는 코드라인을 'app/views/people/_form.html.erb' 파일의 상단에 아래와 같이 추가해 준다.

<% person.build_address %> 
<%= form_form(person) do |f| %> 
  ... 

일대다 관계선언 예

이번에는 'address'를 하나 이상 입력할 수 있는 상황을 예를 들어 보자. 즉, 일대다의 관계선언시 'field_for' 메소드를 사용하는 방법을 살펴 보자. 세가지를 변경하면 된다.

주의 : 주의해서 볼 것은 복수형 'addressess'를 사용했다는 것이다. 이것은 'has_many' 관계선언을 사용한 것을 생각하면 자연스럽게 생각할 수 있다.

1. Person 모델(Parent) 클래스의 수정

# app/models/person.rb 
class Person < ApplicationRecord 
  has_many :addresses 
  accepts_nested_attributes_for :addresses, reject_if: :all_blank 
end 

'address'의 복수형태를 사용한 것과, 'reject_if: :all_blank' 옵션을 추가한 것을 주목하자. 후자는 'address' 객체의 값을 입력하지 않은 경우, 'address' 레코드를 추가하지 않도록 해 준다.

2. 'people' 컨트롤러의 변경

# app/controllers/people_controller.rb 
... 
private 
def person_params 
  params.require(:person).permit(:name, :sex, :age, addresses_attributes: [:street, :zip_code]) 
end 

'addresses_attributes'와 같이 'address'의 복수형을 사용한 것을 주목하자.

3. 폼 파셜의 수정

# app/views/people/\_form.html.erb 
<% person.addresses.build %> 
  <%= form\_for(post) do |f| %> 
  ... 
  <%= f.fields_for :addresses do | address_form | %> 
  ... 

'has_many' 관계선언을 한 후 'child' 객체를 생성할 때는 'person.addresses.build'와 같이 작성한다. 그리고 'fields_for'에는 복수형의 심볼 ':addresses'를 사용했다. 위의 세가지를 수정하면 'fields_for' 메소드를 이용하여 복수개의 'address'를 추가할 수 있게 된다.

삭제 옵션 추가하기 이와 같이 여러 개의

'address'를 추가할 경우 불필요한 'address'를 삭제할 수 있도록 해 보자.

1. :allow_destroy 옵션

# app/models/person.rb 
class Person < ApplicationRecord 
  has_many :addresses accepts_nested_attributes_for :addresses, reject_if: :all_blank, allow_destroy: true 
end 

2. 폼 Partial의 수정

# app/views/people/\_form.html.erb 
... 
<div class="field"> 
  <%= address\_form.text_field :zip_code %> 
  <%= address_form.text_field :zip_code %>
</div> 
<div class="field"> 
  <%= address_form.check_box :\_destroy %> 
  <%= address\_form.label :_destroy, "remove" %>
</div> 
... 

3. Strong parameter 추가

':id', ':_destroy'

# app/controllers/people_controller.rb 
... 
private 
def person_params 
   params.require(:person).permit(:name, :sex, :age, addresses_attributes: [:id, :street, :zip_code, :_destroy]) 
end

이제 'remove' 체크박스를 클릭한 후 저장하면 해당 'address'가 삭제될 것이다.

References:

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.