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
대략적인 코드구현이 완료되었다. 지금까지 작업한 내용을 정리하면,
- 두 개의 모델에 관계 선언을 한다.
'parent'
모델 클래스에'child'
모델 객체에 대한 가상 속성을 추가하기 위해'accepts_nested_attributes_for'
매크로를 추가한다.- 폼 뷰
'partial'
템플릿 파일 내의'form_for'
메소드 내에'fields_for'
메소드를 사용하여'child'
모델 객체 속성을 추가한다. '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'
가 삭제될 것이다.