Rails에서 스캐폴드(scaffold)를 생성하면 익숙한 respond_to 블록이 함께 만들어집니다.
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처럼 일부 액션에는 이 블록이 없습니다!
# 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 블록을 추가하면 해결됩니다.
# 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 한 줄 축약형
렌더링에 특별한 로직이 필요 없다면 더 간단한 방법이 있습니다. 아래처럼 작성하면:
def index
@tasks = Task.all
respond_to :html, :json
end
index에 전체 respond_to 블록을 쓴 것과 동일하게 동작합니다. 액션이 지원하는 모든 형식을 Rails에 한 줄로 알려주는 방법입니다. 액션마다 지원하는 형식이 다르다면, 최소한의 코드로 그 차이를 처리하기에도 좋습니다.
컨트롤러 단위로 형식 처리하기
보통은 컨트롤러의 각 액션이 같은 형식을 다룹니다. index가 json에 응답한다면 new, create 등 나머지 액션도 마찬가지겠죠. 그렇다면 컨트롤러 전체에 영향을 주는 respond_to가 있다면 얼마나 좋을까요?
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로 어떤 객체를 검사할지 알려줘야 합니다. 따라서 이렇게 길게 쓰는 대신:
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
이렇게 간결하게 쓸 수 있습니다:
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에서 플래시 메시지를 설정할 수 있습니다:
class TasksController < ApplicationController
responders :flash
게다가 이 플래시 메시지들의 기본값은 로케일(locale) 파일에서 설정할 수 있어서 매우 편리합니다.
또한 Gemfile에 responders 젬이 있으면, 스캐폴드 생성 시 제너레이터가 respond_to 대신 respond_with를 사용하는 컨트롤러를 만들어 줍니다:
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_to와 respond_with 조합은 컨트롤러 코드를 짧게 유지하는 데 탁월합니다. 다만 모든 액션이 같은 형식에 응답하고 Rails가 기대하는 방식대로 동작할 때 가장 큰 효과를 발휘합니다.
반면 일부 액션만 다르게 동작해야 한다면 한 줄짜리 respond_to가 딱 맞습니다.
더 세밀한 제어가 필요하다면 블록과 함께 전체 respond_to를 사용하세요. 각 형식을 원하는 대로 자유롭게 처리할 수 있습니다.
어떤 방법을 선택하든, 지원하지 않는 형식의 요청에는 정확한 에러가 반환됩니다. 앱도 클라이언트도 훨씬 덜 헷갈리게 되겠죠.