레일스 라우트

많은 레일스 초보자들이 궁금해 하는 레일스 라우팅의 비밀을 설명합니다.

얼마 전 페일스북 그룹에 올라 온 질문을 보고 초보자들에게 흔히 있을 수 있는 궁금증이라고 생각하여 이 글을 작성한다.

웹서비스는 라우팅으로부터 시작한다고 해도 과언이 아닐 것이다.

특히나 레일스도 REST형식의 라우팅을 지원한다. 레일스는 외부로부터 서버로 들어오는 URI 요청을 파싱하여 어떤 컨트롤러를 호출할 것인지 정하게 되는데, 해당 컨트롤러의 특정 액션을 구체적으로 호출한 후 액션명과 동일한 뷰 템플릿 파일(정확히는, [action-name].html.erb)을 app/views/[controlle-name]/ 디렉토리에서 찾아 페이지를 렌더링한 후 요청결과로써 클라이언트에게 발송하게 된다.

이와 같은 요청/응답의 과정은 레일스 프로젝트의 COC 원칙 중의 대표적인 사례다.

효율적인 설명을 위해서 간단한 토이 프로젝트를 작성할 것이다.
아래와 같이 routes_test라는 프로젝트 생성한다.

$ rails new routes_test
Using -d postgresql from /Users/[account-name]/.railsrc
      create
      create  README.md
      create  Rakefile
      create  config.ru
      create  .gitignore
      create  Gemfile
         run  git init from "."
Initialized empty Git repository in /Users/hyo/prj/r5/routes_test/.git/
      create  app
      create  app/assets/config/manifest.js
      create  app/assets/javascripts/application.js
      create  app/assets/javascripts/cable.js
      create  app/assets/stylesheets/application.css
      create  app/channels/application_cable/channel.rb
      create  app/channels/application_cable/connection.rb
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/jobs/application_job.rb
      create  app/mailers/application_mailer.rb
      create  app/models/application_record.rb
      create  app/views/layouts/application.html.erb
      create  app/views/layouts/mailer.html.erb
      create  app/views/layouts/mailer.text.erb
      create  app/assets/images/.keep
      create  app/assets/javascripts/channels
      create  app/assets/javascripts/channels/.keep
      create  app/controllers/concerns/.keep
      create  app/models/concerns/.keep
      create  bin
      create  bin/bundle
      create  bin/rails
      create  bin/rake
      create  bin/setup
      create  bin/update
      create  bin/yarn
      create  config
      create  config/routes.rb
      create  config/application.rb
      create  config/environment.rb
      create  config/secrets.yml
      create  config/cable.yml
      create  config/puma.rb
      create  config/spring.rb
      create  config/environments
      create  config/environments/development.rb
      create  config/environments/production.rb
      create  config/environments/test.rb
      create  config/initializers
      create  config/initializers/application_controller_renderer.rb
      create  config/initializers/assets.rb
      create  config/initializers/backtrace_silencers.rb
      create  config/initializers/cookies_serializer.rb
      create  config/initializers/cors.rb
      create  config/initializers/filter_parameter_logging.rb
      create  config/initializers/inflections.rb
      create  config/initializers/mime_types.rb
      create  config/initializers/new_framework_defaults_5_1.rb
      create  config/initializers/wrap_parameters.rb
      create  config/locales
      create  config/locales/en.yml
      create  config/boot.rb
      create  config/database.yml
      create  db
      create  db/seeds.rb
      create  lib
      create  lib/tasks
      create  lib/tasks/.keep
      create  lib/assets
      create  lib/assets/.keep
      create  log
      create  log/.keep
      create  public
      create  public/404.html
      create  public/422.html
      create  public/500.html
      create  public/apple-touch-icon-precomposed.png
      create  public/apple-touch-icon.png
      create  public/favicon.ico
      create  public/robots.txt
      create  test/fixtures
      create  test/fixtures/.keep
      create  test/fixtures/files
      create  test/fixtures/files/.keep
      create  test/controllers
      create  test/controllers/.keep
      create  test/mailers
      create  test/mailers/.keep
      create  test/models
      create  test/models/.keep
      create  test/helpers
      create  test/helpers/.keep
      create  test/integration
      create  test/integration/.keep
      create  test/test_helper.rb
      create  test/system
      create  test/system/.keep
      create  test/application_system_test_case.rb
      create  tmp
      create  tmp/.keep
      create  tmp/cache
      create  tmp/cache/assets
      create  vendor
      create  vendor/.keep
      create  package.json
      remove  config/initializers/cors.rb
      remove  config/initializers/new_framework_defaults_5_1.rb
         run  bundle install
