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 이병준

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

Languages/Ruby or Rails2008.10.10 11:00

Ruby를 주마간산식으로 배운 사람에게는 감동적인 코드...

module ActionController
  module StatusCodes #:nodoc:
    # Defines the standard HTTP status codes, by integer, with their
    # corresponding default message texts.
    # Source: http://www.iana.org/assignments/http-status-codes
    STATUS_CODES = {
      100 => "Continue",
      101 => "Switching Protocols",
      102 => "Processing",

      200 => "OK",
      201 => "Created",
      202 => "Accepted",
      203 => "Non-Authoritative Information",
      204 => "No Content",
      205 => "Reset Content",
      206 => "Partial Content",
      207 => "Multi-Status",
      226 => "IM Used",

      300 => "Multiple Choices",
      301 => "Moved Permanently",
      302 => "Found",
      303 => "See Other",
      304 => "Not Modified",
      305 => "Use Proxy",
      307 => "Temporary Redirect",

      400 => "Bad Request",
      401 => "Unauthorized",
      402 => "Payment Required",
      403 => "Forbidden",
      404 => "Not Found",
      405 => "Method Not Allowed",
      406 => "Not Acceptable",
      407 => "Proxy Authentication Required",
      408 => "Request Timeout",
      409 => "Conflict",
      410 => "Gone",
      411 => "Length Required",
      412 => "Precondition Failed",
      413 => "Request Entity Too Large",
      414 => "Request-URI Too Long",
      415 => "Unsupported Media Type",
      416 => "Requested Range Not Satisfiable",
      417 => "Expectation Failed",
      422 => "Unprocessable Entity",
      423 => "Locked",
      424 => "Failed Dependency",
      426 => "Upgrade Required",

      500 => "Internal Server Error",
      501 => "Not Implemented",
      502 => "Bad Gateway",
      503 => "Service Unavailable",
      504 => "Gateway Timeout",
      505 => "HTTP Version Not Supported",
      507 => "Insufficient Storage",
      510 => "Not Extended"
    }

    # Provides a symbol-to-fixnum lookup for converting a symbol (like
    # :created or :not_implemented) into its corresponding HTTP status
    # code (like 200 or 501).
    SYMBOL_TO_STATUS_CODE = STATUS_CODES.inject({}) do |hash, (code, message)|
      hash[message.gsub(/ /, "").underscore.to_sym] = code
      hash
    end

    # Given a status parameter, determine whether it needs to be converted
    # to a string. If it is a fixnum, use the STATUS_CODES hash to lookup
    # the default message. If it is a symbol, use the SYMBOL_TO_STATUS_CODE
    # hash to convert it.
    def interpret_status(status)
      case status
      when Fixnum then
        "#{status} #{STATUS_CODES[status]}".strip
      when Symbol then
        interpret_status(SYMBOL_TO_STATUS_CODE[status] ||
          "500 Unknown Status #{status.inspect}")
      else
        status.to_s
      end
    end
    private :interpret_status

  end
end

해쉬 hash는 처음에 {}로 초기화된다. STATUS_CODES 내의 아이템은 하나씩 (code, message)로 전달된다. 이 때 message에 포함된 공백을 전부 ""로 대체하고, 소문자로 바꾼 다음, 심볼 (:ok와 같은)로 바꾸고 hash 객체의 key로 삼는다. 해당 심볼에 매핑되는 값은 code가 된다.

신고
Posted by 이병준

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

Languages/Ruby or Rails2008.08.01 13:36

이 문제는 나그네 님께서 지적해주신 문제입니다. 최신 버전의 Rails는 일반 html 파일에 포함된 form의 input 필드들과 연동할때, 약간의 문제가 있습니다.

가령 Beginning Ruby on rails의 5장에 수록된 예제처럼 input.html 파일을 다음과 같이 만들었다고 해 봅시다.

<html>
<body>
    <form action="/look/at">
    <input type="text" name="text1"/>
    <input type="submit"/>
    </form>
</body>
</html>

이 form에 뭔가를 입력하고 submit 버튼을 누르면 /look/at 액션이 실행될 것이고, 그 결과가 화면에 출력될 것입니다. 그런데 그냥 이렇게만 하면 별 문제가 없다가, 위의 html 코드를 다음과 같이 바꾸면 문제가 발생합니다.

<html>
<body>
    <form action="/look/at" method="post">
    <input type="text" name="text1"/>
    <input type="submit"/>
    </form>
</body>
</html>

