Header

  1. View current page

    딥뿔이 자라나는 노트

Profile_image?t=1225424611&type=big
나를 바꾼 똑똑한 생활 습관, 스프링노트 - 여러분도 지금 시작해보세요!
38

빠른 웹 개발의 선두주자, 레일스 (마소 2008년 3월)

웹 개발 생산성을 이야기하면 가장 먼저 떠오르는 것이 루비 온 레일스(Ruby on Rails)다. 여러가지 이유가 있겠지만, 그 무엇보다 개발자의 재미와 행복을 먼저 생각하는 분위기가 가장 큰 작용을 한 결과일 것이다. 웹 개발을 지루한 반복이 아니라, 창의적이고 즐거운 유희로 바꿔주는 것이 레일스다. CodeTalks 서비스를 함께 만들어보며, 레일스 웹 개발의 재미와 속도감을 느껴보자.

 

강문식 byblue@gmail.com

 

 

해시로켓이라는 회사는 고객이 원하는 웹 애플리케이션을 단 3일 만에 개발해 인도하는 사업을 한다. 3주가 아니라, 3일이다. 3일도 많다고 생각하는지, 48시간동안 웹 애플리케이션을 만들어 겨루는 레일스 럼블(Rails Rumble)이라는 대회도 매년 열리고 있다. 국내로 눈을 돌려볼까? 개발자 한명이 한 달에 하나씩 서비스를 만들었다는 1인 기업 이노크레이지가 떠오른다.  그리고 유니크카드라는 매력적인 서비스는 세명의 개발자가 8시간동안 만들었다고 한다. 황당한 이야기로 들릴 수도 있겠지만, 실제 일어나는 일들이다. 이렇게 빠른 속도로 만들어진 애플리케이션의 완성도가 떨어져 보이지도 않는다. 오히려 번뜩이는 아이디어가 놀랍다. 그리고 의미있는 성공을 거두기도 한다. 파트타임 개발자 2명이 만들었다는 Friends for Sale이 페이북 플랫폼 위에서 한 달에 30억 페이지뷰라는 관심을 받게되는데 걸리는 시간은 고작 세달이었다.

그림_3.png

그림 1. 해시로켓사의 서비스, 루비의 해시 문법(=>)에서 따온 그들의 회사 이름만큼이나 인상적인 서비스 문구다.

 

지금은 누가 뭐래도 속도 경쟁의 시대다. 세상이 빨리 변한다고 하지만, 웹 분야의 변화 속도를 따라올 수는 없을 것 같다. 오늘도 수많은 서비스가 새로 생겨나고, 또 수명을 다한다. 이런 세상을 살아가는 개발자가 하게 되는 가장 큰 고민은 다름 아닌 적기출시(Time to market)가 가능한가이다. 몇백 페이지 두께의 기획서를 만들고 오랜 기간 프로젝트를 한다는 것은 사치일 뿐이다. 항상  실제 구현된 모습을 보며 이야기하고, 실패마저도 빨라야한다. 자연스럽게 높은 생산성을 보장할 좋은 프레임워크를 찾게 된다. 기술 장벽을 낮추고, 시간 낭비를 줄여 개발자가 자신의 문제에 집중할 수 있도록 도와주는 프레임워크가 필요하다.

 

다행스럽게도 우리 주변에는 당장 가져다 활용할 수 있는 좋은 프레임워크들이 많다. 웹에서 비동기 통신을 alert문처럼 쉬운 일로 만들어준 Prototype, 옐로 페이드 효과를 식상하게 만든 Scriptaculous, 누구나 격자형 사이트를 만들 수 있게 해준 BlueprintCSS 등이 좋은 예다. 물론, 서비스 하나 만드는 일을 5분, 10분짜리로 취급한 레일스도 빼놓을 수 없다. 지속 가능한 개발 생산성을 모토로 하는 루비 온 레일스는 그야말로 가뭄의 단비 같은 존재였다. 여러 분야에서 뛰어난 식견을 보여주는 팀 브레이(Tim Bray)는 빠른 개발, 쉬운 유지 보수야말로 레일스가 주는 가장 큰 혜택이라고 설명한다. 이미 눈치 챘겠지만, 글의 서두에서 설명한 사례들은 모두 레일스를 잘 활용한 예다.

 

그림_13.png

그림 2. 팀 브레이가 설명하는 레일스가 생산적인 이유

 

