편집자 노트: 이 글은 Rolling with Ruby on Rails Revisited의 두번째 부분이며, Curt Hibbs의 유명한 Rolling with Ruby on Rails과 Rails and Rolling with Ruby on Rails Part 2의 수정본이다.
Paul: 피자 맛있네요. 감사합니다, 팀장님. 근데요, 팀장님이 저희에게 자세한건 나중에 알라고 말씀하신건 알겠는데요, 레일즈가 이걸 만들기 위해 생성한 코드를 보고 싶어 죽겠어요. 좀만 시간내서 CB에게 보여달라면 안될까요?
팀장: 폴, 사실은 나도 그거에 매우 흥미가 있네. CB 좀 보여주겠나? 하지만 지금은 아주 잠깐만, 어때?
CB: 문제 없습죠. ("지금은 쉽지", CB는 스스로 회상해보며 싱긋 웃었다. "당신은 후크를 잡아당기고 싶지 않을껍니다.") 레시피의 컨트롤러 코드를 잽싸게 한번 봅시다(그림 12). 레일즈는 스캐폴딩을 생성할때 준 인자를 이름으로 하는 컨트롤러 디렉토리 내의 파일에 컨트롤러 코드를 저장합니다. 지금의 경우에는 모델링과 컨트롤러를 생성하라고 했었죠; 둘다 레시피라는 이름이었고. 컨트롤러는 recipe_controller라는 이름을 가지게 됐습니다.
[그림 12] 레시피 컨트롤러
팀장: 정말 코드 적구만, 거의 다 알겠어. 내말은, 레일즈가 생성, 읽기, 수정, 삭제에 대한 코드를 생성하려 한다고 자네가 말했다는 거지. create 메소드나 update, destroy가 보이는데. show와 list 메소드가 읽기를 위한 거라는건 추측할 수 있겠는데, new와 edit 메소드는 뭣때문에 있는거지? 그리 쓸만한 일이 없을듯 한데...
CB: 그게 바로 진짜 웹기반 어플리케이션과 단지 스크립트가 있는 정적인 페이지들을 모아 놓은 사이트와의 차이죠. 스크립팅된 사이트에서는 방문자가 데이터를 입력할 HTML을 직접 코딩해 넣죠. 그리곤 그 폼이 반영될때 데이터를 저장할 생성 메소드를 서버 단에 만들겁니다. 기억하세요, 레일즈는 자신이 실행할 페이지들을 생성합니다. 그러므로 우리가 쓸 HTML 폼을 생성하는 거죠. new와 edit 메소드는 사용할 폼에 단지 오브젝트를 공급합니다. 폼이 반영되면 create와 update 메소드가 데이터베이스에 오브젝트를 저장하죠.
팀장: 좋아. 이제 기본은 이해한것 같네. 폴 이제 다시 시작할까?
폴: 물론이죠, 팀장님. 나중에 CB 만나서 더 자세히 듣겠습니다.
CB: 좋아요 그럼. 다시 하던걸로 돌아가죠. 눈치채셨겠지만 새 레시피 생성 페이지에는 카테고리를 할당할 수 있는 방법이 없었어요. 그건 제가 MySQL에게는 각 레시피가 하나의 카테고리를 가질꺼라고 알려줬지만, 레일즈에게는 알려주지 않았기 때문이죠. 이를 고치기 위해 할일은 단지 레일즈에게 그 둘의 관계를 알려주고 레시피 생성 페이지의 뷰 파일에 몇줄 추가하는 것 뿐입니다.
이제 레시피와 카테고리 모델에서 그 둘사이의 관계를 레일즈에게 알려주겠습니다. cookbook2appmodelscategory.rb 파일에서(그림 13), 각 카테고리는 레시피를 여러개 가질 수 있다고 한 줄 추가해 줍니다.
has_many :recipes
[그림 13] 카테고리 모델 파일
그리고는 레시피 모델(cookbook2appmodelsrecipe.rb, 그림 14)에 한줄 추가해 줌으로써 레일즈에게 각각의 레시피가 하나의 카테고리에 속함을 알려줍니다.
belongs_to :category
[그림 14] 레시피 모델 파일
이제 레일즈는 두 모델 사이의 관계를 알게 됐습니다.
방문자가 카테고리를 선택할 수 있게 하기 위해, 새 레시피 뷰에 몇줄 추가할 필요가 있습니다. 레시피 컨트롤러 내의 new 메소드의 뷰파일은 뷰 밑에 있는 레시피 하위 디렉토리에 있습니다. 뷰를 고치기 위해 cookbook2appviewsrecipe_form.rhtml을 엽니다(그림 15).
[그림 15] 레일즈가 생성한 폼 파셜
폴: CB, 그 디렉토리에는 몇개의 파일들이 있고 그중하나가 new.html이라는 것을 발견했습니다. 그 파일을 수정하지 않고 어떻게 할 수 있죠?
CB: 날카롭군요, 폴. 레일즈는 컨트롤러의 각 메소드에 대한 뷰파일을 생성합니다. 그러나 레일즈에서의 가이드 원리 중 하나가 "반복을 피하라."입니다. 레일즈 뷰는 뷰코드를 모듈화 시킵니다. new 메소드와 edit 메소드가 같은 오브젝트에 대해 작동하기 때문에 레일즈는 it _form.rhtml라는 이름의 파셜(partial)을 생성하고, new와 edit 뷰에서 사용합니다. 뷰는 코드의 반복을 피할 수 있는 만큼 많은 파셜을 가질 수 있습니다. 금방 뷰파일에 좀더 작업할껍니다.
첫 라인은 그냥 HTML 입니다. 두번째 줄을 루비 코드에요. 레일즈는 <% ... %>를 보고 그 안에 있는 것이 브라우저로 되돌려 보낼 HTML을 생성하기 위해 실행할 필요가 있는 내장된 루비 코드라는 것을 압니다. 이것은 = 표시를 가지고 있기 때문에 레일즈는 select 태그에 대해 HTML을 생성할껍니다. 만약 = 표시가 없었다면, 레일즈는 코드를 실행은 하겠지만 아무런 HTML도 생성하지 않을껍니다. 새 레시피 만들기 페이지는 방문자가 날짜를 선택하게 되어 있는 것을 눈치채셨을 텐데요, 요구서에는 반드시 시스템이 날짜를 부여해야 한다고 돼있죠. 방문자가 날짜를 선택할 수 있게 만드는 코드 라인을 제거하겠습니다. 이제 그림 16과 같은 폼 파셜을 가지게 됐습니다.
[그림 16] 수정된 폼 파셜
CB: 이제 누군가 레시피를 생성하거나 수정했을때 날짜를 부여하는 시스템을 가질 필요가 있습니다. 이건 레시피 컨트롤러에서 일어날 필요가 있죠, 그러므로 cookbook2appcontrollersrecipe_controller.rb 파일을 열고(그림 17) create와 update 메소드 둘다 한줄 추가해 줍니다.
ecipe.date = Time.now
[그림 17] 컨트롤러에서 날짜 부여
됐습니다!
CB: 좋습니다. 방금 코드 여섯줄을 추가했죠. 우리가 한 작업을 확인할 시간입니다. 모델 파일을 고쳤기 때문에 웹서버 mongrel을 재시작 해야 합니다. 성능적인 이유로, 개발 모드에서 조차 레일즈는 시작시에만 어플리케이션 모델 파일을 읽어들입니다. 레일즈 모델을 수정할땐 언제나 웹서버를 재시작할 필요가 있습니다. Mongrel을 멈추려면, 커맨드 창에서 그냥 Ctrl-C을 칩니다. 그리곤, 재시작을 하죠, 이렇게...
mongrel_rails start
CB: 좋아요. 한번 해보시죠? 브라우저에 http://localhost:3000/recipe/new라고 입력하면...
좋아요! 날짜는 사라지고 카테고리에 대한 드롭다운 리스트가 생겼습니다.
폴: 컨트롤러에서 대부분의 참조는 @recipe라는 이름을 가진 변수였습니다. 근데 이 폼에서는 좀 다른 문형을 사용해서 같은 오브젝트를 참조하는 것 처럼 보이네요.
CB: 당신 말이 맞습니다. 뷰에서 사용하는 text_field 문장은 다른 문형을 사용하죠. 그러나 인스턴스 변수를 가지고 컨트롤러가 사용하는 정확히 같은 오브젝트를 참조합니다. 처음에는 혼란스러울 수도 있습니다. 더 많은 정확하고 자세한 설명들이 있겠지만, 제가 처음 시작했을때 어떻게 직관적으로 이해했는지 알려드릴께요. 사용자에게 무엇인가를 보내기 위해 인스턴스 변수를 사용합니다. 컨트롤러 내에서 생성한 어떠한 인스턴스 변수도 방문자의 브라우저로 돌려보낼 HTML 파일을 생성하는데 사용하는 뷰에서 자동적으로 이용가능합니다. 방문자로부터 정보를 얻을 필요가 있을때 파라미터 해시로 알려진 것에서 되돌아 올껍니다. 그것은 방문자의 브라우저에 만들어져 있는 데이터 구조이며 새 요청-응답 주기가 시작될때 되돌려 보내집니다. 예를 들어 text_field 문형은 실제로 폼의 밖에서 데이터를 어떻게 얻어내는지 기억하게 도와줬습니다. 만약 <%= text_field "recipe," "description," %>같은 폼에서 text_field를 가진다면 @recipe.description = params[:recipe][:description]로 컨트롤러에서 접근할 수 있을것이고, 아니면 컨트롤러에서 @recipe = Recipe.find(the_one_I_want)를 사용해서 원한다면 다른 주기에 돌려보낼 수도 있습니다. 그리고는 뷰에서는 <%= @recipe.description %>을 사용하는거죠. 물론 이것보다 많은 설명들이 있겠지만 초기에 저에게는 이것이 직관적으로 생각할 수 있게 도움이 됐습니다.
[그림 18] 수정된 레시피 생성 페이지
계속해서 새 레시피를 하나 생성해 보세요. 피자를 막 갖다 주셨으니깐 레시피는 마음속에 신선하게 있을껍니다 ;-)
[그림 19] 첫번째 레시피!
CB: 놀라실 준비되셨나요?
팀장: 음, 물론일세. 하지만 아직 뭔가 원래의 것처럼 보이질 않네. 지금 조금만 고칠 수 있겠나?
CB: 식은죽 먹기죠! 주의가 필요한 것들이 좀 있군요. 레시피 화면에 경계선이 필요해요. Title 컬럼 헤딩을 바꿀 필요가 있네요, 그리고 테이블은 각각의 레시피 카테고리를 표시하는 컬럼을 가질 필요가 있구요. 요구서에 있는 컨설턴트의 리스트에는 레시피의 카테고리를 클릭하면 해당 카테고리의 레시피들만 리스트를 정렬해서 보여주게 되어 있는데요. 이건 각 레시피의 카테고리에 어떤 종류의 링크가 필요하다는 걸 의미하죠. 또 스크린샷에는 리스트에는 소개말이 나오지 않게끔 되어 있습니다. 요구서에는 레시피 이름을 클릭해서 수정을 할 수 있게도 돼 있네요. 마지막으로 스크린샷에는 레시피에 대한 보기와 삭제 링크가 있습니다.
팀장: 그거 많은 작업이 필요한것 처럼 들리는데!
CB: 어떤 플랫폼들에서는 많은 작업이 필요하죠. 레일즈에서는 아닙니다. 이걸 보세요. cookbook2appviewsrecipeslist.rhtml 파일을 엽니다(그림 20). 기본적으로 이것이 하고 있는 것은 레시피 모델을 읽고 각각의 속성에 대한 컬럼을 가진 테이블을 생성하는 것이죠. 속성명을 가진 각각의 컬럼에는 테이블 헤더를 사용하고, 레시피 테이블에 각 레코드에 대한 열을 생성합니다.
CB: 좋아요. 제 계산으로는 방금 일곱줄의 코드를 추가 했죠. 작업을 체크해보는게 더 좋겠어요 ;-) 팀장님, 브라우저 좀 새로고침 해주실래요?
CB: 이봐요 폴. 인스턴스 변수와 파라미터에 관해 했던 말을 기억해요? for 루프에서 인트턴스 변수인 @recipes를 어떻게 사용했는지 보이나요? 이것의 값은 컨트롤러에서 정해집니다. 그리고 지금 그것을 뷰에서 방문자에게 되돌려 보내는 페이지에서 보여주는 레시피 리스트를 생성하기 위해 사용하고 있습니다.
[그림 21] 보기 좋게 만들기
팀장: 점점 비슷하게 보여가는군 (그림 21).
CB: 확실히 그렇군요. 하지만 레시피 카테고리를 클릭했을때 필터링이 제대로 작동하는지 테스트해야 하는걸 기억할 필요가 있습니다. 우린 하나의 카테고리만 입력했었기때문에 아직 테스트할 수는 없습니다. 지금은 뷰코드에 있으니깐 그걸 끝내도록 하겠습니다.
요구서에는 모든 페이지에 레시피 리스트나 카테고리 리스트로 갈 수 있는 푸터를 넣어달라고 하더군요. 윈도우 타이틀과 페이지 제목도 모든 페이지에서 다 똑같아야 하구요. 지금 그것들을 다 추가해보죠.
cookbook2appviewslayouts 디렉토리에는 category.rhtml 과 recipe.rhtml 이렇게 두개의 파일이 있습니다. 이중에 하나를 지우고 나머지 하나의 이름을 application.rhtml로 변경할께요. 이제 application.rhtml를 엽니다(그림 22).
[그림 22] 어플리케이션 레이아웃 파일 작업
CB: 먼저 윈도우 타이틀을 바꿀께요...
Online Cookbook
그리고 페이지 제목을 추가하겠습니다...
Online Cookbook
레일즈는 여기서 발견하는 것들을 어플리케이션의 모든 페이지에 적용합니다. 그러니깐 여기에 단지 몇 줄만 추가하면 모든 페이지에 풋터가 추가되는거죠.
<% if params[:controller] == "recipe" %>
<%= link_to "Create new recipe", :controller => "recipe", :action => "new" %>
<% else %>
<%= link_to "Create new category", :controller => "category", :action => "new" %>
<% end %>
<%= link_to "Show all recipes", :controller => "recipe", :action => "list" %>
<%= link_to "Show all categories", :controller => "category", :action => "list" %>
[그림 23]처럼 됩니다.
[그림 23] 수정된 어플리케이션 레이아웃 파일
저장했습니다. 됐어요, 팀장님. 우리는 라인의 내용을 바꿨구요, 제 후한 접근방법으로 (변경된 라인수도 세면서), 코드 일곱줄을 추가했습니다. 진행상황을 다시 체크해 볼까요?
팀장: 물론. 브라우저를 새로고침 해보면... (그림 24).
[그림 24] 브라우저에서의 어플리케이션 차원 결과
팀장: 이런!
폴: 아까는 비주얼을 고치기 위해 뷰와 파셜(partial)을 사용했었잖아요. 근데 지금은 레이아웃을 사용하네요. 거기에 대해 설명 좀 해주실래요?
CB: 네. 레이아웃은 뷰 코드에 DRY 원리를 쉽게 적용시키기 위해 레일즈가 제공하는 메커니즘입니다. 모델/컨트롤러 쌍에 대한 스캐폴딩을 생성하면, 레일즈는 우리가 컨트롤러 내의 메소드에 의해 렌더링되는 페이지들에 공통된 룩앤필을 원한다고 가정합니다. 레이아웃은 뷰를 "감싸게" 합니다. 기본적으로 레일즈는 페이지를 생성할 필요가 있을때, 메소드와 같은 이름을 가진 뷰파일을 찾습니다. 그리고는 컨트롤러와 같은 이름을 가진 레이아웃 파일에서 정보를 더 얻어내구요. 해당 레이아웃 파일이 존재하지 않으면 application.rhtml 파일을 찾습니다. 파셜과 뷰와 레이아웃의 조합으로 레일즈는 메소드 수준에서, 컨트롤러 수준에서 그리고 어플리케이션 수준에서 방문자에게 보여지는 것들을 쉽게 제어할 수 있게 해줍니다.
CB: 네. 근데 아직 필터링을 테스트하지 않았단 걸 기억하세요. 이제 그걸 해보겠습니다. 그렇게 하기 위해 먼저 또다른 카테고리를 하나 추가하도록 하죠. 모든 카테고리 보기 버튼을 눌러주세요(그림 25).
[그림 25] 카테고리 리스트 초기화면
CB: 정리할게 좀 있네요. 레시피에서 했던 것들 처럼요. 하지만 그렇게 하기전에 먼저 동작을 체크해 보죠. 새 카테고리를 추가하기 위해 링크를 눌렀을때 어떻게 되나 볼까요. 링크들은 잘 작동할껍니다. (그림 26)
[그림 26] 새 카테고리 추가 페이지
여기도 정리할게 똑같이 있네요. 하지만 일단은 포인트에서 벗어나지 말고 계속 진행하죠, "beverages"라는 이름의 카테고리를 추가하겠습니다 (그림 27).
[그림 27] 리스트에 새로 추가된 카테고리
좋습니다. 새 카테고리 기능은 제대로 작동하네요, 이제 화면을 좀 정리해 볼까요. 카테고리 리스트 페이지가 어떻게 보여야 하는지에 대한 스크린 샷이 없기 때문에 그냥 레시피 리스트와 비슷하다고 가정하겠습니다. 계속해서 변경해 봅시다.
cookbook2appviewscategorylist.rhtml 파일을 수정하겠습니다. 레시피의 리스트 뷰와 매우 비슷해 보이네요. 아까 했던것과 본질적으로 같은 변경을 하겠습니다.
제목을 지웁니다.
테이블 태그에 경계선을 추가합니다.
이번에는 테이블 제목 하나는 남겨두겠습니다. 하나밖에 없고, 그게 나아 보이니까요. 즉 단지 테이블 로우를 생성하는 코드만 교체할 필요가 있단 뜻입니다.
팀장: 자네가 "레일즈가 가능케 하는 것들 중 하나는 반복적이고 점진적인 개발이다."라고 말했던 것을 정말로 알겠네. 보통의 프로젝트에서는 고객의 피드백을 받기전에는 많이 진행될 수가 없지. 그들이 가까이 머물러 있지 않으면 꽤 빠르게 해낼 수가 없는걸.
CB: 맞습니다.
viewscategories와 viewsrecipes 양쪽 서브디렉토리 밑에 있는 new.rhtml과 edit.rhtml 파일들이
제목 줄도 지워져야 하는 걸로 기억하는데요. 빨리 진행하겠습니다.
근데요, 팀장님. 이 모든걸 다 타이핑하니깐 제 손가락이 정말로 아프네요 ;-) 이제 우리는 하나 이상의 카테고리를 가지고 있고, 레시피를 추가하거나 수정했을때 카테고리 선택동작이 확실하게 되는지를 볼 필요가 있습니다. 이것 좀 해주시면 감사하겠는데요. 모든 레시피보기 링크를 클릭해주세요. 그리고 새 레시피 만들기 링크를 클릭하세요. 새 레시피를 입력하고 beverages 카테고리로 할당하세요. 적어도 main course 하나랑 beverage 하나가 필요합니다(그림 30).
[그림 30] 서로다른 카테고리를 가진 레시피 리스트
CB: 이제 남은건 레시피의 카테고리를 클릭했을때 필터링되는 것 뿐인듯 하네요. 레시피 리스트 뷰를 고칠때 필터링 하기 위한 링크를 만들었었죠, 하지만 아직 동작을 지정하진 않았습니다. 그건 레시피 컨트롤러에서 돌아가니까, cookbook2appcontrollersrecipe.rb 파일을 열고 다음 코드와 같이 리스트 메소드의 내용을 바꿉니다:
됐습니다, 저장하구요. 좋아요. 코드 여섯줄을 더 추가했네요. 이게 뭘 의미하는지 아시겠죠 ;-)
팀장: 나도 리듬을 타고 있는 것 같네 ;-) 어디보자. 브라우저를 새로고침하고 beverages 링크를 클릭한다(그림 31).
[그림 31] 하나의 카테고리에 대해 필터링된 레시피 리스트
CB: 좋아요. 이제 모든 레시피 보기 링크를 누르면(그림 32).
[그림 32] 거의 다 됐다!
CB: 이런, 팀장님!!! 만약 제가 더 잘 알지 못했다면, 우리 일이 다 끝났다고 생각했을껍니다!
팀장: 물론 그렇게 보이는데!
CB: 그냥보기엔 물론 그렇죠, 하지만 좀 더 철저하게 봐 보세요. 힌트: 모든 카테고리 보기 링크를 클릭하고, Beverages 카테고리에 있는 (delete) 링크를 클릭한다.
팀장: 이봐! 이건 아니잖아! (그림 33)
[그림 33] 레일즈 에러 메시지
CB: 네. 우리가 입력했던 첫 라인 기억나세요? 카테고리 모델 파일인 category.rb 파일이었죠.
has_many :recipes
에러메시지를 보면 자식을 가진 레코드를 삭제하려 했다고 나와 있습니다. 레일즈는 여러개의 레시피가 우연치 않게 삭제되는걸 막기 위해 멈춘겁니다. 이것을 처리하는 방법은 여러가지가 있겠지만 요구서에는 이런 상황이나 대처법에 관해서는 안 나와 있군요. 아시다시피, 이건 정말로 고객이 결정할 일이죠. 지금은 그냥 어플리케이션이 충돌이 나지 않게만 고치겠습니다. 팀장님이 내일 부장님께 가서 어떻게 작동하길 원하시는지 여쭈어 보세요. 자식 레코드가 있을땐 요청을 무시하도록 코드 몇줄을 추가할께요. 이 코드는 카테고리 컨트롤러 파일(cookbook2appcontrollerscategory_controller.rb)에 있는 destroy 메소드에서 실행됩니다. 먼저 삭제하려하는 카테고리에 할당된 모든 레시피를 찾습니다.
그리고는 레일즈가 아무 레코드라도 발견하면 삭제가 일어나지 않게 if문에서 레코드 삭제를 감싸주겠습니다.
if recipes.empty?
Category.find(params[:id]).destroy
endAnd we"re done.
그리고 컨트롤러 파일을 저장하면...(그림 34).
[그림 34] 에러를 고치기 위해 수저된 destroy 메소드
팀장: 그러면 두줄이 더 추가된거지! 우리의 작업을 다시 확인해 볼 시간이군 ;-) 브라우저를 새로고침하고. 레시피 리스트로 돌아가 ice tea 레시피를 삭제한다. 이제 카테고리 리스트로 돌아가 beverages 카테고리를 삭제해 보면. (그림 35).
[그림 35] 휴스턴, 우리는 점심을 먹는다!
팀장: 다 끝났군!!!
CB: 뭐라구요?!?!?!? 그말은 옳지 않아요! 피자는 아직 채 식지도 않았는걸요 ;-)
하지만, 진지하게는 팀장님 말이 맞습니다. 우리는 방금 레일즈를 사용해서 처음으로 완전 기능의 웹기반에, 데이터베이스 주도의 웹 어플리케이션을 만든겁니다. 얼마나 걸렸죠? 삼사십분 걸렸나요? 키보드 입력이요? 제 계산으로는:
팀장: 맞아. 이거 굉장해. 자네 본부 사람들에 대해 도움이 필요하다고 했지? 내가 도와주지. 그리고 내일 아침에도 부장님께 말씀 드리겠네. 사용하길 바라는 말들이 있나?
CB: 정말요? 이것을 말씀드리세요.
레일즈는 웹프로그래밍에 있어서 차세대 방법입니다. 그리고 레일즈를 사용하는 개발자들은 그것을 사용하지 않는 사람들보다 더 빨리 웹 어플리케이션을 만들껍니다. 루비 온 레일즈는 소프트웨어 개발을 단순하게 만듭니다. 빠르게 만듭니다. 그리고 재미있게 만듭니다. 그리고 가장 좋은건? 레일즈는 지금 당장 무료로 이용가능하고 오픈소스의 MIT 라이센스하에 있습니다.
팀장: 알겠네. 그리고 지금 당장 본부에 전화걸어주지. 내가 자네에게 필요한 다음 것들은 레일즈에 관한 정보들과 그걸 어떻게 배우나일세. 다시한번 고맙네. 분명 피자 가격만한 가치가 있었어.