위와 같이 바꿔놓고 submit  버튼을 눌러보면, 다음과 같은 오류가 발생하는 것을 볼 수 있습니다.

ActionController::InvalidAuthenticityToken in LookController#at

액션 컨트롤러 쪽에서 AuthenticityToken이라는 것을 요구하는데 그게 없기 때문에 발생하는 오류입니다. 이 문제를 좀 더 정확하게 이해하기 위해, 다음과 같이 해 봅시다. 일단 controller Look에 input이라는 액션을 추가합니다.

class LookController < ApplicationController
    def at
        @data = params[:text1]
    end

    def input
    end

end

그런 다음 위의 html 코드를 다음과 같이 바꾸고, app/views/look 아래에 input.html.erb 라는 이름으로 복사해 넣습니다.

<html>
<body>
    <% form_tag '/look/at', :method=>:post do %>
    <input type="text" name="text1"/>
    <input type="submit"/>
    <% end %>
</body>
</html>

그런 다음에, 브라우저로 /look/input을 엽니다. 그리고 나서 브라우저의 소스 코드 보기 기능을 통해 소스 코드를 봅시다. 다음과 같이 되어 있는 것을 볼 수 있습니다.

<html>
<body>
 <form action="/look/at" method="post"><div style="margin:0;padding:0"><input name="authenticity_token" type="hidden" value="8f846ee3232eda791b4bf4c9d6d0a20478afcf8a" /></div>
 <input type="text" name="text1"/>
 <input type="submit"/>
 </form>
</body>
</html>

결국, /look/at은 method가 post일때 위의 token이 오기를 기대하는데 안오니까 불평을 해 댔던 것이죠. 이 문제를 해결하는 방법은 두가지가 있는데, 위에서처럼 모든 form 태그를 전부 form_tag 메소드를 써서 생성하는 것입니다. 그러면 알아서 넣어주니까 괜찮죠.

하지만 <div> 태그가 원래 원칙적으로 line break를 유발하는 습성이 있기 때문에, 웹 디자이너에게는 솔직히 짜증나는 부분이 될 수 있습니다. 그냥 hidden input filed로 두면 되었을텐데 div는 뭐하러 두었는지 참 ㅋㅋ

가장 간단한 해결책은 CSRF(Cross-Site Request Forgery) 공격을 막기 위해 도입된 위 feature를 강제로 동작하지 않도록 만드는 것입니다.

위의 경우라면, 컨트롤러 Look에 다음의 한줄을 넣어주세요.

skip_before_filter :verify_authenticity_token

제가 Beginning Ruby on Rails를 번역하면서 원저자가 책을 썼던 시점에 비해 바뀐 부분을 반영해 넣느라 고민을 좀 했었는데, 이렇게 책을 내놓고 보니 또 그 와중에 뭔가 새로운 기능이 계속 추가가 되어서 -_-;;

어쨌던 불편을 끼쳐드려서 죄송합니다. 앞으로도 이런 부분을 발견할 때 마다, 그리고 독자 분들의 질문이 있을 때 마다 가능한한 열심히 답변을 드리도록 하겠습니다.


신고
Posted by 이병준

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

  1. 전 원인도 모르고 그냥 첫번째 방법 썼는데 ^^ 좋은 정보 감사합니다~

    2008.08.01 14:26 신고 [ ADDR : EDIT/ DEL : REPLY ]

Languages/Ruby or Rails2008.07.29 09:06
Openid를 사용한 로그인 기능을 Rails를 사용하여 구현하면 굉장히 간단히 구현가능한 것으로 알려져 있습니다. http://www.aproxacs.com/122 를 보면 잘 알수 있는데요, 그런데 시키는대로 따라하다보면 뭔가 이상한 문제들이 한두가지 생기는 것을 발견할 수 있습니다.

Web Password 팝업 창이 뜬다

http://www.aproxacs.com/122 이 글에 나온 대로 구현을 하다 보면, application controller (application.rb) 를 변경해서 login_required를 before_filter로 적용하는 부분을 볼 수 있습니다. 그 의미인 즉슨 login이 안된 상태에서 action을 실행하려고 하면 (sessions 컨트롤러의 메소드들은 제외) 전부 login 창으로 redirection 되도록 만들기 위함인데, 문제는 IE 상에서 가끔 sessions 컨트롤러의 액션이 아닌 다른 액션에 접근하려고 해 보면, Openid로그인창으로 redirection이 되는게 아니라, Web Password 팝업 창이 떠버린다는 점입니다.

