얼마 전 페일스북 그룹에 올라 온 질문을 보고 초보자들에게 흔히 있을 수 있는 궁금증이라고 생각하여 이 글을 작성한다.
웹서비스는 라우팅으로부터 시작한다고 해도 과언이 아닐 것이다.
특히나 레일스도 REST형식의 라우팅을 지원한다. 레일스는 외부로부터 서버로 들어오는 URI 요청을 파싱하여 어떤 컨트롤러를 호출할 것인지 정하게 되는데, 해당 컨트롤러의 특정 액션을 구체적으로 호출한 후 액션명과 동일한 뷰 템플릿 파일(정확히는, [action-name].html.erb
)을 app/views/[controlle-name]/
디렉토리에서 찾아 페이지를 렌더링한 후 요청결과로써 클라이언트에게 발송하게 된다.
이와 같은 요청/응답의 과정은 레일스 프로젝트의 COC 원칙 중의 대표적인 사례다.
효율적인 설명을 위해서 간단한 토이 프로젝트를 작성할 것이다.
아래와 같이 routes_test
라는 프로젝트 생성한다.
$ $ rails new routes_test 2.5.1
Using -d postgresql #Use postgres from /Users/lucius/.railsrc
create
create README.md
create Rakefile
create .ruby-version
create config.ru
create .gitignore
create Gemfile
run git init from "."
Initialized empty Git repository in /Users/lucius/Projects/myrails/r5/routes_test/.git/
create package.json
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/cable.yml
create config/puma.rb
create config/spring.rb
create config/storage.yml
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/content_security_policy.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_2.rb
create config/initializers/wrap_parameters.rb
create config/locales
create config/locales/en.yml
create config/master.key
append .gitignore
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 tmp
create tmp/.keep
create tmp/cache
create tmp/cache/assets
create vendor
create vendor/.keep
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 storage
create storage/.keep
create tmp/storage
create tmp/storage/.keep
remove config/initializers/cors.rb
remove config/initializers/new_framework_defaults_5_2.rb
run bundle install
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/...
Resolving dependencies...
Using rake 12.3.1
Using concurrent-ruby 1.0.5
Using i18n 1.0.1
Using minitest 5.11.3
Using thread_safe 0.3.6
Using tzinfo 1.2.5
Using activesupport 5.2.0
Using builder 3.2.3
Using erubi 1.7.1
Using mini_portile2 2.3.0
Using nokogiri 1.8.3
Using rails-dom-testing 2.0.3
Using crass 1.0.4
Using loofah 2.2.2
Using rails-html-sanitizer 1.0.4
Using actionview 5.2.0
Using rack 2.0.5
Using rack-test 1.0.0
Using actionpack 5.2.0
Using nio4r 2.3.1
Using websocket-extensions 0.1.3
Using websocket-driver 0.7.0
Using actioncable 5.2.0
Using globalid 0.4.1
Using activejob 5.2.0
Using mini_mime 1.0.0
Using mail 2.7.0
Using actionmailer 5.2.0
Using activemodel 5.2.0
Using arel 9.0.0
Using activerecord 5.2.0
Using mimemagic 0.3.2
Using marcel 0.3.2
Using activestorage 5.2.0
Using public_suffix 3.0.2
Using addressable 2.5.2
Using io-like 0.3.0
Using archive-zip 0.11.0
Using bindex 0.5.0
Using msgpack 1.2.4
Using bootsnap 1.3.0
Using bundler 1.16.1
Using byebug 10.0.2
Using xpath 3.1.0
Fetching capybara 3.3.1
Installing capybara 3.3.1
Using ffi 1.9.25
Using childprocess 0.9.0
Using chromedriver-helper 1.2.0
Using coffee-script-source 1.12.2
Using execjs 2.7.0
Using coffee-script 2.4.1
Using method_source 0.9.0
Using thor 0.20.0
Using railties 5.2.0
Using coffee-rails 4.2.2
Using multi_json 1.13.1
Using jbuilder 2.7.0
Using rb-fsevent 0.10.3
Using rb-inotify 0.9.10
Using ruby_dep 1.5.0
Using listen 3.1.5
Using pg 1.0.0
Using puma 3.11.4
Using sprockets 3.7.2
Using sprockets-rails 3.2.1
Using rails 5.2.0
Using rubyzip 1.2.1
Using sass-listen 4.0.0
Using sass 3.5.6
Using tilt 2.0.8
Using sass-rails 5.0.7
Fetching selenium-webdriver 3.13.0
Installing selenium-webdriver 3.13.0
Using spring 2.0.2
Using spring-watcher-listen 2.0.1
Using turbolinks-source 5.1.0
Using turbolinks 5.1.1
Fetching uglifier 4.1.12
Installing uglifier 4.1.12
Using web-console 3.6.2
Bundle complete! 18 Gemfile dependencies, 78 gems now installed.
Use `bundle info [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
invoke active_record
create db/migrate/20180628094047_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
create test/system/posts_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 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#destroy
위에서 사용한 -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 Pattern
과 Verb
의 조합으로 컨트롤러와 액션이 특정되는 것이다.
예를 들면, /posts
라는 URI Pattern
은 Verb
의 종류에 따라 즉, 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번째 코드라인을 보자.
사실 위의 erb 코드는 축약형이다.
위의 코드를 추가한 후 로컬 서버를 실행한다.
$ rails server
그리고 브라우저에서 http://localhost:3000/posts
로 접근을 시도하고 페이지 하단의 New Post
링크를 클릭하여 글을 하나 추가한다. 글 내용을 달라도 아래와 같은 페이지 화면이 보이게 될 것이다.
2. POST 메소드
http://localhost:3000/posts
페이지로 접근한 후 하단의 New Post
링크를 클릭하여 입력 폼으로 이동하자. 그리고 마우스 우측버튼을 클릭하여 소스보기를 하면 다음과 같이 보일 것이다.
따라서 데이터를 입력한 후 제출하면 /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
인데스 페이지에서 이미 생성된 post
의 Edit
링크를 클릭하여 편집 페이지로 이동하자. 마우스 우측 클릭하여 소스보기를 하면 다음과 같이 보일 것이다.
이제 수정 내용을 제출하면 /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
액션이 호출되도록 한다.
이 또한 축약형으로 사용할 것이다. 이것은 다음과 같이 작성할 수 있다.
위의 두 코드라인은 정확히 동일할 것이다.
이 액선은 동작 후 인덱스 페이지로 이동하게 된다.
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 메소들명을 지정행 주어야 한다는 것이다.
이상으로 부족하지만 레일스 라우팅에 대한 대략적인 내용을 초보자 수준에 맞춰 설명하고자 노력하였습니다.
많은 도움이 되셨기를 기대하면서…
감사합니다.
감사합니다.