has_many 관계를 갖는 모델에 대한 폼을 설계하기 위한 핵심은 첫째, 폼이 서밋되었을때 전달되는 params의 형태와 둘째, 이것이 콘트롤러와 모델에서 처리되는 과정에서 필요한 virutal attributes가 무엇인지를 이해하는 것이다. 레일즈는 네스티드 모델 폼의 설계를 쉽게하기 위해 fields_foraccepts_nested_attributes_for와 같은 헬퍼 메쏘드를 제공한다.
모델 생성
이 글에서는 Blog 모델이 여러개의 Sector로 구성된 경우를 가정한다. Blog는 title과 released_on 속성을 갖고, Sector는 name과 content 속성을 갖는다고 보고 테이블을 생성한다.
rails generate model Blog title released_on:date
rails generate model Sector name content:text blog:references
rake db:migrate
모델간의 관계(Association)는 아래와 같이 설정한다.
vi blog.rb
has_many :sectors
vi sector.rb
belongs_to :blog
폼 & fields_for
vi app/views/blogs/_form.html.erb

<%= form_for blog do |f| %>
  <div class="field">
    <%= f.label :title, '제목' %>
    <%= f.text_field :title %>
  </div>
  <div class="field">
    <%= f.label :released_on, '날짜' %>
    <%= f.date_field :released_on %>
  </div>
  <div class="action">
    <%= f.submit %>
  </div>
<% end %>
위의 폼에서 서밋되었을 때 전달되는 params의 형태는 아래와 같다.
params = {"blog" => {"title" => "xxx", "released_on" => "yyy"}}
아직까지는 네스티드 모델(=sectors)와 관련된 내용은 언급되지 않았다. 이제 레일즈 헬퍼인 fields_for를 사용해서 params가 어떻게 구축되는지 살펴보자.

<div class="nested_sectors">
  <%= f.fields_for :sectors, blog.sectors.build do |builder| %>
  <div class="field">
    <%= builder.label :name %>
    <%= builder.text_field :name %>
  </div>
</div>
이 경우 전달되는 params의 구조는 아래와 같다.

params = {"blog" => {"title" => "xxx", "released_on" => "yyy", "sectors" => {"name" => "zzz"}}}
이 params를 넘겨받은 BlogsController의 create 액션은 Blog.create(params[:blog])를 실행하게 되는데 이때 title=(), released_on=()과 같은 세터(Setter)가 순서대로 호출된다. 마지막으로 호출되는 세터(Setter)는 sectors=()가 되는데 has_many :sectors 정의에 의해 필요한 인자는 Sector 모델의 인스턴스가 배열 형태로 전달되어야 한다. 하지만 실제 전달되는 인자는 {"name" => "zzz"} 형태의 Hash이므로 TypeMismatch 에러가 발생한다.
모델 & accepts_nested_attributes_for
문제를 해결하기 위해서 먼저 아래와 같은 params가 전달된다고 가정을 한다.

params = {"blog" => {"title" => "xxx", "released_on" => "yyy", "sectors_attributes"=>{"0"=>{"name"=>"철수"}, "1"=>{"name"=>"영희"}}}}
create 액션에서 이 params를 제대로 처리하기 위해서는 새로운 세터인 sectors_attributes=()가 필요하다. 이 세터는 id를 key로 사용하는 Hash를 인자로 받아서 Sector 인스턴스를 생성하는 역할을 수행하면 되는데 좋은 소식은 우리가 직접 이 세터를 작성할 필요가 없다는 것이다. 레일즈는 이런 경우를 위해서 accepts_nested_attributes_for를 제공한다. vi blog.rb
accepts_nested_attributes_for :sectors
이 한 줄의 추가는 두가지 문제를 해결한다. 첫번째는 sectors_attributes=() 세터를 자동으로 만들어 준다는 점이고, 두번째는 <%= f.fields :sectors %>를 실행할 때 "sectors"가 아닌 "sectors_attributes" 를 가진 params["blog"] hash를 만들어낸다는 점이다.
콘트롤러 & 스트롱 파라메터(Strong parameters)
BlogsController에서 strong parameters에 sectors_attributes를 반영한다. vi app/controllers/blogs_controller.rb

def blog_params
  params.require(:blog).permit(:title, :released_on, :sectors_attributes => [:title, :content])
end
폼 & 리팩토링
Sector 필드의 동적인 추가, 삭제를 위한 준비 단계로 fields_for 부분을 partial에 넣는다. vi app/views/blogs/_form.html.erb

<%= form_for blog do |f| %>
  <div class="field">
    <%= f.label :title, '제목' %>
    <%= f.text_field :title %>
  </div>
  <div class="field">
    <%= f.label :released_on, '날짜' %>
    <%= f.date_field :released_on %>
  </div>
  <div class="nested_sectors">
    <%= f.fields_for :sectors do |builder| %>
      <%= render 'sector_fields', f: builder %>
    <% end %>
  </div>
  <div class="action">
    <%= f.submit %>
  </div>
<% end %>
vi app/views/blogs/_sector_fields.html.erb

<div class='nested_sector'>
  <div class="field">
    <%= f.label :name, '섹터이름' %>
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :content, '내용' %>
    <%= f.text_area :name %>
  </div>
</div>
f.fieds_for :sectors는 blog.sectors의 갯수만큼 블럭의 내용을 반복하게 되는데 만약 blog.sectors가 없다면 아무런 일도 일어나지 않는다. 따라서 BlogsController create 액션에서 임의의 sectors를 생성한다.

def create
  @blog = Blog.new
  3.times { @blog.sectors.build } # 여기서 3은 임의의 횟수
end
(계속 추가됩니다...)