이 문제는 lib/ 디렉터리 아래에 있는 authenticated_system.rb 파일에 정의되어 있는 다음 메소드 때문에 발생합니다.

    def access_denied
      respond_to do |format|
        format.html do
          store_location
          redirect_to new_session_path
        end
# bjlee:
# block following 3 lines to remove annoying 'Web Password' Popup
# is displayed on the screen especially for IE.
#
#        format.any do
#          request_http_basic_authentication 'Web Password'
#        end

      end
    end

이 문제를 가장 '간단하게' 해결하려면, 위에서 보는 것 처럼 format.any do 부터 end까지를 comment 처리해주면 됩니다. (물론, 그다지 바람직한 해결책이라고는 볼 수 없습니다만...)

로그인 후에 로그인 전에 마지막으로 방문했던 페이지로 돌아가지 못한다

보통 사람들이 기대하는 것이, 로그인이 되고 나면 그 전에 마지막으로 보고 있었던 페이지로 돌아가는 것일텐데요. http://www.aproxacs.com/122 에 나온대로 해 두고 따라해 보면 그 이전 페이지로 돌아가려다가 메소드가 없다느니 객체가 nil이라느니 하는 등등의 오류 메시지가 뜰 때가 있습니다.

그것은 app/controllers/sessions_controller.rb 파일에 있는 successful_login 메소드에 포함된 오류 때문입니다. 다음과 같이 수정하면 해결됩니다.

    def successful_login
        if params[:remember_me] == "1"
            self.current_user.remember_me
            cookies[:auth_token] = {
                :value => self.current_user.remember_token ,
                :expires => self.current_user.remember_token_expires_at
            }
        end
        if ( session[:return_to] != nil )
            redirect_to session[:return_to]
        else
            redirect_to( '/' )
        end
    end

적색으로 표시한 부분이 수정한 부분입니다.



신고
Posted by 이병준

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

Languages/Ruby or Rails2008.07.17 14:47

passenger를 설치하는 절차는 우선 다음의 명령을 입력하는 것으로 시작된다.

sudo gem install passenger

엔간하면 이 절차는 정상적으로 완료될 것. 하지만 그 다음이 문제. 설치 문서에는 passenger-install-apache2-module 을 실행하라고 나오는데, 이 파일은 /var/lib/gems/1.8/gems/passenger-2.0.2/bin 디렉터리 아래에 있는데다, 결정적으로 실행 권한이 설정되어 있지 않다.

그러므로 실행하려면 그 디렉터리 아래에 가서, sudo ruby passenger-install-apache2-module 처럼 실행해 주어야 한다. 그런데 그렇게 실행해 보면, rake가 gems 디렉터리 아래에 분명 설치되어 있는데도 설치되어 있지 않다고 불평하는 일이 발생한다. (이런...)

이런 일이 발생했다면, 여러분의 gems installation에 뭔가 문제가 발생한 것이다. -_-;
http://agileweb.wordpress.com/2008/07/18/how-to-install-rails-21-on-ubuntu-in-5-steps/
위의 링크를 참조해서 여러분의 gems installation 자체를 업데이트해보는 것이 좋겠다. 그런 다음 rails, passenger 등등을 다시 설치해야 한다.

이것은 rake 실행파일이 /usr/bin/ 아래에 복사되어 있지 않기 때문에 발생하는 문제. 따라서 sudo cp /var/lib/gems/1.8/gems/rake-0.8.1/bin/rake /usr/bin 과 같이 해주어야 한다. 그러면 문제가 해결된다. (카피가 찜찜한 분은 심볼릭 링크를 걸어주는 편이 낫겠다.) 실행권한도 빠져있으니 sudo chmod +x로 걸어주어야 한다.

이렇게 하고 나서 다시 sudo passenger-install-apache2-module 를 해 보면 뭔가 make 한다는 메시지가 쫙 나오고 나서 다음과 같은 텍스트가 화면에 출력된다.

Please edit your Apache configuration file, and add these lines:

   LoadModule passenger_module /var/lib/gems/1.8/gems/passenger-2.0.2/ext/apache2/mod_passenger.so
   PassengerRoot /var/lib/gems/1.8/gems/passenger-2.0.2
   PassengerRuby /usr/bin/ruby1.8

After you restart Apache, you are ready to deploy any number of Ruby on Rails
applications on Apache, without any further Ruby on Rails-specific
configuration!

Press ENTER to continue.

/etc/apache2/conf.d 디렉터리 아래에 passenger라는 파일을 만들어 위의 설정을 그대로 입력해 보자. 그런 다음에 enter 키를 누르면 다음의 텍스트가 화면에 출력된다.