그림 2는 레일스에서 중시하는 개발 철학을 나타낸 것이다. 이들이 모두 높은 생산성으로 이어진다. 하나하나에 대해 설명을 하려면 지면을 다 채워도 모자랄 것 같다. 이 글에서 딱딱하게 레일스의 기능을 설명하기보다는, 함께 사이트를 하나 만들어보며 웹 개발의 속도감을 느껴보자. 레일스의 장점을 직접 느낄 수 있으면 좋겠다. 무엇보다도 이 글을 읽는 분들이 '어렵지 않네? 나도 이번 주말에는 서비스를 하나 만들어볼까?'라는 생각이 들게 하는 것이 필자의 목적이다. 하지만 레일스에 대한 사전 지식이 전혀 없다면, 글을 읽기 편치 않을지도 모르겠다. 혹시 그렇다면 인터넷 상에 좋은 글들이 많고, 레일스를 설명한 책도 여러 권 나와있으니 함께 참고하기 바란다. 그리고 궁금한 점은 루비 사용자 모임(http://forum.rubykr.org/)에 올리면 금방 답을 얻을 수 있다.

 

CodeTalks.net - 코드로 말해요!#

작년에 루비 사용자 모임에서 코드로 말해요라는 부제로 세미나를 한 적이 있다. 40여명의 참여자가 모두 자산이 생각하는 가장 인상적인 루비 코드를 준비하고, 이를 차례로 발표하는 형식이었다. 이 날 얻은 정보의 밀도는 매우 높았고, 그 경험도 무척 인상적이었다. 그 이후 비슷한 경험을 할 수 있는 온라인 커뮤니티를 만들고 싶다는 생각을 갖게 되었는데, 이 글을 통해 직접 만들어보면 좋겠다. CodeTalks라는 이름도 붙여봤다. 이 서비스를 통해 자신이 인상깊게 본 코드를 공유하기도 하고, refactormycode.com처럼 코드를 올려 함께 리팩토링을 해보기도 하고, 하나의 코드를 여러가지 언어로 번역도 해보는 등 코드를 중심으로 이야기할 수 공간을 만드는게 목표다.

 

만드는 과정을 최대한 빼지 않고 적을 테니 따라 만들어 봐도 좋겠다. 그리고 중간 결과물을 서브버전에 커밋할 것이다. 원하는 부분의 리비전을 체크 아웃해서 그 부분부터 만들어봐도 좋겠다. 아래 명령은 65번 리비전을 체크아웃한다.

 

  1. svn co svn://rubyforge.org/var/svn/springnote/codetalks@65

 

자, 이제 본격적으로 시작해보자. 첫 출발은 누구도 피해갈 수 없는 과정으로 애플리케이션 뼈대를 만드는 일이다.

 

  1. prompt> rails codetalks -d sqlite3

 

루비 1.8.x, 레일스 2.0.x, 루비젬 등은 이미 설치되어 있으리라 믿는다. 레일스 2.0부터는 mysql 대신 sqlite3를 기본 데이터베이스로 사용한다. sqlite3가 설치도 쉽고 간편해서 개발용으로 좋다.

 

  1. prompt> script/generate scaffold code title:string source:text description:text

 

원래 계획은 이런 스캐폴드로 '짠! 완성'이라며 놀래키고 시작하는 것이었다. 그렇지만 조금 식상하다. 다양한 경로(대표적으로는 10분만에 블로그 만들기 스크린캐스트)를 통해 스캐폴드를 이미 접해 봤을 게 분명하기 때문이다. 한가지 더 짚고 넘어가야할 사실은 스캐폴드가 데모용으로는 최고지만, 실전에서 생각만큼 유용하지는 않다는 점이다. 이렇게 생성되는 코드가 자신의 스타일이 아닐 것이기 때문에 이 코드를 리팩토링 하느라 더 많은 시간이 소모될지도 모르겠다. 추천하는 바는 자신만의 스캐폴드를 플러그인으로 만들어두고 재사용하라는 것이다.

 

플러그인은 레일스의 꽃!#

레일스 코어(core)는 대부분의 경우에 유용한 기능만을 포함하고 있다. 하지만 루비의 동적인 덕분에 얼마든지 코어를 확장해 사용할 수 있다.

 

레일스의 동작이 내가 생각하는 것과 다를 때, 또는 새로운 기능이 필요할 때 취할 수 있는 접근은 두 가지다. 첫째, 직접 구현한다. 둘째, 비슷한 고민을 누군가가 먼저 했을 것이 분명하므로 이미 구현된 플러그인이 없나 찾아본다. 현명하게도 두번째 방법을 택했다면 플러그인 디렉터리(http://agilewebdevelopment.com/plugins/)가 큰 도움이 될 것이다. 이 글을 쓰는 시점에 천여 개의 플러그인이 등록되어 있다. 여유 시간에 어떤 플러그인이 등록되어 있는지 한번쯤 둘러보자. 

 

플러그인을 적용할 때 주의할 점은 버전 호환성이다. 적용하기 전에 코드를 한번 살펴보는 게 좋다. 혹시 플러그인에서 레일스 코드의 일부를 가져와서 바꾸고 있다면 유의해야한다. 뭔가 잘못될 확률이 높기 때문이다.

 

간결한 컨트롤러를 위한 MakeResourceful 플러그인#

레일스 2.0부터는 모든 컨트롤러가 7가지 액션(index, show, edit, new, update, create, destroy)을 거의 같은 코드로 구현하게 된다. 그래서 여기 저기 코드 중복이 생긴다.  특히 스캐폴드를 실행하면 매번 똑같은 코드가 한 벌씩 더 생긴다. 현재의 레일스 컨트롤러는 순수 REST로 가는 과도기에 있기 때문에 이런 중복의 일부는 어쩔 수 없는 부분이라고 생각한다. 그래도 DRY 원칙에 따라 이를 해결할 목적의 플러그인들이 다양하게 나와 있으니 하나 택해서 사용해보는 것도 좋겠다. 이 글에서는  MakeResourceful 플러그인을 사용한다. 이 플러그인은 컨트롤러 기본 탬플릿을 제공하며, 필요에 따라 일부를 재정의해 사용함으로써 코딩 양을 줄인다. 먼저 플러그인을 설치해보자.

 

  1. prompt> script/plugin install http://svn.hamptoncatlin.com/make_resourceful/trunk

 

이제 CodeTalks의 주인공인 코드 리소스를 읽고 쓰는 기능을 구현해보자. 플러그인에서 제공하는 resourceful_scaffold를 사용한다.

 

  1. prompt> script/generate resourceful_scaffold code title:string source:text description:text

 

위 명령은 코드 리소스를 위한 모든 것을 생성해준다. 이 중 주목할 파일은 다음과 같다.

 

  • 컨트롤러: app/controllers/codes_controller.rb
  • 모델: app/models/code.rb
  • 뷰: app/views/codes/*.html.haml
  • 마이그레이션: db/migrate/001_create_codes.rb

 

마이그레이션 파일은 데이터베이스에 대한 변경을 담고 있다. 마이그레이션 기능을 잘 활용한다면, 애플리케이션을 개발해가며 자연스럽게 데이터베이스 구조를 잡아갈 수 있다. 여기서는 코드를 담을 테이블을 새로 만들어야한다. 001_create_codes.rb 파일을 열어 확인해보자. up 메서드와 down 메서드가 있을 것이다. up 메서드는 codes 테이블을 만든다. down 메서드는 마이그레이션을 롤백하는 경우에 실행되는 것으로, up에서 했던 행동을 역으로 행한다. 이 경우에는 codes 테이블을 지운다.

 

  1. prompt> rake db:migrate

 

위 명령을 수행하면 현재 애플리케이션의 스키마 버전이 1이 된다. 롤백하려면 0번 버전을 지칭해 마이그레이션을 수행하면 된다. 시험 삼아 한번 해보자. 마이그레이션의 장점을 느낄 수 있을 것이다.

 

  1. prompt> rake db:migrate VERSION=0

 

또 하나 재미있는 것은 이 플러그인이 생성해주는 뷰 파일의 확장자다. 모든 파일이 html.haml로 끝난다.  레일스 뷰 파일의 이름을 짓는 규칙은 관례로 정해져있는데, 형식은 다음과 같다.

 

  1. /app/views/<컨트롤러_이름>/<액션>.<마임_타입>.<템플릿_엔진>

 

예를 들어 /app/views/codes/index.html.haml이라는 뷰 파일은 CodesController#index 메서드에서 HTML을 렌더링하는데 사용한다. 그리고 이 이름에서는 렌더링 엔진으로 HAML을 사용한다는 사실도 알 수 있다. 루비 프로젝트에서 사용할 수 있는 렌더링 엔진은 다양한데(http://myruby.net/pages/639796 참조), 이 중 HAML은 간결한 문법으로 개발자들 사이에서 인기가 높다. 특히나 기사를 쓸 때는 열고 닫는 태그의 중복이 지면을 많이 차지하는 ERB보다는 HAML이 적당해 보인다. 

 

HAML을 설치하고, 애플리케이션에 플러그인도 설치하자.

 

  1. prompt> sudo gem install haml
  2. prompt> haml --rails .

 

resourceful_scaffold에서 생성한 codes_controller.rb도 한번 살펴보자. 기본 템플릿을 사용하므로 아주 짧고 선언적인 코드를 볼 수 있다. 마치 액티브레코드  ORM의 코드를 보는 느낌이다.

 

  1.   make_resourceful do
        actions :all
      end

 

이제, 간단히 코드를 올리고 지울 수 있는 애플리케이션이 만들어졌다. 서버를 구동하고, 브라우저에서 http://localhost:3000/codes를 입력하면 익숙한 화면을 볼 수 있다.

 

  1. prompt> script/server

 

여기까지 작업한 내용을 저장소에 커밋한다. 리비전 번호는 64다.

 

blueprintCSS를 이용한 레이아웃#

아무리 개발 프로토타입이라도 약간의 노력을 들여 볼만한 화면을 만들어두는 것이 중요하다. 필자의 경험으로는 화면이 예쁘지 않으면, 대충 만들자는 생각에 코드도 예쁘지 않게 되는 것 같다. 이왕이면 다홍치마가 좋지 않겠는가. blueprintCSS(http://code.google.com/p/blueprintcss/)를 사용하면 개발자들도 간단하게 깔끔한 화면을 만들 수 있다. 레일스용 플러그인도 있으니 간편하게 적용할 수 있다.

 

  1. prompt> script/plugin install http://svn.ariejan.net/plugins/blueprint

 

그리고 애플리케이션에서 사용할 css를 생성한다.

 

  1. prompt> script/generate blueprint application

 

위 명령은 layouts/application.rhtml을 생성해준다. 애플리케이션 전체에서 사용할 레이아웃 파일이다. 이 파일을 지우고 application.html.haml이라는 HAML 파일을 만든다. 아래 코드는 이렇게 만든 레이아웃 코드의 일부다.

 

  1.   %body
        .container
          .column.span-24.last
            %h1
              = title
          %hr/
          = render :partial => 'layouts/menu'
          %hr/
          .column.span-18
            = yield :layout
          .column.span-6.last
            = render :partial => 'layouts/sidebar'

 

위 코드에서 span-24, span-18 등이 blueprintCSS에 제공하는 클래스다. 이를 이용하면 간단하게 격자형식으로 컬럼을 정의할 수 있다. 

 

코드에 나타나는 title은 뷰에서 사용할 수 있도록 만든 헬퍼 메서드다. app/helpers/application_helper.rb 파일에서 정의해 주었다.

 

  1.   def title
        @title ||= "CodeTalks: 코드로 말해요!"
      end

 

menu와 side 파셜 뷰 파일을 포함해 사용하는 모습도 볼 수 있다. 이 파일은 각각 layouts/_menu.html.haml과 layout/_sidebar.html.haml로 만들어줘야 한다. 여기까지 작업한 내용이 65번 리비전이며, 적용된 화면은 그림 3에서 볼 수 있다.

 

그림_4.png

그림 3 blueprintcss가 적용된 모습

 

Ultraviolet을 이용한 코드 문법 강조#

혹시 잊었을지 모르니, 다시 한번  이야기하는데 CodeTalks의 주인공은 코드다. 이제 주인공 대접을 좀 해줘야겠다. 보기 좋게 코드 문법 강조를 적용해보자. 이를 위해서는 사용할 외부 라이브러리부터 결정해야 한다. vim을 사용하는 방법도, 있고 순수 루비 라이브러리인 syntax를 사용할 수도 있다. 여기서는 Unltraviolet(http://ultraviolet.rubyforge.org/) 라이브러리를 사용하겠다. 이 라이브러리는 Textpow를 이용해 텍스트메이트(루비 개발자들이 많이 사용하는 맥용 에디터)의 스타일 파일을 가져와서, 이를 이용해 코드를 렌더링한다. Oniguruma도 함께 설치해야해서 약간 복잡할 수 있다. 설치 방법은 http://snippets.aktagon.com/snippets/61-Installing-Ultraviolet-and-Onigurama를 참조하자.

 

  1. prompt> sudo gem install ultraviolet

 

cobalt 테마를 사용할테, 젬이 설치된 디렉터리로 이동해서 cobalt.css 파일을 애플리케이션의 public/stylesheets에 복사하자. 그리고 layouts/application.html.haml에서 cobalt.css를 불러오는 것도 잊지 말자.

 

  1. = stylesheet_link_tag 'application', 'cobalt', :media => 'screen, projection'

 

그 다음 application_helper.rb를 열어 헬퍼 메서드를 구현한다.

 

  1. require 'uv'
    def code_highlight(code, lang = 'ruby')
      ::Uv.parse(code, "xhtml", lang, false, 'cobalt')
     end 

 

끝으로 코드를 출력하는 부분(예를 들어 views/codes/_code.html.haml)을 찾아 모두 아래처럼 바꿔주기만 하면 된다.

 

  1. = code_highlight current_object.source

 

여기까지가 66번 리비전이다.

 

그림_5.png

그림4. 문법 강조가 적용된 모습

 

엑스퀘어드 XHTML 편집기의 적용#

뷰 개선 마지막 단계로, 코드에 대한 설명을 입력하는 부분에 엑스퀘어드를 적용해보자. 엑스퀘어드는 스프링노트의 일부를 오픈 소스로 공개한 XHTML 에디터다. 보다 자세한 내용은 홈페이지(http://xquared.springbook.playmaru.net/)를 참고하기 바란다. 필자가 레일스용 플러그인을 만들어두었으므로, 이를 이용하면 정말 손쉽게 적용할 수 있다.

 

  1. prompt> script/plugin install svn://rubyforge.org/var/svn/springnote/plugins/xquared

 

위 명령을 수행하면 엑스퀘어드에서 사용하는 파일이 public 디렉토리에 복사되는 모습을 볼 수 있다. 그 다음 가장 먼저 할 일은 레이아웃 파일을 열어서 head 부분에 다음을 추가하는 것이다.

 

  1. = xquared_include_tag

 

그리고 text_area 태그 대신 xqured_text_area_tag를 사용하기만 하면 된다. 정말 간단하지 않은가?

 

  1. = xquared_text_area_tag "code[description]", @code['description']

 

그림 5는 수정 폼(_form.html.haml)에 엑스퀘어드를 적용한 화면이다. 리비전 67번이 완성되었다.

 

그림_6.png

그림5. 폼에 엑스퀘어드를 적용한 모습

 

스레드 형식으로 보기#

애플리케이션이 꽤 예뻐졌기 때문에, 이제 새로운 기능을 추가할 용기가 생겼다. 코드를 올리고 코드에 대한 답글(코드)을 달며 토론을 하기 위해서는 스레드 형식의 뷰를 지원하는 것이 좋겠다. 이를 구현하는 방법은 여러 가지가 있을 것이다. 레일스 1.2까지 코어 모듈의 일부였다가 2.0부터 플러그인으로 분리된 ActsAsNestedSet, ActsAsTree가 먼저 떠오른다. 우리는 ActsAsNestedSet을 확장한 BetterNestedSet 플러그인(

http://opensource.symetrie.com/trac/better_nested_set/)을 사용하도록 하자.

 

  1. prompt> script/plugin install svn://rubyforge.org/var/svn/betternestedset/tags/stable/betternestedset

 

참고로 ActsAs로 시작하는 이름을 가진 플러그인은 모델에 새로운 기능을 부여한다. 예를 들어 어떤 모델이 acts_as_taggable로 정의되어 있다면, 태깅을 할 수 있는 기능이 추가되는 식이다. 여기서는 Code 모델을 acts_as_nested_set으로 지정한다. models/code.rb를 열어 다음 문장을 추가하자.

 

  1. class Code < ActiveRecord::Base
      acts_as_nested_set
    end

 

 

스레드를 표현하라면 각 노드에서 부모, 왼쪽, 오른쪽 노드를 알아야 한다. 이 정보를 데이터베이스에 저장하기 위해 마이그레이션을 만들자.

 

  1. prompt> script/generate migration AddThreadSupport

 

생성된 db/migrate/002_add_thread_support.rb을 열어 다음 내용을 추가한다.

 

  1.   def self.up
        add_column :codes, :parent_id, :integer
        add_column :codes, :lft, :integer, :default => 1
        add_column :codes, :rgt, :integer, :default => 2
      end

      def self.down
        remove_column :codes, :parent_id
        remove_column :codes, :lft
        remove_column :codes, :rgt
      end

 

codes 테이블에 3개의 컬럼을 추가하는 것임을 쉽게 알 수 있다.

 

  1. prompt> rake db:migrate

 

이제, 화면에 댓글을 달 수 있는 링크를 추가하자. 적당한 위치를 찾아 아래 link_to 호출을 넣는다. 예를 들어  codes/show.html.haml에는 이런 코드를 추가한다. new_object_path에 parent_id를 덧붙이는 것이다.

 

  1. = link_to 'Reply', new_object_path + "?parent_id=#{current_object.id}"

 

new와 edit에서 사용하는 폼에서는 parent_id를 히든 필드로 넘겨줘야 한다.

 

  1. = hidden_field_tag :parent_id, params[:parent_id]

 

이 히든 필드를 받는 부분은 CodesController의 create 메서드다. 이 메서드는 올바른 코드의 자식으로 객체를 생성해야 한다. 코드는 다음과 같다.

 

  1.     before :new do
          current_object.title = @parent.title if @parent
        end
  2.  
  3.     before :new, :create do
          @parent = Code.find(params[:parent_id]) unless params[:parent_id].blank?
        end
       
        after :create do
          current_object.move_to_child_of(@parent) if @parent
        end

 

먼저 params[:parent_id]가 있으면 @parent 객체를 객체를 생성한다. 그리고 new에서는 @parent.title을 복사해서, 댓글의 경우 기본적으로 같은 타이틀을 사용하도록 정한다. create를 성공적으로 마친 후에는 새로 생성한 객체(current_object)를 @parent의 자식으로 추가한다. 컨트롤러 기본 구현에 before, after 콜백을 적용한 모습이 흥미롭다.

 

이제 코드에 달린 댓글을 화면에 출력해보자. code.level 값을 이용해 들여쓰기를 하면 스레드 구조를 쉽게 표현할 수 있다. 아래 코드는 show.html.haml에서 해당 코드의 자식들(children)을 모두 출력하는 것이다.

 

  1. %h2 Replies
    = render :partial => 'code', :collection => current_object.all_children

 

모든 자식(all_children)을 code 파셜을 이용해 출력하라는 코드다. 이 때 적당한 들여쓰기를 하도록 code.html.haml 파일을 열어 아래처럼 수정하자.

 

  1. %div{:style => "margin-left:#{code.level*3}em;"}[code]

 

그림_7.png

그림 6. 들여쓰기를 이용해 표현한 스레드

 

지금까지의 내용을 63번 리비전으로 커밋한다. 여기서 사용하지는 않았지만 BetterNestedSet은 move_to_left_of, move_to_right_of, move_to_child_of 등의 메서드를 지원한다. 이를 이용해 위 코드를 더 발전시켜봐도 좋겠다. 예를 들어 드래그&드롭으로 스레드에 속한 글의 배열을 바꾼다던지, 특정 규칙에 따라 순서가 바뀐다든가 하는 내용이면 만들어보기에도 재밌을 것 같다. 여러분의 몫으로 남겨두겠다.

 

코드를 주제별로 분류한다 #

지금까지 만든 서비스를 사용하다보니, 코드를 주제별로 따로 담아서 정리하고 싶은 생각이 들었다. 앞에서 이야기한 3가지 주제로 나눠보면 좋겠다. 이를 위해서는, codes 테이블을 바꿔 자신이 속한 주제 아이디를 알 수 있게 해야 한다. 마이그레이션이 있으니 문제 없다. 생성기가 만들어준 마이그레이션 파일인 db/migrate/003_create_subjects.rb를 다음처럼 수정해보자. up 메서드에서 테이블을 새롭게 만드는 부분은 두고 그 아래 다음 코드를 추가한다.

 

  1.     add_column :codes, :subject_id, :integer
       
        execute "INSERT into subjects(title) VALUES('old items')"   
        execute "UPDATE codes SET subject_id = (select max(id) from subjects)"

 

codes 테이블에 자신이 속한 주제를 나타내는 subject_id를 추가하고, 기존 코드들을 모두 하나의 주제로 묶어준다. 마이그레이션에서 SQL도 직접 수행할 수 있으므로, 따로 데이터 이전을 위한 스크립트를 작성하지 않아도되어 편리하다.

 

Subject와 Code는 1:N 관계다. 모델에 이 정보를 적어주자. 특정 주제는 여러개의 코드를 가지고 있고, Subject를 지우면 여기 속한 코드들도 함께 지워준다.

 

  1. class Subject < ActiveRecord::Base
      has_many :codes, :dependent => :destroy
    end

 

그리고 모든 코드는 특정 주제에 속한다.

 

  1. class Code < ActiveRecord::Base
      belongs_to :subject
    end

 

다음 할 일은 모델의 관계(1:N)를 리소스에 적용하는 것이다. 먼저 수정해야할 부분은 라우팅(config/routes.rb)이다. 라우팅은 URL을 디스패치하는 규칙을 적어두는 곳이다. 아래처럼 수정하자.

 

  1. map.root :controller => "subjects"
  2. map.resources :subjects, :has_many => :codes

 

첫번째 줄은 http://localhost:3000/를 SubjectController#index 메서드로 연결한다. 첫 화면에서 주제 목록을 표현하기 위해서다. 두번째 줄은 코드에서도 쉽게 볼 수 있듯, 1:N관계를 설정한 것이다. 이렇게 하면 /subject/1/codes/3 과 같은 주소가 생성된다. 1번 주제에 속한 3번 코드 지칭하는 URL이다. 이를 중첩된 리소스(nested resource)라고 표현한다. 이를 반영하기 위해 코드 컨트롤러도 변경해 줘야 한다.

 

  1. make_resourceful do
      belongs_to :subject       
    end

 

그리고 코드 리스트를 현재 속한 주제로 제한하기 위해 current_objects 메서드를 재정의한다.

 

  1. def current_objects
      parent_object.codes.roots
    end

 

다 하고보니 비슷한 수정이 3군데나 있다. 모델을 고치고 라우팅을 고치고 컨트롤러도 고쳐야했다. 이런 중복도 해결할 수 있다면 좋을 것 같지만, 지금은 이 정도에서 만족하기로 한다. 더 하면 편집증처럼 보일지도 모르겠다.

 

뷰 코드는 전혀 수정하지 않아도 http://localhost:3000/subjects/1/codes에 접속하면 1번 주제에 속한 코드 목록을 볼 수 있다. 그림 7은 주제 목록을 보여주는 화면이다. 이렇게 리비전 69번이 완성되었다.

 

그림_8.png

그림 7. 주제 목록 화면

 

오픈아이디를 이용한 사용자 인증#

지금부터는 사용자 인증 기능을 구현해보자. 역시나 여러 방법이 많겠지만, 가장 코딩량이 적고 빨리 적용해볼 수 있는 방법은 오픈아이디(OpenID)를 지원하는 것이다. 사용자들에도 별도의 가입절차가 필요 없는 오픈아이디가 편리할 것이다. 계속 그렇듯 필요한 라이브러리를 선택하자. ruby-openid 젬과 OpenIdAuthentication 플러그인을 사용한다. 참고로 이 플러그인은 필자가 스프링노트 서비스에서 개발한 소스 코드 일부를 추출해 공개한 것이다.

 

  1. prompt> sudo gem install ruby-openid
  2. prompt> script/plugin install svn://rubyforge.org/var/svn/springnote/plugins/open_id_authentication

 

오픈아이디 인증 과정에서 사용할 데이터베이스도 만들어준다.

 

  1. prompt> script/generate open_id_store_migration AddOpenIdStore
  2. prompt> rake db:migrate

 

사용자 데이터베이스도 필요하겠다.

 

  1. prompt> script/generate model User

 

생성된 마이그레이션 005_create_users.rb를 열어서 기존 테이블(subjects, codes)에도 user_id를 추가하자. 기존 데이터도 모두 특정 사용자 것으로 만들어주는 것이다.

 

  1.   def self.up
          create_table :users do |t|
            t.column :identity_url, :string
            t.timestamps
          end

          add_column :subjects, :user_id, :integer
          add_column :codes, :user_id, :integer

          execute "INSERT into users(identity_url) VALUES('')"   
          execute "UPDATE codes    SET user_id = (select max(id) from users)"
          execute "UPDATE subjects SET user_id = (select max(id) from users)"
      end

 

모델 쪽 준비는 모두 마쳤으니, 이제 로그인/로그아웃을 담당할 세션 컨트롤러를 만들어 보자. 마찬가지로 플러그인에서 제공하는 코드 생성기를 이용한다.

 

  1. prompt> script/generate open_id_consumer_controller session

 

설치 과정에 나오는 안내처럼, routes.rb를 열어 아래 코드를 추가해준다. begin과 compete는 오픈아이디 인증 과정에서 사용하는 액션이다.

 

  1. map.resource  :session, :collection => {:begin => :any, :complete => :any}

 

그 다음 application.rb를 열어서 아래 코드를 추가한다. 인증 라이브러리를 컨트롤러에서 사용할 수 있도록 추가하는 명령이다.

 

  1. class ApplicationController
  2.   include AuthenticatedSystem
  3. end

 

그리고 SessionsController를 열어서 다음 코드를 추가해야 한다.

 

  1.   def authentication_succeed(oidresp)
        self.current_user = User.find_or_create_by_identity_url(oidresp.identity_url)
        redirect_to '/'
      end

 

위 코드에서 정의하고 있는 authentication_succeed 메서드는 오픈 아이디 인증이 성공하면 호출되는 콜백 메서드다. 지금 로그인한 사용자에 해당하는 객체를 만들어 current_user로 설정한다. 그렇게 하면 세션에도 같은 정보가 담겨, 브라우저가 떠 있는 동안 로그인을 유지할 수 있다.

 

로그아웃은 어떻게 할까? 세션을 리셋해주면 된다. 로그아웃은 세션 리소스를 삭제하는 일이므로 SessionsController#destroy 메서드에 구현한다.

 

  1.   def destroy
        reset_session
        redirect_to '/'
      end

 

이제 화면을 만들 차례다. 상단 바(_menu.html.haml)에 로그인 전이라면 로그인 UI를, 로그인 후라면 현재 로그인된 사용자를 표시하고 로그아웃 링크를 출력하도록 수정하자.

 

  1.   - unless logged_in?
        - form_tag :controller => 'sessions', :action => 'begin' do
          = text_field_tag 'openid_identifier', '', :id => 'openid'
          = submit_tag '로그인'
      - else
        = link_to current_user.identity_url
        = link_to '로그아웃', session_path, :method => :delete

 

그림_10.png

그림 8. 로그인 전 화면

그림_11.png

그림 9. 로그인 후 화면

 

여기까지 작성한 내용은 72번 리비전으로 커밋되었다.

 

권한 관리#

인증의 단짝 친구는 권한 관리다. CodeTalks에서는 로그인하지 않는 사용자는 글을 쓸 수 없도록 제한해 보자. 코드는 무척 간단하다. codes_controller.rb를 열어서 다음 코드를 추가하기만 하면 된다.

 

  1. class CodesController
  2.   before_filter :login_required, :except => [:show, :index]
  3. end

 

현재 수행중인 액션이 show나 index(읽기라는 공통접이 있다)가 아니라면 로그인이 필요하다라고 선언하는 문장이다. 이처럼 액션에 대한 전처리, 후처리를 담당하는 기능이 필터다. 액션 전에 수행되는 before_filter는 조건을 검사하는데 유용하고, 후에 실행되는 after_fileter는 응답을 조작하는데(예를 들어 gzip 압축) 유용하다. 혹시 전,후 모두에서 할 일이 있다면 around_filter를 사용한다. 필터는 꽤 유용한 기능이지만, 남용한다면 디버깅을 힘들게 하고 성능에도 악영향을 줄 수 있으니 유의하자. 간단하지만 73번 리비전으로 커밋!

 

왼쪽 영역에는 루비 퀴즈를 표시한다.#

지금까지 완성된 화면을 보니 뭔가 허전한 부분이 있다. 사이드바(_sidebar.html.haml)가 비어있는 것이 아닌가. 이 곳에는 뭔가 유용한 정보를 담는 것이 좋겠다. 고민 끝에 루비 퀴즈(rubyquiz.com) 목록을 보여주기로 했다. 외부의 정보를 가져오고, 파싱하고, 재가공하는 일은 전통적인 언어들 보다 루비가 훨씬 더 잘 할 수 있는 영역이기도 하다. 실제 구현된 모습도 도움이 될 것 같다.

 

루비 퀴즈 사이트에서 제공하는 RSS를 분석하기 위해 hpricot 라이브러리를 사용한다. 이 라이브러리는 CSS 선택자, XPath 등을 사용할 수 있고, 루비의 동적인 특성을 잘 살리고 있어 루비 개발자들 사이에 인기가 많다. 어떤 곳에서는 이 라이브러리를 테스트 자동화에도 사용하고 있다고 한다.

 

다음 코드는 루비 퀴즈 클라이언트인 lib/ruby_quiz.rb의 내용이다.

 

  1. class RubyQuiz
      URL = 'http://rubyquiz.com/index.rss'
     
      def doc
        Hpricot.XML(open(URL))
      end
     
      def quizzes
        (doc/'channel/item').map do |el|
          [el/:title, el/:link].map(&:inner_html)
        end
      end
    end

 

quizzes 메서드를 보면 선택자와 루비의 map 메서드를 이용해 RSS에서 손쉽게 타이틀과 링크를 추출하는 모습을 확인할 수 있다. 이제 뷰에서 사용할 도우미 메서드를 만들자.

 

  1.   def ruby_quizzes
        RubyQuiz.new.quizzes
      end

 

그리고 사이드바에서는 이 메서드를 호출한다.

 

  1. %h3 Ruby Quizzes
    %ul
      - ruby_quizzes.each do |link|
        %li
          = link_to *link

 

그림_12.png

그림 10. 사이드바에 출력한 루비 퀴즈 목록

 

위 코드의 문제점은 매 요청마다 HTTP 호출을 해서 응답이 느려진다는 점이다. 이 때 필요한 것이 캐싱이다. 레일스의 캐싱 기능을 무척 잘 되어있으므로 직접 한번 구현해보면 좋겠다. 이 글을 작성하며 마지막으로 커밋한 코드는 74번 리비전이다.

 

매일 매일 나아지는 웹을 꿈꾸며#

지금까지 CodeTalks를 조금씩 발전시켜가며 완성해 보았다. 지금까지 구현한 기능만으로도 재미있는 서비스가 될 것 같다. 한가지 흥미로운 통계를 소개하겠다. 우리가 작성한 코드는 전부해서 고작 130줄이다. 적은 코드, 짧은 시간만으로 이렇게 많은 기능을 만들어낼 수 있다니 좋은 세상이다.

 

이 프로젝트는 계속 개선해갈 것이며, 실제 서버에 배치할 생각이다. 여러분이 이 글을 읽는 시점에는 사용해볼 수 있을 것이다. 지금 브라우저를 열어서 http://codetalks.net/에 접속해, 인상적인 코드 조각을 올려 함께 공유해보면 어떨까?

 

기능도 더 추가할텐데, 조만간 추가할 예정인 기능 중 하나는 코드와 함께 스펙(특히 RSpec)을 올려 코드에 대한 검증을 하는 것이다. 리팩토링을 하는데 스펙은 필수가 아니던가. 이건 여담이지만 레일스와 RSpec으로 행위 주도 개발(BDD)을 할 때 느낄 수 있는 리듬감도 전달하고 싶었는데, 하지 못해 약간 아쉽다. 스펙(또는 테스트)은 빠른 속도로 완성도 있는 서비스를 만들 수 있는 지름길인데 말이다.

 

그리고 한가지 더, 코드에 생명을 부여하겠다는 아이디어도 있다. 리소스를 기술하는 코드를 서비스에 올리면, 이 때부터 그 리소스는 CodeTalks에 보금자리를 마련하고 살아간다. 리소스가 살아있다는 의미는 완결된 작은 서비스로 다른 리소스에서 가져다 사용할 수 있는 상태임을 의미한다. 살아있는 리소스들이 많이 정의되면, 간단하게 리소스를 연결해주는 것만으로 새로운 매시업 서비스를 만들 수 있을 것이다. 처음에는 작은 코드를 올리는 것 뿐일지 모르지만, 이런 것들이 합쳐지면 눈덩이처럼 큰 가치를 만들 수도 있다. 그래서 하나의 파일에 담은 작은 레일스(박스 기사 참조)도 흥미롭고 보고 있고, 경량 프레임워크도 눈여겨 보고 있는 중이다.

 

글을 마치며 끝으로 하고 싶은 이야기는 지금 떠오르는 아이디어에 대한 구현을 당장 시작해보라는 것이다. 콘솔을 하나 열고 rails new_idea라고 입력하는 것만으로 이미 70%는 끝난 것이나 마찬가지다. 여기에 몇 시간만 더 투자한다면 충분히 잘 돌아가는 멋진 사이트를 만들어볼 수 있다. 이런 말을 하는 이유는 머릿속으로 가진 아이디어와 실제 눈 앞에 구현된 사이트의 차이가 엄청날 것이기 때문이다. 그리고 아이디어에 실체를 부여하는 일은 우리 개발자들이 가진 특권이기도 하다. 

 

앞으로도 레일스라는 좋은 도구를 이용해 번뜩이는 아이디어를 가진 서비스들이 많이 생겨나기를 바란다. 모든 웹 개발자가 행복해지는 그 날까지 오늘도 화이팅!

 

 

History

Last edited on 10/16/2008 01:08 by deepblue

Comments (0)

You must log in to leave a comment. Please sign in.