Languages/Ruby or Rails2008.10.15 16:10

지난 번 글에서 RESTful 프로그래밍을 할 때 구현해야 할 CRUD 연산 중 CREATE를 구현하기 위해 Rails 서버측 코드를 어떻게 고쳐야 하고, jQuery 코드를 어떻게 고쳐야 하는지를 살펴봤습니다. 그럼 오늘은 RETRIEVE, UPDATE, DELETE 연산을 어떻게구현하는지 살펴보도록 하죠.

RETRIEVE 연산은 두 가지가 있습니다. ID를 주고 그 ID에 해당하는 리소스를 조회하는 연산, 그리고 모든 리소스를 전부 조회하는 연산이 바로 그것이죠. 지난 시간에 보았던 location이라는 리소스의 경우를 생각해 본다면, 모든 location을 전부 조회하는 연산이 있을 수 있고, 특정한 location만을 조회하는 연산이 있을 수 있습니다. 그런데 가만히 생각해 보면 모든 location을 전부 조회하는 것은 그다지 바람직하지 않습니다. 뭔가 조건을 줄 수 있도록 하는 것이 좋죠.

그러니, rails의 scaffold 명령이 생성한 코드들 조금 고쳐 주어야 합니다. 아래의 코드를 보시죠.

  # GET /locations
  # GET /locations.xml
  def index
    if ( params[:elementid] )
      @locations = Location.find(:all, :conditions =>
                          [ "elementid= ?", params[:elementid] ]);
    else
      @locations = Location.find(:all)
    end

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @locations }
      format.js { render :json => @locations }
    end
  end

'모든 location을 조회하는 연산'은 LocationsController의 index 메소드에 의해 처리됩니다. 이 메소드에 전달되는 params 해쉬에 :elementid를 키로 하는 값이 있는 경우, 그 값을 조건으로 하여 location 리소스들을 검색하도록 코드를 변경했습니다.물론 그런 값이 없는 경우에는 하던 대로 하면 되구요.

클라이언트 코드는 다음과 같이 작성하면 됩니다. HTTP 메소드로 GET을 사용할 것이기 때문에 $.getJSON을 사용해 구현할 수 있습니다.

var loadLocations = function(options, callback) {
  $.getJSON('locations', {elementid: options.elementid}, function(data) {
    $.each( data, function(index, d) {
      callback( d );
    });
  });
}

가져온 location 리소스 (json 형식입니다) 하나 하나를 callback 함수의 인자로 넘겨 처리하도록 하고 있습니다. 이 때 전달받은 데이터의 각 필드들을 접근하기 위해서는 d.location.id 뭐 이런 식으로 해야겠죠. 그런 부분은 일단 코드에서 다 뺐습니다. 구현하시는 분들이 알아서 하셔야 하는 부분이라서요.

어쨌든, RETRIEVE쪽은 이렇게 구현할 수 있습니다. RETRIEVE 연산 중 나머지 하나 (특정한 ID에 해당하는 리소스만들 조회하는 부분)는 다루지 않겠습니다. 여기까지만 아셔도 충분히 하실수 있으리라 믿고...

그럼 이제 CRUD 중 U, 즉 UPDATE를 하는 쪽을 알아보도록 하죠.

RETRIEVE를 통해 각각의 location 리소스에 할당된 id를 다 알아낼 수 있다는 것에 우선 유념하도록 합시다. 일단 rails 컨트롤러쪽 코드를 보면 다음과 같습니다.

  # PUT /locations/1
  # PUT /locations/1.xml
  def update
    @location = Location.find(params[:id])

    respond_to do |format|
      if @location.update_attributes(params[:location])
        flash[:notice] = 'Location was successfully updated.'
        format.html { redirect_to(@location) }
        format.xml  { head :ok }
        format.json { render :json => {}, :status => :accepted }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @location.errors, :status => :unprocessable_entity }
        format.json  { render :xml => @location.errors, :status => :unprocessable_entity }
      end
    end
  end

클라이언트가 참조한 URL에 붙어있는 ID값을 사용해 갱신할 location 객체를 찾은 다음, 갱신합니다. format.json이 어떻게 구현되어 있는지를 잘 보시기 바랍니다. 클라이언트가 json 데이터를 요구한 경우, format.json 안에서 head :ok만 해버리면 설사 rails 서버 쪽에서는 데이터베이스 갱신을 제대로 했다고 하더라도 웹 브라우저는 (그러니까 클라이언트) 이를 오류로 간주한다는 문제가 있습니다.

이 문제를 피해가기 위해서는 서버 쪽에서 비어있는 json 객체라도 만들어 클라이언트 쪽으로 던져줘야 합니다. :statis의 값은 저처럼 :accepted로 해도 되고, :ok로 해도 되겠죠.

여기에 대한 클라이언트 측 코드는 다음과 같습니다.

var updateLocation = function( options ) {
         $.ajax( {
            url: 'locations/' + options.id,
            type: 'put',
            data: {
              'location[x]': options.pageX,
              'location[y]': options.pageY,
              'location[width]': options.width,
              'location[width]': options.width
            },
            dataType: 'json'
          }
        );
}