Deploying a Ruby on Rails application: an example

Suppose you have a Ruby on Rails application in /somewhere. Add a virtual host
to your Apache configuration file, and set its DocumentRoot to
/somewhere/public, like this:

   <VirtualHost *:80>
      ServerName
www.yourhost.com
      DocumentRoot /somewhere/public
   </VirtualHost>

And that's it! You may also want to check the Users Guide for security and
optimization tips and other useful information:

  /var/lib/gems/1.8/gems/passenger-2.0.2/doc/Users guide.html

Enjoy Phusion Passenger, a product of Phusion (www.phusion.nl) :-)
http://www.modrails.com/

Phusion Passenger is a trademark of Hongli Lai & Ninh Bui.

일단 여기까지 해서 설치는 끝났다. 이제 남은 일은 VirtualHost 설정을 적절히 해서 Rails의 실행 결과가 브라우저에 전송될 수 있도록 하는 것 뿐...

우선, apache에 또다른 VIrtualHost 설정을 꾸며서, 접속 도메인 명이 달라질 경우 해당 VirualHost가 실행될 수 있도록 한다.

<VirtualHost *>
    ServerAdmin byungjoon.lee@gmail.com
    ServerName www.xxxxx.com
    DocumentRoot /home/bjlee/work/rails/xxxxx/public
    RailsBaseURI /
</VirtualHost>

대략 위와 같이 하면 된다. 다만 한가지 주의할 것은, 위의 /home/.../public 디렉터리에 www-data (apache2가 실행되는 사용자명) group에 대한 읽기쓰기 권한을 주어야 한다는 것. 필자는 /home/.../work/rails 디렉터리에 가서 그 아래에 있는 모든 subdirectory에 대해 chown -R bjlee:www-data *를 실행하고, chmod -R g+w * 를 실행해 주었다.

이렇게 하고 나서 브라우저의 주소창에 www.xxxxx.com을 입력하고 접속해보면 접속이 된다. 접속이 안될 경우 해당 도메인 명이 등록이 안된 것이니 ㅋㅋ 윈도우의 hosts 파일을 고쳐서 등록된 것처럼 꾸며야 할 것.

그런데 이렇게 하면서 몇가지 프로그래머가 반드시 알아야 할 사항이 드러났는데.

1. development 모드에서는 *.rhtml의 확장자를 사용해 view template을 꾸밀 수 있었는데, production mode를 가정하는 passenger의 특성상 *.html.erb의 확장자가 아니면 인식을 못한다는 것. (Rails 2.0부터는 *.rhtml의 확장자는 더이상 사용하지 않으며, *.html.erb의 확장자를 사용해야 한다. 다만 현재로서는 하위호환성을 위해 *.rhtml을 Development 모드에서는 지원하고 있다.)

2. 원래 그런 건지 아니면 설정을 잘못해서 그런건진 몰라도 (아마 전자일듯) passenger를 사용해 rails 프로그램을 띄우면 소스코드를 바꿔도 그 결과가 바로 반영되어 실행되지는 않는다. (소스코드의 변경을 반영하려면 웹 서버를 다시 띄워야 한다.) 아마 production mode 로 release를 하는 것을 가정하는 passenger의 특성상 그러할 것. 물론 시간이 지나 모든 세션이 종료되고 난 뒤에는 수정된 코드가 다시 반영된다. :-) Rails app 디렉터리 아래에 tmp/restart.txt 파일을 'touch' 해도 된다. 이 경우에는 웹 서버 전체가 재시작되는 것이 이니라, 해당 app만 재시작된다.

여기까지만 주의하면 passenger를 통해 rails 프로그램을 쾌적하게 배포할 수 있을 것이다.

다른 문제에 대해서는 passenger를 배포하고 있는 웹 사이트에 가서 http://www.modrails.com/ 사용자 가이드를 보면 될 것이다.


신고
Posted by 이병준

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

Languages/Ruby or Rails2008.06.13 10:03

제가 번역했던 Beginning Ruby on Rails 책을 구매하신 분중 두 분깨서 다음과 같은 오류를 지적해 주셨습니다. 해결책을 포스팅하겠다고 약속했었는데 늦어서 죄송합니다.

이 문제는 Rails가 sqlite3를 기본 database로 사용하게 되면서 빚어진 문제입니다. MySQL을 데이터베이스로 사용하려면 rails <app name> --database=mysql 과 같이 해주어야 합니다. 그렇게 하지 않으면, 지적된 오류가 발생하게 됩니다.