Fetching gem metadata from https://rubygems.org/.........
Fetching version metadata from https://rubygems.org/..
Fetching dependency metadata from https://rubygems.org/.
Resolving dependencies...
Using rake 12.0.0
Using concurrent-ruby 1.0.5
Using i18n 0.8.1
Using minitest 5.10.2
Using thread_safe 0.3.6
Using builder 3.2.3
Using erubi 1.6.0
Using mini_portile2 2.1.0
Using rack 2.0.2
Using nio4r 2.0.0
Using websocket-extensions 0.1.2
Using mime-types-data 3.2016.0521
Using arel 8.0.0
Using public_suffix 2.0.5
Using bindex 0.5.0
Using bundler 1.13.6
Using byebug 9.0.6
Using ffi 1.9.18
Using coffee-script-source 1.12.2
Using execjs 2.7.0
Using method_source 0.8.2
Using thor 0.19.4
Using multi_json 1.12.1
Using rb-fsevent 0.9.8
Using ruby_dep 1.5.0
Using pg 0.20.0
Using puma 3.8.2
Using rubyzip 1.2.1
Using sass 3.4.23
Using tilt 2.0.7
Using websocket 1.2.4
Using turbolinks-source 5.0.3
Using tzinfo 1.2.3
Using nokogiri 1.7.2
Using rack-test 0.6.3
Using sprockets 3.7.1
Using websocket-driver 0.6.5
Using mime-types 3.1
Using addressable 2.5.1
Using childprocess 0.7.0
Using rb-inotify 0.9.8
Using coffee-script 2.4.1
Using uglifier 3.2.0
Using turbolinks 5.0.1
Using activesupport 5.1.1
Using loofah 2.0.3
Using xpath 2.0.0
Using mail 2.6.5
Using selenium-webdriver 3.4.0
Using listen 3.1.5
Using rails-dom-testing 2.0.3
Using globalid 0.4.0
Using activemodel 5.1.1
Using jbuilder 2.6.4
Using spring 2.0.1
Using rails-html-sanitizer 1.0.3
Installing capybara 2.14.0
Using activejob 5.1.1
Using activerecord 5.1.1
Using spring-watcher-listen 2.0.1
Using actionview 5.1.1
Using actionpack 5.1.1
Using actioncable 5.1.1
Using actionmailer 5.1.1
Using railties 5.1.1
Using sprockets-rails 3.2.0
Using coffee-rails 4.2.1
Using web-console 3.5.1
Using rails 5.1.1
Using sass-rails 5.0.6
Bundle complete! 16 Gemfile dependencies, 70 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.
         run  bundle exec spring binstub --all
* bin/rake: spring inserted
* bin/rails: spring inserted

참고로 개발환경은 루비 2.4.0, 레일스 5.1.1 버전이다.
한가지 특이한 것은 레일스 프로젝트가 생성되면 자동으로 Git 초기화가 된다는 것이다. 이전에는 직접 해주어야 했었다.
또 하나는, 레일스 명령설정 파일을 사용했다는 것이다. (두번째 줄)

...
Using -d postgresql from /Users/[account-name]/.railsrc
...