type:의 값이 put이 되었다는 것 뺴고는 별거 없는 코드니까 설명은 생략하겠습니다. URL에 id를 붙여 주어야 한다는 점만 주의하면 되겠습니다. 그래야 어떤 아이템을 update할 것인지 서버가 알수 있을테니까요.

그럼 이제 CRUD 중 마지막 연산, DELETE를 한번 살펴보죠. 역시 rails 서버측 코드를 먼저 보겠습니다.

  # DELETE /locations/1
  # DELETE /locations/1.xml
  def destroy
    @location = Location.find(params[:id])
    @location.destroy

    respond_to do |format|
      format.html { redirect_to(locations_url) }
      format.xml  { head :ok }
      format.json { render :json => {}, :status => :accepted }
    end
  end

코드는 UPDATE의 경우와 비슷하므로, 설명은 생략하고 바로 클라이언트 코드로 넘어가겠습니다.

var deleteLocation = function( option ) {
       $.ajax( {
          url: 'locations/' + option.id,
          type: 'delete',
          dataType: 'json'
        }
      );
};

역시 URL에 삭제될 리소스의 ID가 붙어 들어간다는 것과, type이 delete로 지정되었다는 것을 제외하면 크게 새로울 것은 없는 코드입니다.

신고
Posted by 이병준
TAG ajax, jQuery, Rest

소중한 의견, 감사합니다. ^^

Languages/Ruby or Rails2008.10.13 13:39

잘 알려진 이야기입니다만, Rails는 REST를 꽤 우아하게 지원하는 플랫폼입니다. 그리고 Prototype을 사용한 JavaScript 연동 기능도 잘 지원하고 있죠. 그래서 프로그래밍하기에 따라서는 JavaScript를 전혀 할 줄 몰라도 AJAX 프로그래밍을 할 수 있습니다.

그런데 Rails가 제공하는 JavaScript 지원 기능들을 사용해 프로그래밍을 하다 보면, 이런 생각이 듭니다. 본질적으로 JavaScript는 CLIENT쪽 제어를 위한 기능이거든요. 이런 기능들이 서버쪽에서 생성하는 코드에 묻어들어가게 되면, 요즘 한참 뜨고 있는 "무간섭 자바스크립트" 코딩 철학에 맞는 프로그래밍을 하기는 좀 어려워지죠. 거기다, 클라이언트쪽 코드를 서버측 기술에 독립적인 형태로 구현하기도 어려워집니다. "서로 다른 티어(tier) 사이에 존재하는 종속성을 최소화한다"라는 것을 프로그래밍의 대원칙으로 삼는다면, 그 원칙에 맞게 코딩하기가 어려워진다는 겁니다.

그렇다면, 그 원칙을 지키면서 코딩을 하려면 어떻게 해야 하나요? 가급적 HTML과 JavaScript 코드는 서버쪽 스크립팅 기술과는 독립적으로 작성해야 할 겁니다. 그렇게 하면 다음과 같은 부수적 효과를 누릴 수 있습니다.

1. 서버측 기능이 풍부하지 않아도 된다. (REST만 지원해도 상관없다.)
2. 서버측에서 어떤 언어나 스크립팅 태그를 지원하건, 그에 무관한 코드를 작성할 수 있다.
3. 클라이언트 JavaScript 코드를 서버와는 상관없이 간단하게 교체/수정할 수 있다.

웹 서버가 REST만 지원해도 된다는 것은 어떤 의미일까요? RESTful 코딩 패턴만을 사용하여 프로그래밍을 하면 웹 서버를  마치 클라이언트 측 Resource에 대한 Persistency layer로 추상화한 코딩을 할 수 있다는 것에 주목합시다. 물론, 이렇게 하게 되면 MVC 패턴에 있어서 Model, View, Controller 로직이 전부 클라이언트 쪽으로 이동하게 됩니다. 그게 과연 옳은 일인지는 논쟁의 소지가 있으므로 언급하고 싶지는 않습니다만, 최근 jQuery등의 라이브러리 등장과 JavaScript에 관한 좋은 교과서들이 나옴에 따라, 그런 코드를 만들기가 쉬워지긴 했습니다.

예를 들어서 한번 살펴보죠. 여러분이 클라이언트 측에 임의의 Element를 동적으로 생성하고, 그 Element가 만들어질 때 마다 그 절대 위치를 Location이라는 이름의 웹 서버 리소스에 저장하고 싶다고 해 봅시다. 그런 일을 하려면, 우선 rails 서버에 Location이라는 이름의 리소스를 만들어 줘야 합니다.

$~ /work/rails/foo$ script/generate scaffold location elementid:string x:integer y:integer width:integer height:integer

위와 같이 실행하면 location이라는 resource에 대한 scaffold 코드가 쫙 만들어질겁니다. 그 컨트롤러 코드의 일부분을 잠깐 보시면...

class LocationsController < ApplicationController
  ...
  # POST /locations
  # POST /locations.xml
  def create
    @location = Location.new(params[:location])

    respond_to do |format|
      if @location.save
        flash[:notice] = 'Location was successfully created.'
        format.html { redirect_to(@location) }
        format.xml  { render :xml => @location, :status => :created, :location => @location }
        format.json { render :json => {:dbid => @location.id}, :status => :created }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @location.errors,
                             :status => :unprocessable_entity }
        format.json { render :json => @location.errors,
                             :status => :unprocessable_entity }
      end
    end
  end
  ...