Windows

Ruby 1.8.6 버전을 Windows에 설치한 다음 gem을 통해 rails를 설치하고 Rails 프로그램을 만들어 돌려보면 no such file to load -- sqlite3 라는 오류가 발생하는 것을 보게 됩니다. 이 오류의 원인은, Rails가 sqlite3를 기본적으로 설치하지 않는데서 빚어지는 것입니다. 따라서, 이 오류를 교정하기 위해서는 다음과 같은 절차를 밟아야 합니다. sqlite3를 설치하기 위한 절차입니다. 현재로서는 Windows에서 발생한 경우만 확인되었으므로, Windows에서의 해결방법을 기준으로 말씀드리겠습니다.

1. http://www.sqlite.org/download.html을 방문합니다.

2. 화면 좌측의 Precompiled Binaries for Windows 섹션에서 sqlite-3_5_9.zip과 sqlitedll-3_5_9.zip의 두 파일을 다운로드 받습니다. 이 두 파일의 압축을 ruby 설치 디렉터레 아래의 bin 이라는 이름의 서브디렉터리에 풀어 놓습니다. 그 결과 해당 디렉터리 아래에 sqlite3.exe sqlite3.dll sqlite3.def의 세 가지 파일이 생겨있어야 합니다.

3. 그런 다음에 명령행에서 gem install sqlite3-ruby를 실행합니다. 화면에 여러 가지 옵션들이 나오게 될텐데, 1번 옵션에 나오는 sql3-ruby 1.2.2 (mswin32)를 선택합니다.

Bulk updating Gem source index for: http://gems.rubyforge.org
Select which gem to install for your platform (i386-mswin32)
 1. sqlite3-ruby 1.2.2 (mswin32)
 2. sqlite3-ruby 1.2.2 (ruby)
 3. sqlite3-ruby 1.2.1 (mswin32)
 4. sqlite3-ruby 1.2.1 (ruby)
 5. Skip this gem
 6. Cancel installation
> 1

위와 같이 하면 이제 sqlite3가 설치될 것입니다. 여기까지 하면 sqlite3 관련 오류는 해결이 되어야 합니다.

Linux (Ubuntu)

Ubuntu에서는 원래

sudo apt-get install sqlite3
sudo gem install sqlite3-ruby

위의 두 과정이 제대로 수행되면 문제가 없이 되어야 합니다.
그런데 간혹 두 번째 명령을 실행하다가 no such file to load -- mkmf 라는 메시지가 나오면서 설치가 제대로 안되는 경우가 있습니다. 그런 경우에는

sudo apt-get install ruby1.8-dev

위의 명령을 먼저 실행해 줍니다.

그런 다음 다음과 같이 하면 됩니다.

bjlee@bjlee-ubuntu804:~$ sudo gem install sqlite3-ruby
Select which gem to install for your platform (i486-linux)
 1. sqlite3-ruby 1.2.2 (mswin32)
 2. sqlite3-ruby 1.2.2 (ruby)
 3. sqlite3-ruby 1.2.1 (mswin32)
 4. sqlite3-ruby 1.2.1 (ruby)
 5. Skip this gem
 6. Cancel installation
> 2

그러면 다음과 같은 메시지가 주욱 나오면서 설치가 될 것입니다.

Building native extensions.  This could take a while...
Successfully installed sqlite3-ruby-1.2.2
Installing ri documentation for sqlite3-ruby-1.2.2...
Installing RDoc documentation for sqlite3-ruby-1.2.2...

이제 웹 서버를 내렸다가 다시 올리기만 하면 문제가 해결되어야 합니다.

감사와 사죄의 말씀

이 오류를 지적해주신 분은 킹상수님과 딸기벌레님, 두분입니다. 답변은 구글에 문의하면 찾을 수 있는 내용이긴 합니다만, 제가 빠른 답변을 드리겠다고 해 놓고 답을 제때 못드려서 죄송하다는 말씀, 다시한번 드립니다.

질문주신 두분께는 메일로 따로 연락드리도록 하겠습니다.



신고
Posted by 이병준

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

  1. 까아~

    이게 문제였군요!! ㅠㅠ 안되서 설치만 여러번 한것 같아요...
    감사합니다. ^ㅡ^ 책은 구매해서 잘 보고있어요. 행복한 하루 되세요..

    2008.08.29 10:08 신고 [ ADDR : EDIT/ DEL : REPLY ]