.railsrc 파일은 rails new 명령을 실행할 때 추가로 사용하는 옵션을 저장해 두는 곳이다.
예를 들어 레일스 프로젝트를 생성할 때 디폴트로 sqlite3 데이타베이스를 사용하게 된다. 최근 레일스 프로젝트에서는 Postgresql 데이터베이스를 사용하는 추세다. 그래서 저자는 좀 더 이 데이터베이스와 친밀하게 지내기 위해서 기본 데이터베이스를 Postgresql로 변경하기 위해 아래와 같이 ~/.railsrc 파일을 작성했다.

-d postgresql

이런 연유로 위의 명령 초기에 .railsrc 파일이 호출된 것이다. 이야기가 옆으로 샌 듯하다. 다시 본론으로 돌아가자.

다음으로 posts 리소스를 scaffold 제너레이터를 이용하여 생성한다.

$ rails generate scaffold Post title:string content:text
Running via Spring preloader in process 69599
      invoke  active_record
      create    db/migrate/20170515104734_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
      create      test/models/post_test.rb
      create      test/fixtures/posts.yml
      invoke  resource_route
       route    resources :posts
      invoke  scaffold_controller
      create    app/controllers/posts_controller.rb
      invoke    erb
      create      app/views/posts
      create      app/views/posts/index.html.erb
      create      app/views/posts/edit.html.erb
      create      app/views/posts/show.html.erb
      create      app/views/posts/new.html.erb
      create      app/views/posts/_form.html.erb
      invoke    test_unit
      create      test/controllers/posts_controller_test.rb
      invoke    helper
      create      app/helpers/posts_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/posts/index.json.jbuilder
      create      app/views/posts/show.json.jbuilder
      create      app/views/posts/_post.json.jbuilder
      invoke  test_unit
      create    test/system/posts_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/posts.coffee
      invoke    scss
      create      app/assets/stylesheets/posts.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss

위에서 생성된 마이그레이션 파일을 이용하여 데이터베이스를 마이그레이트(실제 테이블생성)한다.

$ rails db:create
$ rails db:migrate

위에서 db:create 는 Postgresql 데이터베이스를 생성하는 명령이므로 프로젝트에 대해서 한번만 실행하면 된다.
그리고 아래와 같이 명령을 실행한다.

$ rails routes -c posts
   Prefix Verb   URI Pattern               Controller#Action
    posts GET    /posts(.:format)          posts#index
          POST   /posts(.:format)          posts#create
 new_post GET    /posts/new(.:format)      posts#new
edit_post GET    /posts/:id/edit(.:format) posts#edit
     post GET    /posts/:id(.:format)      posts#show
          PATCH  /posts/:id(.:format)      posts#update
          PUT    /posts/:id(.:format)      posts#update
          DELETE /posts/:id(.:format)      posts#destroyrails routes -c posts

위에서 사용한 -c 옵션은 특정 컨트롤러의 라우트만 보고 싶을 때 사용하면 된다. 따라서 위에서는 posts 컨트롤러에 대한 라우트만 출력하여 보여 준다.

Prefix, Verb, URI Pattern, Controller#Action와 같이 4개의 컬럼 항목들이 출력되는데, 각각에 대해서 간략하게 설명한다.

1. Prefix

레일스에서는 두가지 종류의 경로 헬퍼메소드를 지원한다. _path_url. 즉, 이와 같은 경로 헬퍼 앞에 붙여서 사용하는 목적(접두어)으로 이용하기 때문에 이와 같은 이름이 붙여진 것이다. posts 라는 prefix를 예를 들어 설명해 보자. 뷰 파일에서 사용할 때는 posts_path 또는 posts_url 와 같이 사용하게 된다. 전자는 상대경로, 후자는 절대경로를 표시한다는 정도로 알아두면 된다.

2. Verb

실제로 요청은 두가지 항목이 조합되어 이루어 진다. 하나는 이미 언급한 경로이고 다른 하나는 HTTP 프로토콜의 메소드(Verb)이다. 디폴트 메소드는 GET이다. 따라서 경로만 주어지고 별다른 메소드 언급이 없다면 GET 메소드가 호출되는 것이다.

3. URI Pattern

