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:

글쓴이: 최효성

외과전문의,웹프로그래밍,컴퓨터 일러스트레이션 / Surgeon, Medical Illustration, Web Programmer

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중