들어가기 앞서
개발 환경
- OS: macOS Big Sur 11.5.2
- IDE: Visual Studio Code, RubyMine를 혼용하여 사용
(책에서는 AWS Cloud9 IDE 환경에서 사용) - Ruby: ruby 2.6.9p207 (2021-11-24 revision 67954) [x86_64-darwin20]
- Ruby on Rails: Rails 5.1.6
- SCM: GitHub
AWS Cloud9 환경이 아닌 로컬환경에서 실행하였기 때문에 루비와 레일즈 설치는 가이드 문서를 보고 진행하였다.
클라우드 IDE를 사용할 경우 책의 내용을 따라 하면 된다.
지난 글
지난 장에서 배운 내용을 토대로 학습을 진행하고 있기 때문에 앞의 내용을 모르면 이해가 어렵게 느껴질 수 있다. 이전 글을 읽고 해당 글을 읽기를 추천한다.
목표
3장부터는 Sample 애플리케이션을 개발하고 점차 개선해 나가는 방식으로 튜토리얼이 진행될 예정이다. 따라서 이전 장을 보고 오는 것을 권고한다. 레일즈는 데이터베이스와 연계하여 동적인 웹 사이드를 개발할 수 있도록 설계되어 있지만 HTML 파일만으로 구성되어 있는 정적인 페이지도 만들 수 있다. 이번 장에서는 이러한 정적페이지 작성에 대해 알아볼 예정이다. 또한 코드의 동작을 보장할 수 있는 자동화 테스트에 대해서도 알아볼 예정이다.
프로젝트 생성
프로젝트 생성 및 Gem 설정
1장, 2장과 동일하게 rails new 커맨드를 사용하여 프로젝트를 생성한다. 이때 버전은 5.1.6으로 고정한다.
$ cd ~/workspace #작업 폴더로 이동
$ rails _5.1.6_ new sample_app
$ cd sample_app/
아래와 같이 Gemfile을 수정하고 bundle install 명령어를 사용하여 실제 배포환경의 gem을 제외한 로컬 gem을 설치한다. 그 후 원한다면 소스 코드를 Git에 올려준다.
# Gemfile
source 'https://rubygems.org'
gem 'rails', '5.1.6'
gem 'puma', '3.9.1'
gem 'sass-rails', '5.0.6'
gem 'uglifier', '3.2.0'
gem 'coffee-rails', '4.2.2'
gem 'jquery-rails', '4.3.1'
gem 'turbolinks', '5.0.1'
gem 'jbuilder', '2.7.0'
group :development, :test do
gem 'sqlite3', '1.3.13'
gem 'byebug', '9.0.6', platform: :mri
end
group :development do
gem 'web-console', '3.5.1'
gem 'listen', '3.1.5'
gem 'spring', '2.0.2'
gem 'spring-watcher-listen', '2.0.1'
end
group :production do
gem 'pg', '0.20.0'
end
# Windows 환경에서는 tzinfo-data 라고 하는 gem을 포함 시킬 필요가 있습니다.
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
$ bundle install --without production
# 만약 Gemfile에서 지정한 버전과 다른 gem을 설치한 경우에는 아래 명령어를 실행하여 gem을 갱신해주세요.
$ bundel update
아래와 같이 루트 디렉토리의 README.md 파일을 수정하여 구체적인 작업 내용과 사용 방법을 알기 쉽게 작성한다.
# Ruby on Rails Tutorial Sample Application
이 프로젝트는, 아래의 링크의 내용을 바탕으로 만들어진 프로젝트입니다.
- [*Ruby on Rails 튜토리얼*](https://railstutorial.jp/)
- [Michael Hartl](http://www.michaelhartl.com/)
## 라이센스
[Ruby on Rails 튜토리얼](https://railstutorial.jp/)에 기재되어 있는
소스코드는 MIT라이센스와 Beerware라이센스를 기반으로 공개되어있습니다.
<br>
상세한 내용은 [LICENSE.md](LICENSE.md)를 참고해주세요.
## 사용방법
이 어플리케이션을 구동시키는 방법은, 일단 레포지토리를 로컬로 clone해주세요.
그 다음, 아래의 커맨드를 사용하여 필요한 RubyGem을 설치해주세요.
```
$ bundle install --without production
```
그 다음, 데이터베이스를 마이그레이션해주세요.
```
$ rails db:migrate
```
마지막으로, 테스트를 실행하여 제대로 동작하는지 확인해주세요.
```
$ rails test
```
테스트가 무사히 종료되었다면, Rails서버를 실행시켜주세요.
```
$ rails server
```
자세한 것은 [Ruby on Rails Tutorial](https://www.railstutorial.org/)을 참고해주세요.
Hello world 표시하기
앞장에서 배운 내용을 토대로 기본 페이지에 Hello, world! 가 표시되도록 수정한다. 작업 내용은 Git과 같은 형상관리 툴을 이용하여 관리하는 것을 권장한다.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
def hello
render html: 'hello, world!'
end
end
# config/routes.rb
Rails.application.routes.draw do
root 'application#hello'
end
정적 페이지 작성
브랜치 생성
Git에서 버전을 관리하고 있다면 master 브랜치에서 작업하는 것이 아닌 작업 당시의 토픽 브랜치를 작성하여 작업하는 것이 좋다. 다음 명령어를 이용하여 정적인 페이지용 토픽 브랜치를 체크아웃하겠다.
$ git checkout -b static-pages
Static Pages 생성
2장에서 scaffold 명령어와 함께 사용한 generate 스크립트를 이용하여 컨트롤러 생성을 진행한다. 현재 만들 컨트롤러는 정적인 페이지를 다루기 위한 용도이기 때문에 컨트롤러의 이름을 Static Pages로 정하고 카멜 케이스를 사용하여 StaticPages로 입력한다. 이어서 Home페이지, Help페이지, About페이지에 사용할 액션도 각각 생성해 주도록 하는데 이름은 모두 소문자로 home, help, about으로 한다. generate 스크립트에서는 액션의 이름을 한 번에 지정할 수 있다. Home과 Help 페이지는 아래와 같이 명령어를 통해 생성하고 About 페이지는 연습을 위해 수동으로 만들어보겠다.
아래 명령어에서는 컨트롤러의 이름을 카멜 케이스(StaticPages)로 입력하고 있지만 사실 스네이크 케이스(static_pages)로 입력해도 결과는 동일하다. 이것은 클래스명을 카멜케이스로 생성하는 루비의 관습으로부터 나온 것이고 어차피 루비에서는 파일명은 스네이크 케이스로 기술하기 때문에 generate 스크립트에서 underscope 메소드를 사용하여 카멜 케이스를 스네이크 케이스로 변환한다.
$ rails generate controller StaticPages home help
create app/controllers/static_pages_controller.rb
route get 'static_pages/help'
route get 'static_pages/home'
invoke erb
create app/views/static_pages
create app/views/static_pages/home.html.erb
create app/views/static_pages/help.html.erb
invoke test_unit
create test/controllers/static_pages_controller_test.rb
invoke helper
create app/helpers/static_pages_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/static_pages.coffee
invoke scss
create app/assets/stylesheets/static_pages.scss
💡 명령어(Command)의 축약형
우리가 지금까지 배우거나 배울 명령어는 아래와 같이 축약하여 사용할 수 있다. 레일즈는 다수의 단축형을 제공하지만 튜토리얼에서는 알기 쉽게 설명하기 위해 완전한 명령어로 사용할 것이다.
- rails server = rails s
- rails console = rails c
- rails generate = rails g
- rails test = rails t
- bundle install = bundle
한 번의 명령어만으로 여러 파일들이 생성되었다. 그렇지만 만약 컨트롤러의 이름 변경되거나 필요가 없어진다면 어떻게 해야 할까? generate 스크립트는 컨트롤러 파일 외에도 다수의 관련 파일을 생성하며 기존 파일에도 코드를 삽입하기에 원래대로 돌리기 쉽지 않다. 이럴 때 generate 대신 destroy를 실행하여 원래대로 되돌릴 수 있다. 자세한 사용방법은 아래 코드 참고 바란다.
# 자동 생성
$ rails generate controller StaticPages home help
# 위에서 생성된 내용을 삭제
$ rails destroy controller StaticPages home help
정상적으로 StaticPages 컨트롤러를 생성했다면 레일즈의 라우팅 설정 파일이 자동적으로 업데이트된다. 앞서 home 액션과 help 액션을 생성하였기 때문에 해당 파일에는 각 액션에서 쓰이는 룰이 정의되어 있다.
# config/routes.rb
Rails.application.routes.draw do
get 'static_pages/home'
get 'static_pages/help'
root 'application#hello'
end
위 내용을 조금 자세히 설명하면 get 'static_pages/home'라는 코드는 /static_pages/home이라는 URL로 온 요청을 StaticPages 컨트롤러의 home 액션과 연결한다. 코드에서 get이라고 쓰여있기 때문에 Http Method가 GET일 때 발동하는 액션을 연결한다. 레일즈 서버를 작동시킨 후 /static_pages/home에 접속하면 아래와 같은 화면을 볼 수 있다.
생성된 컨트롤러를 자세히 살펴보도록 하겠다. StaticPages 컨트롤러는 2장에서 만든 컨트롤러와 달리 일반적인 REST 액션에 대해서 대응하지 않고 있다. 대신 정적 페이지들에 대해서는 적절하게 대응하고 있는데 이는 REST 아키텍처가 모든 문제에 대한 해결 방법은 아니라는 것을 알 수 있게 해 준다.
코드를 보면 class 키워드를 사용하여 StaticPagesController라는 클래스를 정의하고 있는 것을 볼 수 있다. 클래스는 메서드(또는 함수)를 한 번에 모아 정의할 때 편리하다. 또한 def 키워드를 사용하여 home, help 액션을 정의하고 있다. 두 액션 내에는 내용이 전혀 없는 상태이다. 순수한 루비 언어라면 이러한 메서드는 아무것도 실행하지 않을 것이다. 그러나 루비의 클래스인 StaticPagesController는 ApplicationController 클래스를 상속받고 있기 때문에 레일즈 특유의 동작을 수행할 수 있다.
# app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
def home
end
def help
end
end
구체적으로는 /static_pages/home에 접속해 보면 레일즈는 StaticPages 컨트롤러를 참조하여 home 액션에 작성되어 있는 코드를 실행한다. 그 후 액션에 대응하는 뷰를 출력한다. 지금은 home 액션에 아무것도 정의되어 있지 않기 때문에 단순한 뷰가 출력된다. 그렇다면 뷰는 어떻게 출력되고 어떠한 뷰가 출력되는 것일까? 컨트롤러를 generate 할 때의 로그를 다시 한번 주의 깊게 확인해면 액션과 뷰의 관계에 대해 추측해 볼 수 있다. home 액션은 home.html.erb라고 하는 뷰 파일과 대응한다.
Static Pages 수정
레일즈의 뷰는 이름에서 알 수 있듯이 정적인 HTML을 포함하고 있다. 그렇기 때문에 당장 레일즈에 대한 지식이 없어도 화면을 수정할 수 있다. 다음과 같이 코드를 수정하고 화면에서 결과를 확인해 보자.
<!-- app/views/static_pages/home.html.erb -->
<h1>Sample App</h1>
<p>
This is the home page for the
<a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
sample application.
</p>
<!-- app/views/static_pages/help.html.erb -->
<h1>Help</h1>
<p>
Get help on the Ruby on Rails Tutorial at the
<a href="https://railstutorial.jp/help">Rails Tutorial help page</a>.
To get help on this sample app, see the
<a href="https://railstutorial.jp/#ebook"><em>Ruby on Rails Tutorial</em>
book</a>.
</p>
TDD 방식으로 About 페이지 만들기
애플리케이션을 개발하면서 Test Suite(Test Case의 묶음)를 제대로 만들어놓고 자동화 테스트를 진행하는 습관을 들이는 것을 권장한다. 테스트 케이스는 추가적인 작성 시간을 요구하지만 결과적으로 애플리케이션 신뢰도를 높이고 불필요한 버그와 이로 인한 시간 낭비를 방지함으로써 개발이 빨라질 수 있다. 때문에 초반부터 테스트 케이스를 작성하는 습관을 들인다면 효율적으로 개발할 수 있을 것이다.
💡TDD(Test Driven Development)
한국어로는 테스트 주도 개발이라고 불리며 반복 테스트를 이용한 소프트웨어 방법론이다. "올바른 코드가 아니면 실패하는 테스트 케이스"를 작성하고 그다음으로 제대로 된 코드를 작성하여 통과하도록 하는 단계를 반복적으로 수행한다.
rails generate controller를 수행한 시점에 함께 생성된 테스트 파일을 확인해 보자.
$ ls test/controllers/
static_pages_controller_test.rb
지금 당장은 모든 코드를 이해할 필요는 없다. 다만 해당 파일에 2개의 테스트 케이스가 작성되어 있다는 정도만 알아두어도 된다. 2개의 테스트 케이스는 컨트롤러의 home과 help 액션이 정상적으로 동작하는 것을 확인하는데 이때 assertion이라는 방법으로 진행한다.
# test/controllers/static_pages_controller_test.rb
require 'test_helper'
class StaticPagesControllerTest < ActionDispatch::IntegrationTest
test "should get home" do
get static_pages_home_url
assert_response :success
end
test "should get help" do
get static_pages_help_url
assert_response :success
end
end
자세히 보면 아래 코드는 다음과 같은 의미를 나타내고 최종적으로는 "Home 페이지의 테스트 케이스: GET 요청을 home액션으로 보내면 응답이 success(OK)가 될 것이다"라는 테스트 케이스이다.
- get: 페이지가 GET 요청을 받아 동작하는 웹 페이지라는 것을 나타낸다.
- response :success: Http의 상태코드(여기서는 200 OK)를 나타낸다.
test "should get home" do
get static_pages_home_url
assert_response :success
end
본격적으로 테스트 코드를 작성하기 전 rails test 명령어를 사용하여 현재 테스트 케이스를 동작해 보았고 성공하는 것을 확인하였다.
$ rails test
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
앞서 설명한 것처럼 코드를 작성하기 전 실패하는 About 페이지 테스트 코드를 먼저 작성해 보도록 하자. 이미 작성되어 있는 코드를 참고하여 아래와 같이 About 페이지에 대한 테스트 코드를 작성할 수 있다. 2개의 테스트 케이스와 다른 점은 페이지명뿐이니 어렵지 않게 작성할 수 있을 것이다.
require 'test_helper'
class StaticPagesControllerTest < ActionDispatch::IntegrationTest
test "should get home" do
get static_pages_home_url
assert_response :success
end
test "should get help" do
get static_pages_help_url
assert_response :success
end
test "should get about" do
get static_pages_about_url
assert_response :success
end
end
다시 한번 테스트를 수행해 보면 아직 About 페이지에 대한 처리는 한 것이 하나도 없으니 예상대로 실패한다.
$ rails test
3 tests, 2 assertions, 0 failures, 1 errors, 0 skips
이상한 표현이지만 테스트 케이스가 성공적으로 실패했기 때문에 이제 테스트 케이스가 성공할 수 있도록 코드를 작성해 보겠다. 실패한 테스트의 오류 메시지를 보면 다음과 같다. 해석하자면 "About 페이지용의 URL을 찾을 수 없다"라는 의미이다.
$ rails test
NameError: undefined local variable or method `static_pages_about_url'
위 메시지를 토대로 아래와 같이 라우팅 파일을 수정하였다. static_pages/about라는 URL에 대해 GET 요청이 오면 StaticPages 컨트롤러의 about 액션이 실행되게 레일즈에 설정해 주었다. 그 결과 자동적으로 static_pages_about_url 헬퍼를 사용할 수 있게 됐다.
# config/routes.rb
Rails.application.routes.draw do
get 'static_pages/home'
get 'static_pages/help'
get 'static_pages/about' #추가한 코드
root 'application#hello'
end
다시 한번 테스트 케이스를 실행하면 달라진 오류 메시지를 확인할 수 있다. 해석하자면 "StaticPages 컨트롤러에 about 액션이 없다"이다.
$ rails test
AbstractController::ActionNotFound:
The action ‘about’ could not be found for StaticPagesController
위 액션을 근거로 다른 두 페이지처럼 액션을 추가해 주었다.
# app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
def home
end
def help
end
def about
end
end
이제는 되지 않을까? 하지만 테스트 케이스는 여전히 실패한다 😂 해석하면 요청에 대한 템플릿이 없다는 것인데 레일즈에서 템플릿은 뷰(View)를 의미한다. 앞에서 컨트롤러에 대해 설명할 때 언급했지만 home 액션은 home.html.erb 뷰와 관련되어 있다. 이 뷰는 app/views/static_pages 디렉터리에 있으니 여기에 about.html.erb라는 파일을 만들면 될 것 같다. 파일을 생성하고 아래와 같이 코드를 작성한다.
# app/views/static_pages/about.html.erb
<h1>About</h1>
<p>
<a href=“https://railstutorial.jp/“>Ruby on Rails Tutorial</a>
is a <a href=“https://railstutorial.jp/#ebook”>book</a> and
<a href=“https://railstutorial.jp/#screencast”>screencast</a>
to teach web development with
<a href=“http://rubyonrails.org/“>Ruby on Rails</a>.
This is the sample application for the tutorial.
</p>
그 후 테스트 케이스를 수행하면 드디어 모든 테스트 케이스가 통과되었다. 😆
$ rails test
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips
실제 화면에서도 테스트한 내용이 정상적으로 동작하는 것을 확인해 볼 수 있다.
조금은 동적으로 Title이 바뀌는 페이지 만들기
많은 브라우저에서는 title 태그의 내용을 브라우저 상단 탭의 제목으로 표시한다. title태그는 SEO(Search Engine Optimization: 검색엔진 최적화)에 있어서도 매우 중요한 역할을 한다. 3개 페이지의 제목을 “<페이지 이름> | Ruby on Rails Tutorial Sample App”이라고 하는 형식으로 바꾸어보겠다. rails new 명령을 실행하면 레이아웃도 기본으로 작성되지만 여기서는 학습을 위해 일시적으로 다음과 같이 파일명을 바꾸겠다.
$ mv app/views/layouts/application.html.erb layout_file
목표하는 제목은 각 페이지별로 다음과 같다.
Page | URL | Title |
Home | /static_pages/home | "Home | Ruby on Rails Tutorial Sample App" |
Help | /static_pages/help | "Help | Ruby on Rails Tutorial Sample App" |
About | /static_pages/about | "About | Ruby on Rails Tutorial Sample App" |
코드를 수정하기 전 각 제목에 대한 테스트 코드를 먼저 작성한다. 이번 테스트 코드에서 사용하고 있는 assert_select 메서드는 특정 HTML 태그의 존재 여부를 테스트할 수 있다. 이런 종류의 assert 테스트 메서드는 셀렉터라고 부르는 경우도 있다. assert_select "title", "Home | Ruby on Rails Tutorial Sample App"에서는 <title> 태그 안에 "Home | Ruby on Rails Tutorial Sample App"이라는 있는지 체크한다. 같은 방법으로 3개의 페이지를 수정하겠다.
# test/controllers/static_pages_controller_test.rb
require 'test_helper'
class StaticPagesControllerTest < ActionDispatch::IntegrationTest
test "should get home" do
get static_pages_home_url
assert_response :success
assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
end
test "should get help" do
get static_pages_help_url
assert_response :success
assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
end
test "should get about" do
get static_pages_about_url
assert_response :success
assert_select "title", "About | Ruby on Rails Tutorial Sample App"
end
end
위의 테스트 코드를 실행해 보면 테스트 결과는 실패일 것이다.
$ rails test
3 tests, 6 assertions, 3 failures, 0 errors, 0 skips
그럼 각 페이지에 <title> 태그를 추가하여 테스트가 통과할 수 있도록 해보자. 기본적인 HTML 구조를 우리의 페이지에 적용한다면 아래와 같이 될 것이다.
<!-- app/views/staitc_pages/home.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title>Home | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>Sample App</h1>
<p>
This is the home page for the
<a href=“https://railstutorial.jp/“>Ruby on Rails Tutorial</a>
sample application.
</p>
</body>
</html>
<!-- app/views/static_pages/help.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title>Help | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>Help</h1>
<p> Get help on the Ruby on Rails Tutorial at the
<a href=“https://railstutorial.jp/help”>Rails Tutorial help
page</a>.
To get help on this sample app, see the
<a href=“https://railstutorial.jp/#ebook”>
<em>Ruby on Rails Tutorial</em> book</a>.
</p>
</body>
</html>
<!-- app/views/static_pages/about.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title>About | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>About</h1>
<p>
<a href=“https://railstutorial.jp/“>Ruby on Rails Tutorial</a>
is a <a href=“https://railstutorial.jp/#ebook”>book</a> and
<a href=“https://railstutorial.jp/#screencast”>screencast</a>
to teach web development with
<a href=“http://rubyonrails.org/“>Ruby on Rails</a>.
This is the sample application for the tutorial.
</p>
</body>
</html>
각 페이지의 제목이 원하던 대로 변경된 것을 확인할 수 있다.
코드 변경 후 테스트 코드를 다시 실행해 보면 모두 통과된 것을 확인할 수 있다.
$ rails test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips
묘하게 냄새나는 코드 리팩토링(Refactoring) 하기
테스트를 모두 성공했지만 여기서 끝이 아니다. 애플리케이션을 개발하다 보면 코드 어디선가 일종의 "구린내"가 느껴지기 시작한다. 통일성 없는 표현을 사용하거나 메서드가 몇 백 줄씩 늘어나 가독성이 떨어지고 히스토리가 불분명하거나 중복된 코드가 많아져 코드의 품질이 떨어진 상태를 구린내가 난다고 표현한다. 컴퓨터는 아무리 더럽고 구린 코드라도 실행만 하면 되지만 사람이란 그럴 수는 없다. 따라서 코드의 구린내를 잡는 리팩토링 과정이 필요하다. 리팩토링 과정은 필수는 아니지만 코드 점검을 통해 필요하다고 판단될 경우 수행하는 습관을 들이면 코드 품질을 유지하는데 효과적이다.
먼저 테스트 코드를 리팩토링 해보겠다. 기존 코드에는 "Ruby on Rails Tutorial Sample App"이라는 기본 타이틀이 각 테스트마다 반복적으로 입력되어 있다. 당장은 문제가 없어 보이지만 향후 값이 변경되었을 경우 대응이 어려워지고 중복이 많은 코드이다. 때문에 setup이라는 메서드를 사용하여 아래와 같이 중복을 해결하였다. (setup: 각 메서드가 실행되기 직전에 실행되는 메서드)
# test/controllers/static_pages_controller_test.rb
require ‘test_helper’
class StaticPagesControllerTest < ActionDispatch::IntegrationTest
def setup
@base_title = "Ruby on Rails Tutorial Sample App"
end
test "should get home" do
get static_pages_home_url
assert_response :success
assert_select "title", "Home | #{@base_title}"
end
test "should get help" do
get static_pages_help_url
assert_response :success
assert_select "title", "Help | #{@base_title}"
end
test "should get about" do
get static_pages_about_url
assert_response :success
assert_select "title", "About | #{@base_title}"
end
end
테스트 케이스를 수정하였지만 모두 통과되는 결과에는 영향이 없다.
$ rails test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips
그렇다면 이제 정적 페이지를 리팩토링할 필요가 있다. 지금까지 3개의 페이지를 생성하며 여러 가지 것들을 배웠다. 그렇지만 지금까지 해온 것들은 단순한 정적 페이지이고 레일즈의 기능을 충분히 지키지 않았으며 중복된 코드가 많았다. 특히 같은 코드를 반복적으로 작성하는 것은 루비의 DRY(Don't Repeat Yourself) 원칙을 지키지 않는 것이다.
- 페이지의 타이틀이 전부 똑같다.
- "Ruby on Rails Tutorial Sample App"가 세 페이지의 제목에서 반복적으로 사용된다.
- HTML의 구조가 각 페이지에 중복되어 있다.
중복을 없애기 위한 방법으로 뷰에서 ERB(Embedded Ruby)를 사용할 수 있다. 레일즈의 provide 메서드를 사용하여 Home, Help, About 페이지의 제목을 각각 다르게 변경해 보자. ERB는 웹 페이지에 동적인 요소를 더하여 사용할 때 사용하는 템플릿 시스템이다. 아래 예제를 통해 HTML 뷰 파일의 확장자가 html.erb로 되어있는 이유를 알게 될 거라 생각한다.
<!-- app/views/static_pages/home.html.erb -->
<% provide(:title, "Home") %>
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>Sample App</h1>
<p>
This is the home page for the
<a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
sample application.
</p>
</body>
</html>
<!-- app/views/static_pages/help.html.erb -->
<% provide(:title, “Help”) %>
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>Help</h1>
<p> Get help on the Ruby on Rails Tutorial at the
<a href=“https://railstutorial.jp/help”>Rails Tutorial help
section</a>.
To get help on this sample app, see the
<a href=“https://railstutorial.jp/#ebook”>
<em>Ruby on Rails Tutorial</em> book</a>.
</p>
</body>
</html>
<!-- app/views/static_pages/about.html.erb -->
<% provide(:title, “About”) %>
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>About</h1>
<p>
<a href=“https://railstutorial.jp/“>Ruby on Rails Tutorial</a>
is a <a href=“https://railstutorial.jp/#ebook”>book</a> and
<a href=“https://railstutorial.jp/#screencast”>screencast</a>
to teach web development with
<a href=“http://rubyonrails.org/“>Ruby on Rails</a>.
This is the sample application for the tutorial.
</p>
</body>
</html>
이 코드에서는 "<% … %>”라고라고 하는 표기법이 쓰여지고 있는데 그 안에 레일즈의 provide 메소드가 사용되고 있다. 메소드의 파라미터로는 "Home" 문자열과 :title이라고이라고 하는 심볼이 설정되어 있다. 이 메소드를 사용하여 템플릿의 해당 부분을 실제 제목으로 대체할 수 있다.
<% provide(:title, "Home") %>
여기서는 위와 다르게 "<%=... %>"와 같이 등호(=)가 추가되었다. 등호가 없을 때는 코드를 단순하게 실행만 하지만 등호가 추가되면 실행 결과가 템플릿의 해당 부분에 삽입된다.
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
제목의 변수 부분이 ERB에 의해 동적으로 생성되게 변경되었지만 페이지의 출력결과는 이전과 동일하다.
$ rails test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips
제목을 ERB 변수로 바꾸었으니 현재 각 페이지는 다음과 같은 구조로 되어있을 것이고 다른 점이 있다면 body 태그 내부의 내용뿐일 것이다. HTML의 구조는 중복이 일어난 것이다. 레일즈에서는 이러한 HTML의 중복을 피하기 위해 application.html.erb라고 하는 이름의 레이아웃 파일을 제공한다.
<% provide(:title, "The Title") %>
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
Contents
</body>
</html>
이전에 연습을 위해 레이아웃 파일의 이름을 바꾸었으니 다음의 명령어로 파일이름을 원래대로 되돌릴 필요가 있다
$ mv layout_file app/views/layouts/application.html.erb
이 레이아웃 파일을 유효하게 하기 위해서 title 태그 부분을 ERB 코드로 바꾼다.
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag ‘application’, media: ‘all’,
‘data-turbolinks-track’: ‘reload’ %>
<%= javascript_include_tag ‘application’,
‘data-turbolinks-track’: ‘reload’ %>
</head>
<body>
<%= yield %>
</body>
</html>
위 내용에서 특이한 코드를 조금 살펴보면 이 코드는 각 페이지의 내용을 레이아웃에 삽입하기 위해 존재한다. 여기서 이 코드의 정확한 동작을 정확히 것은 중요하지 않다. 레이아웃을 사용할 때에는 /static_pages/home에 접속하면 home.html.erb의 내용이 HTML로 변환되어 <%= yield %>의 위치에 삽입되는 처리만 기억하면 될 것이다.
<%= yield %>
또한 Rails의 디폴트 레이아웃에서는 다음의 코드가 추가되어 있는 것을 확인하길 바란다. 3줄의 ERB 코드는 각각 Stylesheet, Javascript, csrf_meta_tags 메서드를 페이지 내부에서 전개하고 있다. 스타일시트와 자바스크립트는 Asset Pipline의 일부이고 csrf_meta_tags는 웹 공격방법 중 하나인 Cross-Site Request Forgery: CSRF를 막기 위해 사용되는 레일즈 메서드이다.
<%= csrf_meta_tags %>
<%= stylesheet_link_tag ... %>
<%= javascript_include_tag "application", ... %>
Home, Help, About 페이지의 중복되는 부분을 application.html.erb로 처리하였으니 각 페이지 파일에서 중복되는 부분을 제거하여 다음과 같이 수정한다.
<!-- app/views/static_pages/home.html.erb -->
<% provide(:title, "Home") %>
<h1>Sample App</h1>
<p>
This is the home page for the
<a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
sample application.
</p>
<!-- app/views/staic_pages/help.html.erb -->
<% provide(:title, "Help") %>
<h1>Help</h1>
<p> Get help on the Ruby on Rails Tutorial at the
<a href="https://railstutorial.jp/help">Rails Tutorial help section</a>.
To get help on this sample app, see the
<a href="https://railstutorial.jp/#ebook"><em>Ruby on Rails Tutorial</em>
book</a>.
</p>
<!-- app/views/static_pages/about.html.erb -->
<% provide(:title, "About") %>
<h1>About</h1>
<p>
<a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
is a <a href="https://railstutorial.jp/#ebook">book</a> and
<a href="https://railstutorial.jp/#screencast">screencast</a>
to teach web development with
<a href="http://rubyonrails.org/">Ruby on Rails</a>.
This is the sample application for the tutorial.
</p>
출력은 수정하기 전과 달라지는 것이 없지만 중복된 코드를 줄일 수 있게 되었다. 이번 섹션에서 다룬 작고 사소한 리팩토링도 실제로 해보면 여러 가지 에러를 야기할 수 있다. 때문에 테스트 케이스를 제대로 계획해 놓는 것이 얼마나 중요한 것인지 이해해 주면 좋겠다. 물론 엄밀히 말하자면 테스트가 통과한 것만으로 해당 코드가 정말 올바른 코드인지 아닌지 증명할 수는 없다. 그러나 어떻게 하면 올바른 코드로 수정할 수 있는지 힌트를 얻을 수 있고 무엇보다도 향후 발생할 버그를 막을 수 있는 최소한의 안전장치 역할을 할 수 있다.
$ rails test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips
라우팅 설정 변경
이제 애플리케이션의 루트 페이지를 Home 페이지로 설정하기 위해 레일즈의 라우팅 설정 파일을 아래와 같이 수정하겠다.
# config/routes.rb
Rails.application.routes.draw do
root ‘static_pages#home’
get ‘static_pages/home’
get ‘static_pages/help’
get ‘static_pages/about’
end
변경한 루트 페이지에 대한 테스트 케이스는 아래와 같이 작성하였다.
require 'test_helper'
class StaticPagesControllerTest < ActionDispatch::IntegrationTest
def setup
@base_title = "Ruby on Rails Tutorial Sample App"
end
# 추가
test "should get root" do
get root_url
assert_response :success
end
test "should get home" do
get static_pages_home_url
assert_response :success
assert_select "title" , "#{@base_title}"
end
test "should get help" do
get static_pages_help_url
assert_response :success
assert_select "title" , "Help | #{@base_title}"
end
test "should get about" do
get static_pages_about_url
assert_response :success
assert_select "title" , "About | #{@base_title}"
end
end
테스트를 수행하여 정상적으로 결과가 나오는지 확인하도록 한다.
$ rails test
4 tests, 7 assertions, 0 failures, 0 errors, 0 skips
브랜치 머지(Merge)
다음 장으로 넘어가기 전 커밋(Commit)을 하고 마스터 브랜치(Master branch)에 머지(Merge) 하도록 하겠다. 우선 여태까지의 작업 내역을 커밋한다.
$ git add -A
$ git commit -m “Finish static pages”
다음 마스터 브랜치를 체크아웃 받고 아래처럼 머지하도록 한다.
$ git checkout master
$ git merge static-page
환경설정
minitest reporters
일반적으로 대부분의 언어에서 테스트의 실패는 RED, 성공은 GREEN으로 표시해주고 있다. 레일즈의 기본 테스트에서 RED나 GREEN을 표시하기 위해서는 아래의 코드를 테스트용 헬퍼 파일에 추가한다. 이 코드에서는 minitest-repoters라는 gem을 이용합니다.
# test/test_helper.rb
ENV[‘RAILS_ENV’] ||= ‘test’
require File.expand_path(‘../../config/environment’, __FILE__)
require ‘rails/test_help’
require “minitest/reporters” #추가
Minitest::Reporters.use! #추가
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests
# in alphabetical order.
fixtures :all
# Add more helper methods to be used by all tests here…
end
위 코드의 추가로 테스트 결과에 아래와 같이 색상이 추가된다.
Guard에 의한 테스트 자동화
테스트를 하려고 할 때마다 커맨드 라인을 활성화시켜 손으로 직접 rails test 명령어를 입력할 필요가 있다. 이 불편함을 줄이기 위해 Guard를 사용하여 테스트를 자동화해 보자. Guard는 파일 시스템의 변경을 감시합니다. 예를 들어 home.html.erb 파일이 변경되면 static_pages_controller_test.rb를 자동으로 실행하는 것을 Guard에서 설정할 수 있다. 이미 Sample 애플리케이션에서는 Gemfile에 guard gem이 선언되어 있기 때문에 초기화만 해주면 사용할 수 있을 것이다
$ bundle exec guard init
Writing new Guardfile to /home/ec2-user/environment/sample_app/Guardfile
00:51:32 - INFO - minitest guard added to Guardfile, feel free to edit it
혹시 책과 동일하게 Cloud9을 사용하고 있는 경우 Guard의 알람을 유효하게 하기 위해 tmux를 설치할 필요가 있다. Cloud9을 사용하는 경우에 다음 명령어를 이용하여 tmux를 설치하면 된다.
$ sudo yum install -y tmux # Cloud9을 사용하는 경우에는 필요
초기화를 수행하면 Guard 파일이 생성될 것이다. 코드가 갱신되면 자동적으로 적절한 테스트 케이스가 실행되게 하기 위해 생성된 Guardfile을 수정해 보도록 하자.
# Guard의 매치 패턴 선언
guard :minitest, spring: "bin/rails test", all_on_start: false do
watch(%r{^test/(.*)/?(.*)_test\.rb$})
watch('test/test_helper.rb') { 'test' }
watch('config/routes.rb') { integration_tests }
watch(%r{^app/models/(.*?)\.rb$}) do |matches|
"test/models/#{matches[1]}_test.rb"
end
watch(%r{^app/controllers/(.*?)_controller\.rb$}) do |matches|
resource_tests(matches[1])
end
watch(%r{^app/views/([^/]*?)/.*\.html\.erb$}) do |matches|
["test/controllers/#{matches[1]}_controller_test.rb"] +
integration_tests(matches[1])
end
watch(%r{^app/helpers/(.*?)_helper\.rb$}) do |matches|
integration_tests(matches[1])
end
watch('app/views/layouts/application.html.erb') do
'test/integration/site_layout_test.rb'
end
watch('app/helpers/sessions_helper.rb') do
integration_tests << 'test/helpers/sessions_helper_test.rb'
end
watch('app/controllers/sessions_controller.rb') do
['test/controllers/sessions_controller_test.rb',
'test/integration/users_login_test.rb']
end
watch('app/controllers/account_activations_controller.rb') do
'test/integration/users_signup_test.rb'
end
watch(%r{app/views/users/*}) do
resource_tests('users') +
['test/integration/microposts_interface_test.rb']
end
end
# 주어진 리소스에 대응하는 테스트 코드를 리턴
def integration_tests(resource = :all)
if resource == :all
Dir["test/integration/*"] else
Dir["test/integration/#{resource}_*.rb"]
end
end
# 주어진 리소스에 대응하는 컨트롤러 테스트를 리턴
def controller_test(resource)
"test/controllers/#{resource}_controller_test.rb"
end
# 주어진 리소스에 대응하는 모든 테스트를 리턴
def resource_tests(resource)
integration_tests(resource) << controller_test(resource)
end
Guardfild의 내용 중 이 부분을 자세히 살펴보면 Guard에서 Spring 서버를 이용하여 소스를 읽어 들이는 시간을 단축하고 있고 Guard 작동 시에 테스트 케이스를 전부 실행하지 않도록 선언하고 있다. 이때 Spring은 레일즈의 기능 중 하나이다.
guard :minitest, spring: "bin/rails test", all_on_start: false do
Guard를 사용할 때에는 Spring과 Git의 conflict를 막기 위해 .gitignore 파일에 spring 디렉터리를 아래와 같이 추가해야 한다. .gitignore은 Git의 설정 파일인데 여기에 쓰인 파일은 Git 레포지토리에 커밋되지 않는다.
# .gitignore
# See https://help.github.com/articles/ignoring-files for more about
# ignoring files.
#
# If you find yourself ignoring temporary files generated by your
# text editor or operating system, you probably want to add
# a global ignore instead:
# git config —global core.excludesfile ‘~/.gitignore_global’
# Ignore bundler config.
/.bundle
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore Byebug command history file.
.byebug_history
# Ignore Spring files. // 추가
/spring/*.pid
글을 마치며
글을 작성하면서 학습을 하려니 확실히 이해는 더 잘 되는데 시간이 너무 오래 걸리고 너무 힘들다. 그래도 어찌어찌 이 긴 3장을 모두 정리하였다. 4장에서도 미래의 내가 화이팅하기를 진심으로 응원한다😂
오늘 작성한 코드 또한 아래 GitHub에서 확인할 수 있으니 참고바란다.
참고 자료 및 사이트
- https://www.railstutorial.org/
- https://github.com/Yoodahun/Rails_Tutorials_Translation