end

생성된 코드 그대로는 아니고, 수정을 좀 했습니다. (몇줄 안됩니다. 코드라고 보기엔 좀 그렇고, '설정(configuration)'에 가까운 코드죠. Ruby 프로그래밍을 할 줄 몰라도 짤 수 있는, 그런 수준의 코드입니다.) 본 예제에서는 클라이언트에서 json 형태의 회신을 요구하는 것으로 가정을 했습니다. 서버는 클라이언트에서 POST 형태로 전달된 파라메터를 분석해서 Location 객체를 만들고, 그 객체를 database에 성공적으로 저장하고 나면 역시 json 포멧의 메시지를 ㄹ응답으로 전달해야 합니다. 그래서 format.json {...} 부분의 코드를 넣었구요. 이 코드를 보면, @location.save가 성공할 경우, { 'dbid': 33 } 과 같은 형식의 json 데이터가 클라이언트로 날아가게 됨을 알 수 있습니다.

자. 그러면 이것으로 서버측에서는 클라이언트가 날리는 데이터를 가지고 Location이라는 모델 객체(리소스)를 만들어 database에 저장할 준비를 끝냈습니다. 남은 일은 클라이언트에서 제대로 된 json 형태의 request를 날려주는 것 뿐입니다. 그럼 클라이언트 측 코드를 보시죠. jQuery로 작성한 코드입니다.

var createLocationAjax = function(options) {

  $.ajax( {
      url: options.url,
      type: 'post',
      data: {
        'location[elementid]': options.elementId,
        'location[x]': options.pageX,
        'location[y]': options.pageY,
        'location[width]': this.width,
        'location[height]': this.height
      },
      dataType: 'json',
      success: function(data, status) {
        alert(data.dbid);
      }
    }
  );
  return false;
}

createLocationAjax라는 함수를 하나 정의하고 있는데, 이 함수는 options라는 프라퍼티 객체를 인자로 받는 함수입니다. 이 프라퍼티 객체를 통해 url, elementId, pageX, pageY, width, height 등의 값을 인자로 전달할 수 있고, ajax 호출이 정상적으로 수행되었을 경우의 핸들러와 실패했을 경우의 핸들러도 인자로 전달할 수 있습니다. 위의 함수는 전달받은 인자들을 가지고 json 객체를 만들어, 그 객체에 대한 참조를 data라는 프라피터의 값으로 두고 $.ajax 함수를 호출합니다.

유의할 것은 서버측에서 params[:location]과 같은 형태로 클라이언트가 전송한 인자들을 가져와 분석하여 객체를 생성하기 때문에, 클라이언트측에서도 그에 맞게 json 프라퍼티 이름을 설정해 주어야 한다는 겁니다. rails의 form_for 태그가 생성하는 HTML form 코드를 보셨다면 아마 쉽게 이해하실 수 있을 듯. 따라서 모든 json 프라퍼티 이름은 location[xxx]의 형태로 지정하였습니다. 그래야 서버쪽에서 :location을 키로 사용하여 클라이언트가 전송한 모든 파라메터들을 한번에 가져올 수 있죠.

각설하고, 이렇게 하는 것 만으로 클라이언트쪽에서 생성한 모델 객체(!)의 위치 정보를 서버에 지속적(persistent)으로 저장할 수 있습니다. 저장이 잘 되었을 경우, 서버에 생성된 location 데이터 투플의 ID 필드값이 반환되기 때문에, (위의 코드에서 data.dbid가 바로 반환된 ID 필드값) 클라이언트 측에서는 그 ID를 사용해 update 같은 다른 작업도 할 수 있게 됩니다.

물론 이런 식으로 코딩하게 되면 웹 프로그래밍을 하는 사람이 자바 스크립트도 알아야 하고, rails도 쓸 줄 알아야 한다는 부담이 있긴 합니다만, rails 코딩에 드는 부담이 굉장히 낮기 때문에 오히려 JavaScript만 알더라도 간단하게(?) 웹 프로그래밍을 할 수 있게 됩니다. 웹 서버를 그냥 persistency API를 제공하는 서버 정도로 생각해버려도 되는데다, 사실 Ruby에 대해서 잘 몰라도 되거든요.

물론, 이 방법이 100% 완벽한 것은 아닙니다. 최신 버전의 rails에는 클라이언트 측 공격을 막기 위한 feature가 들어 있는데, (그에 관해서는 http://www.buggymind.com/148 를 참고하세요) 이 기능과 연동하기가 어렵습니다. rails API를 사용해 폼을 생성하거나 하지 않기 때문이죠. 따라서, 위의 예제가 제대로 돌아가기 위해서는 locations_controller.rb 파일의 맨 위쪽에 다음과 같은 코드를 추가해 주어야 합니다.

class LocationsController < ApplicationController
  skip_before_filter :verify_authenticity_token

  ...
end

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^