외부로부터 들어오는 경로명의 패턴을 언급하는 말이다. URL에서 통신프로토콜명, 도메인명, 그리고 끝에 붙을 수 있는 각종 쿼리옵션들을 뺀 나머지를 URI라고 한다.

4. Controller#Action

이와 같이, 외부로부터 들어오는 URI PatternVerb의 조합으로 컨트롤러와 액션이 특정되는 것이다.
예를 들면, /posts 라는 URI PatternVerb 의 종류에 따라 즉, GET 또는 POST냐에 따라 각기 다른 액션을 호출하게 되는 것을 확인할 수 있다. 실제로 이런 상황을 확인하기 위해서 레일스 컨솔창을 열고 아래와 같이 명령을 실행해 보자.

$ rails console
Running via Spring preloader in process 70885
Loading development environment (Rails 5.1.1)
>> app.posts_path
=> "/posts"
>> app.post_path(:id)
=> "/posts/id"
>> app.new_post_url
=> "http://www.example.com/posts/new"
>> app.edit_post_url(:id)
=> "http://www.example.com/posts/id/edit"

위의 결과에서 _path_url 헬퍼의 차이점을 확연하게 구분할 수 있을 것이다.
그렇다면 동일한 경로명에 Verb는 어떻게 구분해서 지정할 수 있을까 의문이 든다.
다음을 보자.

>> helper.link_to("List", app.post_path(:id), method: :get)
=> "List"
>> helper.link_to("List", app.post_path(:id), method: :patch)
=> "List"
>> helper.link_to("List", app.post_path(:id), method: :put)
=> "List"
>> helper.link_to("List", app.post_path(:id), method: :delete)
=> "List"

레일스 콘솔에서와 실제로 뷰 템플릿 파일에서 사용할 때는 약간의 차이가 있다. 헬퍼 메소드란 원래 뷰 템플릿 파일에서 사용하기 위한 파일이라서 실제로 뷰 템플릿 파일에서 사용할 때는helper.라든지 app.와 같은 접두어를 사용할 필요가 없다.

위의 코드를 실제로 뷰 파일에서 사용해 보자.

1. GET 메소드

app/views/posts/index.html.erb 파일을 열고 19번째 코드라인을 보자.

<%= link_to 'Show', post %>

사실 위의 erb 코드는 축약형이다.

<%= link_to 'Show', post_path(post), method: :get %>

위의 코드를 추가한 후 로컬 서버를 실행한다.

$ rails server

그리고 브라우저에서 http://localhost:3000/posts 로 접근을 시도하고 페이지 하단의 New Post 링크를 클릭하여 글을 하나 추가한다. 글 내용을 달라도 아래와 같은 페이지 화면이 보이게 될 것이다.

img-alternative-text

2. POST 메소드

http://localhost:3000/posts 페이지로 접근한 후 하단의 New Post 링크를 클릭하여 입력 폼으로 이동하자. 그리고 마우스 우측버튼을 클릭하여 소스보기를 하면 다음과 같이 보일 것이다.

img-alternative-text

따라서 데이터를 입력한 후 제출하면 /posts URI 패턴이 POST 메소드로 호출되어 결국 posts 컨트롤러의 create 액션이 실행된 후 show 액션으로 리디렉트(redirect_to @post)된다. 다음은 posts 컨트롤러의 create 액션부분의 코드베이스다.

  def create
    @post = Post.new(post_params)

    respond_to do "format
      if @post.save
        format.html { redirect_to @post, notice: 'Post was successfully created.' }
        format.json { render :show, status: :created, location: @post }
      else
        format.html { render :new }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

3. PATCH 메소드

posts 인데스 페이지에서 이미 생성된 postEdit 링크를 클릭하여 편집 페이지로 이동하자. 마우스 우측 클릭하여 소스보기를 하면 다음과 같이 보일 것이다.

img-alternative-text

이제 수정 내용을 제출하면 /posts/1 URI 가 POST 메소드로 호출되지만 자세히 보면 hidden 속성값으로 PATCH 값도 함께 제출되는 것을 알 수 있다. 이것은 PATCH 메소드가 브라우저에서 GET, POST와 같은 HTTP 메소드처럼 작동하지 않기 때문에 레일스에서 내부적으로 사용하기 위한 꽁수로 이해하면 된다.

