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

고통 없이 Rails respond_to 활용하기

Rails에서 스캐폴드(scaffold)를 생성하면 익숙한 respond_to 블록이 함께 만들어집니다.

app/controllers/tasks_controller.rb
  def destroy
    @task.destroy
    respond_to do |format|
      format.html { redirect_to tasks_url, notice: 'Task was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

그런데 index처럼 일부 액션에는 이 블록이 없습니다!

app/controllers/tasks_controller.rb
  # GET /tasks
  # GET /tasks.json
  def index
    @tasks = Task.all
  end

이건 문제가 됩니다. 왜일까요? 앱이 지원하지 않는 txt 형식으로 /tasks.txt에 접근하면 엉뚱한 에러가 발생합니다.

ActionView::MissingTemplate (Missing template tasks/index, application/index with {:locale=>[:en], :formats=>[:text], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}

이건 적절한 응답이 아닙니다. 클라이언트에게 "지원하지 않는 형식을 요청했다"고 알려줘야지, "파일을 찾을 수 없다"고 답해서는 안 됩니다.

만약 UnknownFormat 에러였다면 훨씬 더 의미 있는 응답 코드를 반환할 수 있었을 겁니다. 하지만 지금은 이런 에러가 전혀 관련 없는 다른 에러들과 뒤섞여 버려서, 나중에 처리하기가 매우 까다로워집니다.

index 액션에 respond_to 블록을 추가하면 해결됩니다.

app/controllers/tasks_controller.rb
  # GET /tasks
  # GET /tasks.json
  def index
    @tasks = Task.all
    respond_to do |format|
      format.html
      format.json
    end
  end

이제 기대했던 대로 예외와 에러 코드가 반환됩니다.

Started GET "/tasks.txt" for 127.0.0.1 at 2014-11-03 22:05:12 -0800
Processing by TasksController#index as TEXT
Completed 406 Not Acceptable in 21ms

ActionController::UnknownFormat (ActionController::UnknownFormat):
  app/controllers/tasks_controller.rb:8:in `index'

훨씬 낫네요. 하지만 모든 컨트롤러에 respond_to 블록을 도배하는 건 현명한 방법이 아닙니다. Rails답지 않고, DRY 원칙에도 위배되며, 컨트롤러가 실제로 하는 핵심 작업에서 주의를 분산시킵니다.

그래도 잘못된 형식의 요청은 제대로 처리하고 싶을 텐데요. 어떻게 하면 좋을까요?

respond_to 한 줄 축약형

렌더링에 특별한 로직이 필요 없다면 더 간단한 방법이 있습니다. 아래처럼 작성하면:

app/controllers/tasks_controller.rb
def index
  @tasks = Task.all
  respond_to :html, :json
end

index에 전체 respond_to 블록을 쓴 것과 동일하게 동작합니다. 액션이 지원하는 모든 형식을 Rails에 한 줄로 알려주는 방법입니다. 액션마다 지원하는 형식이 다르다면, 최소한의 코드로 그 차이를 처리하기에도 좋습니다.

컨트롤러 단위로 형식 처리하기

보통은 컨트롤러의 각 액션이 같은 형식을 다룹니다. indexjson에 응답한다면 new, create 등 나머지 액션도 마찬가지겠죠. 그렇다면 컨트롤러 전체에 영향을 주는 respond_to가 있다면 얼마나 좋을까요?

app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  before_action :set_task, only: [:show, :edit, :update, :destroy]
  respond_to :html, :json

  # GET /tasks
  # GET /tasks.json
  def index
    @tasks = Task.all
    respond_with(@tasks)
  end

실제로 이렇게 동작합니다:

Started GET "/tasks.txt" for 127.0.0.1 at 2014-11-03 22:17:37 -0800
Processing by TasksController#index as TEXT
Completed 406 Not Acceptable in 7ms

ActionController::UnknownFormat (ActionController::UnknownFormat):
  app/controllers/tasks_controller.rb:8:in `index'

바로 우리가 기대했던 에러죠! 게다가 각 액션을 일일이 수정할 필요도 없었습니다.

모델의 상태에 따라 다르게 동작해야 할 때도 있습니다. 예를 들어 create 액션은 모델 유효성 검사 결과에 따라 리다이렉트하거나 폼을 다시 렌더링해야 하죠.

Rails도 이를 처리할 수 있습니다. 다만 respond_with로 어떤 객체를 검사할지 알려줘야 합니다. 따라서 이렇게 길게 쓰는 대신:

app/controllers/tasks_controller.rb
  def create
    @task = Task.new(task_params)

    respond_to do |format|
      if @task.save
        format.html { redirect_to @task, notice: 'Task was successfully created.' }
        format.json { render :show, status: :created, location: @task }
      else
        format.html { render :new }
        format.json { render json: @task.errors, status: :unprocessable_entity }
      end
    end
  end

이렇게 간결하게 쓸 수 있습니다:

app/controllers/tasks_controller.rb
  def create
    @task = Task.new(task_params)
    flash[:notice] = "Task was successfully created." if @task.save
    respond_with(@task)
  end

이렇게 하면 비즈니스 로직과 응답 형식이 깔끔하게 분리됩니다. 어떤 형식을 처리할지 Rails에게 딱 한 번만 알려주면 되고, 매 액션마다 반복할 필요가 없습니다.

responders 젬(gem)

Rails 4.2부터는 한 가지 주의할 점이 있습니다. respond_with가 기본으로 포함되지 않았다는 것이죠. 대신 responders 젬을 설치하면 다시 사용할 수 있습니다. 이 젬은 그 외에도 유용한 기능들을 여럿 제공합니다.

컨트롤러 상단에 responders :flash를 추가하면 respond_with에서 플래시 메시지를 설정할 수 있습니다:

app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  responders :flash

게다가 이 플래시 메시지들의 기본값은 로케일(locale) 파일에서 설정할 수 있어서 매우 편리합니다.

또한 Gemfileresponders 젬이 있으면, 스캐폴드 생성 시 제너레이터가 respond_to 대신 respond_with를 사용하는 컨트롤러를 만들어 줍니다:

app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  before_action :set_task, only: [:show, :edit, :update, :destroy]
  respond_to :html, :json
  
  def index
    @tasks = Task.all
    respond_with(@tasks)
  end

  def show
    respond_with(@task)
  end

  # ...

Rails 기본 스캐폴드보다 훨씬 깔끔합니다.

마지막으로, 특정 액션에만 별도의 형식으로 응답하고 싶다면 respond_to를 여러 번 호출하면 됩니다:

class TasksController < ApplicationController
  respond_to :html
  respond_to :js, only: :create
end

마지막 팁은 댓글로 공유해 주신 Jeroen Weeink님께 감사드립니다!

respond_with vs respond_to, 무엇을 쓸까?

형식별로 다른 정보를 반환하고 싶다면 몇 가지 선택지가 있습니다. 컨트롤러 단위의 respond_torespond_with 조합은 컨트롤러 코드를 짧게 유지하는 데 탁월합니다. 다만 모든 액션이 같은 형식에 응답하고 Rails가 기대하는 방식대로 동작할 때 가장 큰 효과를 발휘합니다.

반면 일부 액션만 다르게 동작해야 한다면 한 줄짜리 respond_to가 딱 맞습니다.

더 세밀한 제어가 필요하다면 블록과 함께 전체 respond_to를 사용하세요. 각 형식을 원하는 대로 자유롭게 처리할 수 있습니다.

어떤 방법을 선택하든, 지원하지 않는 형식의 요청에는 정확한 에러가 반환됩니다. 앱도 클라이언트도 훨씬 덜 헷갈리게 되겠죠.