Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby each 메서드의 한계: 빈 컬렉션을 우아하게 처리하는 방법

Ruby의 each 메서드를 정말 좋아하지만, 한 가지 아쉬운 점이 있습니다. 컬렉션이 비어 있으면 어떻게 될까요?

[].each를 호출하면 아무 일도 일어나지 않고 []가 그대로 반환됩니다. 때로는 이것이 원하는 동작일 수 있습니다. 하지만 대부분의 경우, 특히 UI를 개발할 때는 빈 목록을 특별한 방식으로 처리하고 싶어집니다. 예를 들어 데이터가 하나도 없을 때는 별도의 안내 메시지를 보여주고 싶은 경우가 많죠.

그런데 [].eachnil이 아니라 []를 반환하기 때문에, 어쩔 수 없이 이런 코드를 작성해야 합니다:

app/views/users/index.html.erb
<% if @users.empty? -%>
  <p>You don't have any users. <%= link_to "Add one!", new_user_path %></p>
<% else -%>
  <% @users.each do |user| -%>
      <p><%= user.name %></p>
  <% end -%>
<% end -%>

동작은 하지만 뭔가 어색합니다. 차라리 "컬렉션의 각 항목에 대해 이 작업을 수행하되, 컬렉션이 비어 있다면 완전히 다른 작업을 수행하라"라고 선언하고 싶습니다. 즉, "or else" 절이 붙은 each 같은 것이 필요한 순간입니다.

Rails에서 컬렉션 렌더링하기

뷰(view) 안에서 작업한다면 Rails가 도움을 줄 수 있습니다. Rails는 객체 컬렉션을 렌더링하는 데 탁월한 프레임워크입니다:

app/views/users/index.html.erb
<%= render @users %>
app/views/users/_user.html.erb
<p><%= user.name %></p>

render@users를 전달하면, @users 안의 각 사용자마다 _user.html.erb partial이 한 번씩 렌더링됩니다. each 루프를 아예 생략할 수 있는 셈이죠.

게다가 보너스로, @users가 비어 있으면 render는 우리가 원하던 대로 nil을 반환합니다! 따라서 다음과 같이 작성하면 처음 버전과 동일한 출력을 얻을 수 있습니다:

app/views/users/index.html.erb
<%= render(@users) || render('empty') %>
app/views/users/_user.html.erb
<p><%= user.name %></p>
app/views/users/_empty.html.erb
<p>You don't have any users. <%= link_to "Add one!", new_user_path %></p>

Rails의 관례(convention)를 이해하고 나면 훨씬 더 직관적이고 깔끔한 코드가 됩니다.

뷰 밖에서는 어떻게 할까?

Rails 방식을 따른다면, 컬렉션과 함께 사용하는 render는 객체 목록을 화면에 그려내는 빠르고 강력한 도구입니다.

하지만 매번 각 항목을 위한 별도의 partial 파일을 만들고 싶지 않을 때도 있습니다. 혹은 render가 원하는 디자인을 지원하지 않을 수도 있고, 애초에 뷰 계층 밖에서 작업 중일 수도 있습니다.

지금까지 찾은 최선의 해결책은 바로 presence입니다.

list.presence는 사실상 다음 코드와 같습니다:

if list.present?
  list
else
  nil
end

즉, 목록에 내용이 있으면 목록 자체를 반환하고, 비어 있으면 nil을 반환합니다.

presence를 활용하면 이렇게 작성할 수 있습니다:

@users.each do |user|
  puts user.name
end.presence || puts("You don't have any users.")

@users에 사용자가 있으면 각 이름을 출력하고, 비어 있으면 "You don't have any users."를 출력합니다.

그래도 presence 없이 해결할 수 있다면 더 좋겠습니다. 솔직히 말해 이건 일종의 꼼수(hack)이기 때문입니다. 언어 차원에서 지원된다면 다음과 같은 문법이 훨씬 자연스러울 것입니다:

@users.each do |user|
  puts user.name
end || puts("You don't have any users.")

혹은 Smalltalk 스타일의:

@users.each(if_empty: lambda { puts "You don't have any users." }) do |user|
  puts user.name
end

혹은 (깜짝 놀라실지도 모르지만!):

for user in @users
  puts user.name
else
  puts "You don't have any users."
end

참고로 Python의 for-else 문법이 실제로 마지막 방식과 유사하게 동작한다는 점이 흥미롭습니다. 하지만 당분간은 presence나 기본적인 if blank?; else each 패턴을 사용하는 것이 현실적인 선택일 것입니다.

여러분은 어떤 방법이 더 낫다고 생각하시나요? 빈 목록은 보통 어떻게 처리하시나요? 더 좋은 방법을 찾으셨다면 의견을 들려주세요!