라우팅 테이블을 보면 PATCH 메소드로 /posts/:id URI 패턴이 호출될 경우 posts 컨트롤러의 update 액션이 호출되고 create 액션과 동일하게 리디렉트된다.

  def update
    respond_to do "format
      if @post.update(post_params)
        format.html { redirect_to @post, notice: 'Post was successfully updated.' }
        format.json { render :show, status: :ok, location: @post }
      else
        format.html { render :edit }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

4. PUT 메소드

PUT 메소드는 PATCH 메소드와 비슷한 기능(속성값을 업데이트)을 하지만 차이점이 있다. PUT 메소드를 사용할 경우에는 해당 모델의 모든 속성값을 지정해 주어야 한다는 것이다. 한편 PATCH 메소드를 사용할 때는 원하는 속성값만 업데이트할 수 있다.

그렇다면 뒤늦게 PATCH 메소드가 추가된 이유는 뭘까. PUT 메소드를 사용할 때는 일부 속성만 변경할 때라도 모든 속성값을 넘겨 주어야 한다는 것이다.
이로써 불필요한 데이터 전송 대역폭을 사용해야하는 단점을 보완하기 위해서 PATCH 메소드가 추가된 것이다. 일부 속성값만 전송하게 되어 데이터 전송대역폭을 줄일 수 있는 장점을 가지게 된 것이다.

5. DELETE 메소드

이 메소드 역시 HTTP 표준 메소드가 아니다. 따라서 동일한 URI에 DELETE 메소드를 지정해서 호출하여 posts 컨틀로러의 destroy 액션이 호출되도록 한다.

<%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %>

이 또한 축약형으로 사용할 것이다. 이것은 다음과 같이 작성할 수 있다.

<%= link_to 'Destroy', post_path(post), method: :delete, data: { confirm: 'Are you sure?' } %>

위의 두 코드라인은 정확히 동일할 것이다.

이 액선은 동작 후 인덱스 페이지로 이동하게 된다.

  def destroy
    @post.destroy
    respond_to do "format
      format.html { redirect_to posts_url, notice: 'Post was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

이상에서 URI 패턴과 HTTP 메소드의 조합으로 다양한 액션을 특정해서 호출할수 있다는 것을 예로 들어 설명하였다.

커스텀 경로 지정하기

이상의 것은 scaffold 제너레이터를 이용하여 생성된 리소스 라우팅에 대한 설명이었다. 즉, resources :posts 코드라인으로 7개의 표준 라우팅이 자동으로 생성되는 것이다.

그러나 때로는 라우트를 직접 정의해서 사용해야 한다. 방법은 다음과 같다.

get 'url-pattern', to: controller#action`, as: :custom_prefix

지금까지 설명한 내용을 잘 이해했다면 일견에 이해가 갈 것이다. 마지막의 :as 옵션은 prefix 값을 사용자가 지정할 때 사용한다.

예를 들면,

get /posts/:id/archive, to: posts#archive’, as: :archive_post

위의 같이 정의할 경우 뷰 템플릿 파일에서 erb 코드로 archive_post_path(post)와 같이 사용할 수 있게 된다.
주의할 것을 GET 이외의 HTTP 메소드를 사용하여 커스텀 라우트를 정의할 경우에는 link_to 헬퍼메소드를 사용할 때 반드시 :method 옵션으로 해당 HTTP 메소들명을 지정행 주어야 한다는 것이다.

이상으로 부족하지만 레일스 라우팅에 대한 대략적인 내용을 초보자 수준에 맞춰 설명하고자 노력하였습니다.

많은 도움이 되셨기를 기대하면서…
감사합니다.

글쓴이: 최효성

외과전문의,웹프로그래밍,컴퓨터 일러스트레이션 / Surgeon, Medical Illustration, Web Programmer

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중