들어가기 앞서
개발 환경
- 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를 사용할 경우 책의 내용을 따라 하면 된다.
지난 글
이번 장에서는 레일즈 개발자에게 필요한 기본적인 루비 지식을 배울 것이다. 하지만 중간중간 앞장하고 이어지는 내용들이 나올 수 있어 이전 글을 먼저 읽고 오길 권장한다.
목표
레일즈는 루비(Ruby)로 작성된 프레임워크지만 이전 장까지는 루비에 대한 기초지식이 전혀 없는 상태에서 레일즈 애플리케이션의 골격을 만들고 테스트를 진행했다. 테스트 케이스를 통과할 때까지 수정을 몇 번이나 반복하는 식으로 작업을 진행해 왔지만 이런 초보적인 작업을 언제까지 할 수는 없기 때문에 루비를 배워보도록 할 것이다. 루비는 많은 기능을 가진 언어지만 레일즈 개발자에게 필요한 지식은 비교적 작은 수준이다. 또한 많은 주제가 포함된 이번 장을 한 번에 이해할 필요는 없다. 꾸준히 되새긴다고 생각하고 시작해 보자.
커스텀 헬퍼 생성
브랜치 생성
이번 장에서는 별도로 프로젝트를 생성하지 않고 3장에서 만든 Sample App에 계속해서 작업을 이어갈 것이다. Git에서 버전을 관리하고 있다면 master 브랜치에서 작업하는 것이 아닌 작업 당시의 토픽 브랜치를 작성하여 작업하는 것이 좋다. 다음 명령어를 이용하여 루비 학습 용 브랜치를 생성하겠다.
$ git checkout -b rails-flavored-ruby
Helper 맛보기
아래 소스코드는 3장에서의 레이아웃 파일과 같은 내용이다. 레일즈에서 뷰 헬퍼(View Helper)란 뷰를 위한 재사용 가능한 코드 즉, 메서드를 의미한다.
<!-- 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>
특히 이 부분은 stylesheet_link_tag를 사용하여 application.css를 모든 미디어 타입에서 사용할 수 있게 한다. 숙련된 레일즈 개발자에게 이 소스는 매우 단순한 코드지만 입문자에게는 혼란스러울 수 있는 개념이 4가지 정도 포함되어 있다. 바로 레일즈의 메서드와 괄호 없는 호출 형식, 심볼, 해시이다. 지금부터 해당 개념들에 대해 알아보자.
<%= stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track': 'reload' %>
Custom Helper
레일즈의 뷰에서는 기본적으로 다양한 메소드를 사용할 수 있지만 필요에 의해 새롭게 선언하는 것도 가능하다. 이때 새롭게 만든 메서드를 커스텀 헬퍼(Custom Helper)라고 한다. 이전에 작성한 코드를 확인해 보면 아래 부분은 :title 정의에 의존한다.
<!-- app/views/layouts/application.html.erb -->
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
그리고 :title은 다음과 같이 각 뷰 화면에서 provide에 의해 정의되어 있다.
<!-- 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>
이 때 만약 뷰 하나에서 provide를 삭제한다면 그 페이지의 고유 제목 대신에 다음과 같이 표시될 것이다.
"| Ruby on Rails Tutorial Sample App"
완전히 틀린 제목은 아니지만 앞 쪽에 불필요한 파이프 (|)가 들어가 있다. 이러한 현상을 방지하기 위해 full_title이라고 하는 헬퍼를 만들어 보도록 하겠다. full_title 헬퍼는 페이지 제목이 정의되어있지 않을 때 기본 제목인 "Ruby on Rails Tutorial Sample App"을 표시하도록 하고 페이지 제목이 정의되어 있다면 기본 내용에 파이프를 추가하여 표시하도록 한다.
# app/helper/application_helper.rb #
module ApplicationHelper
# 페이지마다 완전한 타이틀을 리턴합니다.
def full_title(page_title = '')
base_title = "Ruby on Rails Tutorial Sample App"
if page_title.empty?
base_title
else
page_title + " | " + base_title
end
end
end
이 헬퍼를 사용하여 아래와 같이 레이아웃을 심플하게 할 수 있다.
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
즉, 위 코드가 아래와 같이 바뀌게 된다.
<title><%= full_title(yield(:title)) %></title>
전체적인 소스 코드는 아래와 같다.
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title><%= full_title(yield(:title)) %></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>
full_title 헬퍼를 정의하는 것으로 Home 페이지에서는 불필요하게 표시하고 있던 "Home"이라는 단어는 표시되지 않고 기본 제목만 표시할 수 있게 되었다. Home 페이지에서는 기본 제목만 표시되도록 코드를 수정하고 이를 위한 테스트 케이스 또한 함께 수정하도록 한다. TDD 방식에 따라 테스트 케이스 먼저 수정하도록 하겠다.
# 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 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, 1 failures, 0 errors, 0 skips
테스트를 통과하기 위해서 아래와 같이 Home 페이지의 뷰에서 provide 행을 삭제한다.
<!-- 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>
다시 한번 테스트를 진행해 보면 Home 페이지에 대한 테스트가 통과하는 것을 확인할 수 있을 것이다.
문자열과 메서드
Rails Console 사용하기
이제부터 루비에 대해 알아보겠다. 레일즈는 애플리케이션을 대화형으로 조작하기 위한 커맨드 라인 툴로 레일즈 콘솔을 제공하고 있다. 레일즈 콘솔은 irb(IRB: interactive Ruby)를 확장시켜 만들어 루비의 기능도 모두 사용해 볼 수 있기 때문에 해당 툴로 학습을 진행하려고 한다. (클라우드 IDE에서 학습을 진행하는 경우 책에서 추천되는 IRB 설정이 있는데 해당 설정은 본문에서 확인 바란다.) 커맨드 라인에 rails console을 입력해서 콘솔에 접속해 보자.
$ rails console
Loading development environment
>>
주석(Comment)
루비의 주석(Comment)은 # 기호부터 시작하며 그 행의 끝나는 부분까지를 모두 주석처리 한다. 주석은 실행의 대상은 아니지만 가독성 있는 코드를 작성하는데 아주 중요 요소라고 할 수 있다. 예를 들어 아래 코드 같은 경우 주석을 통해 메서드의 목적을 정의하고 있어 메서드 전체를 다 읽지 않고도 용도를 알 수 있다.
# 페이지 별로 완전한 타이틀을 리턴합니다.
def full_title(page_title = '')
.
.
.
end
주석을 콘솔 내부에 입력하는 경우는 거의 없지만 튜토리얼에서는 학습을 위해 다음과 같이 추가해 보았다. 콘솔에 주석을 입력해도 실행 시에는 항상 무시되기 때문에 문제 될 것은 없다.
$ rails console
>> 17 + 42 # 정수의 덧셈
=> 59
문자열(String)
문자열은 웹 애플리케이션에서 가장 중요한 데이터 구조 중 하나이다. 콘솔에서 문자열에 대해 알아보도록 하자. 문자열은 큰따옴표("")로 감싸 작성할 수 있다. 이 콘솔에서는 입력한 각각의 행을 확인하여 결과를 표시하고 있으며 문자열의 경우 문자열 자신을 표시한다. + 기호를 사용하여 문자열의 결합도 가능하다.
$ rails console
>> "" # 빈 문자열
=> ""
>> "foo" # 비어있지 않은 문자열
=> "foo"
>> "foo" + "bar" # 문자열의 결합
=> "foobar"
문자열을 조합할 수 있는 다른 방법으로 식의 전개(interpolation)라고 하는 것이 있는데 #{}라고 하는 특수한 구문을 사용한다. 예를 들어 "Michael"이라고 하는 값을 first_name 변수에 대입하여 "#{first_name} Hartl"라고 하는 형태로 문자열 안에 기입하게 되면 문자열의 안에서 해당 변수가 전개된다.
>> first_name = "Michael" # 변수의 대입
=> "Michael"
>> "#{first_name} Hartl" # 문자열의 식 전개
=> "Michael Hartl"
마찬가지로 성과 이름 전부 변수에 할당하여 사용할 수도 있다.
>> first_name = "Michael"
=> "Michael"
>> last_name = "Hartl"
=> "Hartl"
>> first_name + " " + last_name # 이름과 성 사이에 공백을 넣은 것.
=> "Michael Hartl"
>> "#{first_name} #{last_name}" # 식 전개를 사용하여 결합 (위 결과와 동일)
=> "Michael Hartl"
문자열을 출력하고 싶을 경우(= 화면에 표시하고 싶을 경우) 루비에서 가장 일반적으로 사용하는 메서드는 puts이다. (put의 3인칭 단수 표현이 아닌 put string의 약자다. 원래는 put ess라고 발음했지만 최근에는 puts라고 많이 발음한다.)
>> puts "foo" # 문자열을 출력하는 경우
foo
=> nil
puts "foo"는 문자열 "foo"를 스크린에 표시하지만 리턴값으로서는 문자 그대로 아무것도 없다는 뜻의 nil을 리턴한다. nil은 "아무것도 없다"라는 것을 나타내는 루비의 특별한 값이다. 그렇지만 => nil 결과는 간소화를 위해 이제부터 생략한다. 예제에서 볼 수 있듯 puts를 사용하여 출력하면 개행문자(\n)가 자동으로 맨 마지막에 추가된다. 이는 함께 출력을 담당하는 print 메서드와의 다른 점이다.
>> print "foo" # 문자의 화면출력 (puts와 같지만 개행이 추가되지 않는다.)
foo=> nil
print 메서드는 마지막에 개행이 추가되지 않기 때문에 그 상태에서 => nil 이 연속해서 출력된다. 의도적으로 개행을 추가하고 싶을 때에는 개행문자를 사용한다. 이 개행문자와 print를 조합하면 puts와 동일한 출력을 한다.
>> print "foo\n" # puts "foo" 와 동일
foo
=> nil
지금까지의 예시에서는 전부 큰따옴표(") 문자열을 사용하였지만 루비에서는 홑따옴표(')도 지원한다. 대부분의 경우 큰따옴표과 홑따옴표 어느 쪽이든 똑같이 사용할 수 있지만 홑따옴표 문자열 안에는 식의 전개를 사용할 수 없다.
>> '#{foo} bar' # 싱글 쿼테이션 안에서는 식의 전개를 할 수 없다.
=> "\#{foo} bar"
반대로 쌍따옴표 문자열에서는 #과 같은 특수문자를 사용하고 싶을 경우 해당 문자를 백슬래시(\)로 이스케이프(escape)하여 사용할 필요가 있다. 큰따옴표 문자열로 특수문자 처리도 가능하고 식의 전개도 할 수 있다면 홑따옴표 문자열은 어디에 쓸 수 있을까? 홑따옴표는 입력한 문자를 이스케이프 하지 않고 "그대로" 사용하고 싶을 때 편리하다. 예를 들어 백슬래시 문자는 개행문자와 마찬가지로 많은 시스템상에서 특수문자로 다뤄진다. 홑따옴표로 문자열을 감싸면 간단하게 특수문자를 그대로 변수에 넣을 수 있다.
>> '\n' # '백슬래시 n' 그대로 다룬다.
=> "\\n"
조금 더 극단적인 예를 확인해 보겠다. 루비에서 백슬래시 자체를 이스케이프 하기 위해서는 백슬래시가 하나 더 필요하기 때문에 큰따옴표 문자열 안에서는 백슬래시 문자는 두 개의 백슬래시에 의해 표시된다. 따라서 다음과 같이 이스케이스가 필요한 문자가 대량으로 발생한 경우에는 홑따옴표가 매우 편리하다.
>> 'Newlines (\n) and tabs (\t) both use the backslash character \.'
=> "Newlines (\\n) and tabs (\\t) both use the backslash character \\."
지금까지 큰따옴표와 홑따옴표의 차이를 알아보았다. 그러나 대부분의 경우에는 큰따옴표와 홑따옴표 사용에 큰 차이는 없고 실제로 일반적인 소스코드에서는 명확한 이유 없이 혼용되어 사용되는 케이스도 많다. 하지만 아는 것과 모르는 것은 큰 차이니 어떤 점이 다른지는 알아두면 좋겠다.
객체와 메시지 송수신
루비에서는 거의 모든 것이 객체(Object)이다. 조금 전 배웠던 문자열이나 nil 또한 객체이다. 객체에 관한 의미나 정의는 책을 읽어보기만 해서는 한 번에 이해하기 어려울 수 있기 때문에 많은 예시를 통해 "객체란 무엇인가"에 대한 직감을 기를 필요가 있다. 반면 객체가 무엇인지 설명하는 것은 간단하다. 객체라는 것은 (어떠한 경우에도) 메시지에 응답하는 것이다. 예를 들어 문자열과 같은 객체는 length라는 메세지에 응답할 수 있다. 이 메시지는 문자열의 문자수를 반환한다.
>> "foobar".length # 문자열에 "length" 라고 하는 메세지를 보냄.
=> 6
객체에 넘겨지는 메시지는 일반적으로 메서드이며 해당 객체 안에 정의되어 있다. 루비에서 문자열은 다음과 같이 empty? 메서드에도 응답할 수 있다. empty? 메서드를 보면 물음표가 붙어있는 것을 확인할 수 있는데 루비에서는 메서드가 true 혹은 false라는 논리식(boolean)을 반환하는 경우 물음표를 붙이는 관습이 있다.
>> "foobar".empty?
=> false
>> "".empty?
=> true
논리값은 제어의 흐름을 변경할 때 특히 유용하다.
>> s = "foobar"
>> if s.empty?
>> "The string is empty"
>> else
>> "The string is nonempty"
>> end
=> "The string is nonempty"
또한 논리식은 각각 &&(and) 나 ||(or), !(not) 연산자로 나타낼 수 있다.
>> x = "foo"
=> "foo"
>> y = ""
=> ""
>> puts "Both strings are empty" if x.empty? && y.empty?
=> nil
>> puts "One of the strings is empty" if x.empty? || y.empty?
"One of the strings is empty"
=> nil
>> puts "x is not empty" if !x.empty?
"x is not empty"
=> nil
앞서 말했듯이 루비에서는 대부분이 객체이며 nil 또한 객체이기 때문에 많은 메서드를 다룬다. 거의 대부분의 객체를 문자열로 반환하는 to_s 메서드를 사용하여 nil을 문자열로 변환해 보자. 빈 문자열이 출력되는 것을 확인할 수 있다.
>> nil.to_s
=> ""
이번에는 nil에 대한 메서드를 체인(Chain) 형태로 넘겨보자. nil 객체는 empty? 메소드를 사용할 수 없지만 nil.to_s를 통해 사용할 수 있다.
>> nil.empty?
NoMethodError: undefined method `empty?' for nil:NilClass
>> nil.to_s.empty? # 메소드 체인
=> true
객체가 nil 인지 아닌지 확인하는 메서드도 있다.
>> "foo".nil?
=> false
>> "".nil?
=> false
>> nil.nil?
=> true
다음 코드는 if 키워드의 다른 사용법을 표현하고 있다. 루비에서는 이렇게 뒤에 오는 if의 조건식이 참일 때만 실행되는 식을 작성하는 것이 가능하다.
puts "x is not empty" if !x.empty?
위와 같은 방법을 사용하면 간결하게 코드를 작성할 수 있다. 아래와 같이 unless도 표현할 수 있다.
>> string = "foobar"
>> puts "The string '#{string}' is nonempty." unless string.empty?
The string 'foobar' is nonempty.
=> nil
루비에서 nil은 특별한 객체이다. 루비의 객체 중 그 자체의 값이 논리값 false가 되는 것은 false와 nil 2개밖에 없다. 또한 !!라는 연산자를 사용하면 그 객체를 2번 부정하는 것이 되기 때문에 어떠한 객체도 강제적으로 논리값을 변환할 수 있다.
>> !!nil
=> false
이 외에 많은 루비 객체는 0 또한 true를 반환한다.
>> !!0
=> true
메서드 정의
레일즈의 콘솔에서도 Home 액션이나 full_title 헬퍼와 같은 방법으로 메서드를 정의할 수 있다. 예를 들어 매개변수 1개를 받아 매개변수가 비어있는지 어떤지를 확인하는 string_message라는 메서드를 정의해 보자. (메서드의 정의를 콘솔에서 하는 것은 조금은 귀찮지만 간단한 것 정도는 가능하다.)
>> def string_message(str = '')
>> if str.empty?
>> "It's an empty string!"
>> else
>> "The string is nonempty."
>> end
>> end
=> :string_message
>> puts string_message("foobar")
The string is nonempty.
>> puts string_message("")
It's an empty string!
>> puts string_message
It's an empty string!
마지막 예시를 보면 알겠지만 메서드 호출 시 괄호와 매개변수를 생략할 수 있다. 매개변수 생략이 가능한 이유는 첫 번째 줄에 매개변수의 기본값을 포함하고 있기 때문이다. 이렇게 지정해 놓으면 str 변수에 매개변수를 지정하는 것도 지정하지 않은 것도 가능하다. 파라미터를 넘기지 않는 경우에는 지정된 디폴트 값인 빈 값이 자동으로 설정된다.
여기서 루비의 메서드에는 "암묵적인 리턴값이 있다" 라는 것에 주의해야한다. 이것은 메서드 내부에서 식의 값이 자동으로 리턴된다는 것을 의미하는데 위의 예시의 경우 매개변수 str이 비어있는지 아닌지에 따라 두 가지 문자열 중 하나를 리턴한다. 물론 루비에서는 리턴값을 명시적으로 지정하는 것도 가능하다. 다음 메서드는 위의 코드와 같은 결과를 리턴한다.
>> def string_message(str = ‘’)
>> return “It’s an empty string!” if str.empty?
>> return “The string is nonempty.”
>> end
눈치챘을지도 모르겠지만 2번째의 return은 사실 없어도 그만이다. 메서드 내부 마지막에 있는 식은 return 키워드가 없어도 암묵적으로 값을 리턴한다. 여기서는 양쪽에 return을 사용하는 것이 대칭성이 있고 보기 쉽기 때문에 바람직하다고 할 수 있겠다. 또한, 메서드에서 매개변수의 변수 이름에 어떠한 것을 사용해도 호출하는 쪽에는 아무런 영향이 없다는 점을 알아두길 바란다. 즉, 맨 첫 줄의 str을 다른 변수명으로 변경하여도 메서드를 호출하는 입장에서는 똑같이 동작한다.
>> def string_message(the_function_argument = ‘’)
>> if the_function_argument.empty?
>> “It’s an empty string!”
>> else
>> “The string is nonempty.”
>> end
>> end
=> :string_message
>> puts string_message(“”)
It’s an empty string!
>> puts string_message(“foobar”)
The string is nonempty.
다시 보는 Title Helper
위 내용으로 full_title 헬퍼의 코드를 다시 한번 이해해 보자. 각 행에 대한 설명은 주석으로 표시해 두었으니 코드와 함께 확인하기 바란다. 아래 코드에서 정체를 파악할 수 없는 module ApplicationHelper 요소에 대해서도 간단히 설명을 하자면 모듈은 관련된 메서드를 모아놓은 단위이다. include 메서드를 사용하여 모듈을 읽어 들이는 것이 가능하며 이를 mix in 이라고도 부른다. 간단히 루비의 코드를 작성하는 것이라면 모듈을 작성할 때마다 명시적으로 선언하여 읽어 들이는 것이 보통이지만 레일즈에서는 자동적으로 헬퍼 모듈을 읽어 들이기 때문에 include 키워드를 사용할 필요가 없다. 즉 full_title 메서드는 자동적으로 모든 뷰에서 사용하는 것이 가능하다.
module ApplicationHelper
# 각 페이지 당 완전한 페이지 타이틀을 리턴합니다. # 주석 행
def full_title(page_title = ‘’) # 메서드 정의와 파라미터
base_title = “Ruby on Rails Tutorial Sample App” # 변수 대입
if page_title.empty? # 논리값 확인
base_title # 암묵적 리턴값
else
page_title + “ | “ + base_title # 문자열의 결합
end
end
end
다른 데이터 구조
배열과 범위 연산자
배열(Array) 은 특정의 순서를 가진 요소의 리스트다. 튜토리얼에서는 지금까지 배열에 대해 설명하지 않았지만 배열을 이해하는 것 해시나 레일즈의 데이터 모델을 이해하기 위해 중요한 기반이 된다. split 메서드를 사용하면 문자열을 변환한 배열을 얻을 수 있다.
>> “foo bar baz”.split # 문자열을 3개의 요소로 분할
=> [“foo” “bar” “baz”]
이 조작에 의해 3개의 문자열로 구성된 배열을 얻을 수 있다. split으로 문자열을 잘라내어 배열로 변환할 때는 공백이 기본값으로 설정되어 있지만 다음과 같이 다른 문자를 지정하여 잘라낼 수도 있다.
>> “fooxbarxbaz”.split(‘x’)
=> [“foo” “bar” “baz”]
많은 프로그래밍 언어와 마찬가지로 루비의 배열은 0부터 시작한다.
>> a = [42, 8, 17]
=> [42, 8, 17]
>> a[0] # Ruby에서는 각 괄호를 이용하여 배열의 요소에 접근할 수 있다.
=> 42
>> a[1]
=> 8
>> a[2]
=> 17
>> a[-1] # 배열의 인덱스는 마이너스를 지정할 수도 있다.
=> 17
위에서 알 수 있듯이 배열의 요소에 접근하기 위해서는 각괄호를 사용한다. 루비에서는 각괄호 이외에도 배열의 요소를 접근하기 위한 방법을 제공하고 있다. 마지막 줄에서는 같은 것을 확인하는 비교연산자 == 을 사용해 보았다.
>> a # 배열 a의 내용을 확인한다.
=> [42, 8, 17]
>> a.first
=> 42
>> a.second
=> 8
>> a.last
=> 17
>> a.last == a[-1] # 이퀄기호 2개를 이용하여 비교할 수 있음.
=> true
== (같다), != (같지 않음) 등의 비교 연산자는 다른 많은 프로그래밍 언어와 같다.
>> x = a.length # 배열도 문자열과 마찬가지로, length를 사용할 수 있다.
=> 3
>> x == 3
=> true
>> x == 1
=> false
>> x != 1
=> true
>> x >= 1
=> true
>> x < 1
=> false
배열은 length 이외에도 많은 여러 가지 메서드를 사용할 수 있다. 아래 예제에서 어떤 메소드를 실행시켜도 a 자체는 변경되지 않는 점을 기억하길 바란다.
>> a
=> [42, 8, 17]
>> a.empty?
=> false
>> a.include?(42)
=> true
>> a.sort
=> [8, 17, 42]
>> a.reverse
=> [17, 8, 42]
>> a.shuffle
=> [17, 42, 8]
>> a
=> [42, 8, 17]
배열의 내용을 변경하고 싶을 때에는 해당 메서드에 대응하는 “강제적” 메소드를 사용한다. 강제 메소드의 이름에는 원래 메소드 마지막에 느낌표(!) 를 붙여 사용한다.
>> a
=> [42, 8, 17]
>> a.sort!
=> [8, 17, 42]
>> a
=> [8, 17, 42]
또한 push 메소드 혹은 같은 기능의 << 연산자를 사용하여 배열에 요소를 추가할 수 있다. 마지막 라인에서는 요소의 추가를 체인으로 연결하고 있는 것을 볼 수 있다. 많은 프로그래밍 언어의 배열과는 다르게 루비에서는 다른 데이터형을 같은 배열 안에 넣을 수 있다. 아래 예시의 경우 정수와 문자열이 한 배열에 들어가 있다.
>> a.push(6) # 6을 배열에 추가한다.
=> [42, 8, 17, 6]
>> a << 7 # 7을 배열에 추가한다.
=> [42, 8, 17, 6, 7]
>> a << “foo” << “bar” # 배열에 연속하여 추가한다.
=> [42, 8, 17, 6, 7, “foo”, “bar”]
앞서 문자열을 배열로 변환하는 split 메서드를 사용했었다. join 메서드는 반대로 동작한다.
>> a
=> [42, 8, 17, 6, 7, “foo”, “bar”]
>> a.join # 단순 연결
=> “4281767foobar”
>> a.join(‘, ‘) # 쉼표+스페이스를 사용하여 연결
=> “42, 8, 17, 6, 7, foo, bar”
범위(range)는 배열과 밀접한 관계를 가지고 있다. to_a 메서드를 사용하여 배열로 변환하면 이해하기 쉬울 것이다. 이때 0..9는 범위로서는 유효하지만 2번째 코드를 통해 메서드를 호출할 때에 괄호로 감싸줄 필요가 있다는 걸 알 수 있다.
>> 0..9
=> 0..9
>> 0..9.to_a # 9에 대해 to_a를 호출하고 있습니다.
NoMethodError: undefined method `to_a’ for 9:Fixnum
>> (0..9).to_a # 둥근 괄호를 사용하여 범위 오브젝트로서 to_a 메소드를 호출합니다.
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
범위는 배열의 요소를 꺼낼 때 편리하게 사용할 수 있다.
>> a = %w[foo bar baz quux] # %w를 사용하여 문자열 배열로 변환
=> [“foo”, “bar”, “baz”, “quux”]
>> a[0..2]
=> [“foo”, “bar”, “baz”]
인덱스로 음수를 사용하면 더욱 편리하게 배열 요소에 접근할 수 있다. 아래 예시처럼 -1을 사용하면 배열의 길이를 몰라도 배열의 맨 마지막 요소를 지정할 수 있으며 이로 인해 배열을 특정 시작 위치부터 맨 마지막 요소까지 한 번에 선택할 수도 있다.
>> a = (0..9).to_a
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>> a[2..(a.length-1)] # 명시적으로 배열의 길이를 이용하여 요소 선택.
=> [2, 3, 4, 5, 6, 7, 8, 9]
>> a[2..-1] # 인덱스에 -1를 사용하여 선택
=> [2, 3, 4, 5, 6, 7, 8, 9]
다음과 같이 문자열에 대해서도 범위 객체로 다룰 수 있다.
>> ('a'..'e').to_a
=> ["a", "b", "c", "d", "e"]
본문 1.5.4에서 랜덤한 서브도메인을 생성하기 위해 다음과 같은 루비 코드를 소개한 적이 있다. 당시에는 이해가 어려웠겠지만 지금은 가능할 것이다.
('a'..'z').to_a.shuffle[0..7].join
>> ('a'..'z').to_a # 영어로 이루어진 배열을 작성
=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
"p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
>> ('a'..'z').to_a.shuffle # 순서를 셔플
=> ["c", "g", "l", "k", "h", "z", "s", "i", "n", "d", "y", "u", "t", "j", "q",
"b", "r", "o", "f", "e", "w", "v", "m", "a", "x", "p"]
>> ('a'..'z').to_a.shuffle[0..7] # 배열의 앞 8개 요소를 추려낸다
=> ["f", "w", "i", "a", "h", "p", "c", "x"]
>> ('a'..'z').to_a.shuffle[0..7].join # 추려낸 요스를 하나의 문자열로 만든다.
=> "mznpybuj"
블록
배열과 범위는 블록을 포함하는 여러 가지 메서드에 대해 대응할 수 있다. 블록은 루비의 매우 강력한 기능이기도 하며 이해하기 어려운 기능이기도 하다. 다음 코드에서는 범위 객체인 (1..5)에 대해 each 메서드를 호출하고 있다. 메서드에 전달되는 { |i| puts 2 * i }는 블록이라고 불리는 부분이다. |i| 에서 변수를 파이프로 감싸고 있는데 이것은 블록변수를 다루는 루비의 문법으로 블록을 조작할 때 사용하는 변수를 지정한다. 이 경우 범위 객체의 each 메서드는 i라고 하는 하나의 로컬변수를 사용하여 블록을 조작할 수 있다. 그리고 범위에 포함되는 각각의 값을 i 변수에 대입하여 블록을 실행한다.
>> (1..5).each { |i| puts 2 * i }
2
4
6
8
10
=> 1..5
블록을 나타내기 위해선 () 괄호를 사용하여 감싸지만 다음과 같이 do와 end로 감싸 표현할 수도 있다.
>> (1..5).each do |i|
?> puts 2 * i
>> end
2
4
6
8
10
=> 1..5
블록에서는 여러 행을 기술할 수 있고 실제 대부분의 블록은 여러 행으로 이루어져 있다. 루비 공통 관습으로는 짧은 1행의 블록에는 괄호를 사용하고 긴 1행이나 여러 행의 블록에는 do..end 문법을 사용한다. 아래서 블록 변수로 i 대신에 number를 사용했듯이 변수명으로 반드시 i를 사용할 필요는 없다.
>> (1..5).each do |number|
?> puts 2 * number
>> puts '--'
>> end
2
--
4
--
6
--
8
--
10
--
=> 1..5
보기와는 달리 쓰임새가 많은 블록을 충분하게 이해하기 위해서는 상당한 경험이 필요한다. 블록과 관련된 코드를 최대한 많이 읽어서 이해하는 수밖에 없다. 다행스럽게도 인간에게는 각각의 사례를 일반화하는 능력이 있다. 참고를 위해서 map 메서드 등을 사용한 블록의 사용예시를 몇 가지 소개하겠다. 참고로 map 메서드는 주어진 블록을 배열이나 범위 오브젝트의 각 요소에 대해 적용하여 그 결과를 리턴하는 메서드이다
>> 3.times { puts "Betelgeuse!" } # 3. times에서는 블록에 변수를 사용하지 않음
"Betelgeuse!"
"Betelgeuse!"
"Betelgeuse!"
=> 3
>> (1..5).map { |i| i**2 } # "**"기법은 멱승(거듭제곱)
=> [1, 4, 9, 16, 25]
>> %w[a b c] # %w로 문자열 배열 작성
=> ["a", "b", "c"]
>> %w[a b c].map { |char| char.upcase }
=> ["A", "B", "C"]
>> %w[A B C].map { |char| char.downcase }
=> ["a", "b", "c"]
>> %w[A B C].map(&:downcase)
=> [“a”, “b”, “c”]
map의 블록 내부에 선언한 매개변수에 대해 메서드를 호출하는 경우 생략기법이 일반적으로 사용되어 다음과 같이 작성하는 것도 가능하다. 이러한 기법을 symbol-to-proc 이라고도 한다.
>> %w[A B C].map { |char| char.downcase }
=> [“a”, “b”, “c”]
>> %w[A B C].map(&:downcase)
=> [“a”, “b”, “c”]
마지막으로 Unit Test를 살펴보자. 동작을 세세하게 이해할 필요는 없다. 여기서 중요한 점은 테스트 코드에 do 키워드가 사용된다는 점이고 거기에 "테스트 코드가 블록으로 되어있다"라는 것이다. 즉, 해당 test 메서드는 문자열(설명문)과 블록을 매개변수로 삼아 테스트를 실행할 때 블록 내부의 문법이 실행되는 것으로 이해할 수 있다.
test “should get home” do
get static_pages_home_url
assert_response :success
assert_select “title”, “Ruby on Rails Tutorial Sample App”
end
본문 1.5.4에서 랜덤한 서브도메인을 생성하기 위해 다음 코드를 소개한 적이 있다. 당시에는 이해가 어려웠을 수 있지만 지금은 이 코드를 이해하기 위한 준비가 끝났기 때문에 다시 한번 더 읽어보자.
('a'..'z').to_a.shuffle[0..7].join
실행 순서를 확인해보면 조금은 이해하기 쉬워질 것이다.
>> ('a'..'z').to_a # 영어로 이루어진 배열을 작성
=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
"p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
>> ('a'..'z').to_a.shuffle # 순서를 셔플
=> ["c", "g", "l", "k", "h", "z", "s", "i", "n", "d", "y", "u", "t", "j", "q",
"b", "r", "o", "f", "e", "w", "v", "m", "a", "x", "p"]
>> ('a'..'z').to_a.shuffle[0..7] # 배열의 앞 8개 요소를 추려낸다
=> ["f", "w", "i", "a", "h", "p", "c", "x"]
>> ('a'..'z').to_a.shuffle[0..7].join # 추려낸 요스를 하나의 문자열로 만든다.
=> "mznpybuj"
해시와 심볼
해시는 본질적으로는 배열과 같지만 인덱스로 정수 값 이외의 값도 사용할 수 있는 점이 배열과 다르다. Perl 등의 다른 언어에서는 이와 같은 이유로 해시를 연상배열이라고 부르기도 한다. 주로 키(Key)라고 불리는 해시의 인덱스는 보통 객체의 형태다. 예를 들어 다음과 같이 문자열을 키로 사용한다. 해시는 키값에 대한 값을 대괄호로 감싸 표시하며 키와 값의 짝을 가지지 않은 대괄호는 빈 해시이다. 여기서 중요한 점은 해시의 대괄호는 블록의 대괄호와 전혀 다르다는 점이다. 해시는 배열과 비슷하지만 순서가 보장되어 있지 않다. 따라서 순서가 중요한 목록이라면 배열을 사용할 필요가 있다.
>> user = {} # {}는 빈 해시
=> {}
>> user[“first_name”] = “Michael” # Key는 “first_name” . Value는 “Michael”
=> “Michael”
>> user[“last_name”] = “Hartl” # Key는 “last_name”. Value는 “Hartl”
=> “Hartl”
>> user[“first_name”] # 요소에 접근하는 것은 배열과 비슷함.
=> “Michael”
>> user # 해시의 내용 표현
=> {“last_name”=>”Hartl”, “first_name”=>”Michael”}
다음과 같이 해시로켓(=>)을 이용하면 더욱 간단하게 해시를 표현할 수 있다. 루비에서는 관습적으로 해시의 제일 첫 번째 값과 제일 마지막 값에 공백을 추가한다. 이 공백은 있어도 없어도 그만이며 콘솔에서는 무시되어 표시된다. 어디서 시작한 관습인지는 명확하지 않지만 관습이니 알아두길 바란다.
>> user = { "first_name" => "Michael", "last_name" => "Hartl" }
=> {"last_name"=>"Hartl", "first_name"=>"Michael"}
여기까지 해시의 키로 문자열을 사용했지만 레일즈에서는 문자열보다도 심볼을 사용하는 편이 일반적이다. 심볼은 문자열과 비슷하지만 큰따옴표로 감싸는 대신에 콜론을 앞에 놓는 점이 다르다. 예를 들어 :name 은 심볼이다. 물론 일단은 심볼을 단순한 문자열이라고 생각해도 상관없다.
>> “name”.split(‘’)
=> [“n”, “a”, “m”, “e”]
>> :name.split(‘’)
NoMethodError: undefined method `split’ for :name:Symbol
>> “foobar”.reverse
=> “raboof”
>> :foobar.reverse
NoMethodError: undefined method `reverse' for :foobar:Symbol
심볼은 매우 소수의 언어에서만 쓰이지 않는 특수한 데이터 형식이다. 처음에는 신기할 수도 있겠지만 레일즈에서는 심볼을 매우 흔하게 사용하고 있기 때문에 점차 익숙해질 것이다. 문자열과는 다르게 모든 문자를 심볼로 사용할 수 있는 것은 아니기에 주의가 필요하나 일반적인 알파벳은 모두 심볼로 사용할 수 있기 때문에 큰 문제는 없을 것이다.
>> :foo-bar
NameError: undefined local variable or method `bar’ for main:Object
>> :2foo
SyntaxError
해시의 키로 심볼을 쓰는 경우 user의 해시는 다음과 같이 정의할 수 있다. 마지막 예시를 보면 정의되지 않은 해시값은 단순하게 nil이라는 것도 알 수 있다.
>> user = { :name => “Michael Hartl”, :email => “michael@example.com” }
=> {:name=>”Michael Hartl”, :email=>”michael@example.com”}
>> user[:name] # :name 의 밸류값을 액세스.
=> “Michael Hartl”
>> user[:password] # 정의되지 않은 키의 밸류값에 액세스.
=> nil
해시에서는 심볼을 키로 사용하는 것이 일반적이기에 루비 1.9에서 새로운 문법이 지원되기 시작되었다. 심볼과 해시로켓의 조합을 2번째 예시처럼 키의 이름 이후 콜론(:)을 놓고 그다음에 값이 오도록 하였다. 이러한 구성은 JavaScript 등 다른 언어의 해시 기법과 매우 닮아 있어 레일즈 커뮤니티에서도 인기가 높다. 두 문법 모두 자주 사용되기 때문에 두 가지 문법을 구분하는 것이 중요한다. 굳이 차이점을 찾자면 :name은 독립된 심볼이지만 파라미터가 없는 name:은 심볼 자체로 성립되지 않는다. 다음의 코드에서는 파라미터가 있기 때문에 name =>와 name: 의 해시 데이터 구조는 완전히 일치한다. 일반적으로 생략 기법이 바람직하지만 명시적으로 선두에 콜론을 붙여 사용하는 것이 심볼이라고 강조하는 경우도 있다.
>> h1 = { :name => "Michael Hartl", :email => "michael@example.com" }
=> {:name=>"Michael Hartl", :email=>"michael@example.com"}
>> h2 = { name: "Michael Hartl", email: "michael@example.com" }
=> {:name=>"Michael Hartl", :email=>"michael@example.com"}
>> h1 == h2
=> true
문자열 외에도 해시의 키값에는 거의 모든 값을 넣을 수 있으며 다른 해시를 사용할 수도 있다. 레일즈에서는 이러한 해시의 해시가 매우 많이 사용되고 있다.
>> params = {} # 'params' 라고 하는 해시 정의 ('parameters' 의 약자)。
=> {}
>> params[:user] = { name: "Michael Hartl", email: "mhartl@example.com" }
=> {:name=>"Michael Hartl", :email=>"mhartl@example.com"}
>> params
=> {:user=>{:name=>"Michael Hartl", :email=>"mhartl@example.com"}}
>> params[:user][:email]
=> "mhartl@example.com"
배열이나 범위 객체와 마찬가지로 해시에서도 each 메서드를 호출할 수 있다. 예제에서는 :success와 :danger라는 2가지 상태를 가지는 flash 해시를 예로 들었다. 여기서 배열의 each 메서드에서는 블록의 매개변수가 1개뿐이었지만 해시의 each에서는 블록의 파라미터가 2개로 되어 있는 것을 알 수 있다. 따라서 해시에 대하여 each 메서드가 실행되면 해시 내 1개의 “키-값” 단위로 처리를 반복한다.
>> flash = { success: "It worked!", danger: "It failed." }
=> {:success=>"It worked!", :danger=>"It failed."}
>> flash.each do |key, value|
?> puts "Key #{key.inspect} has value #{value.inspect}"
>> end
Key :success has value "It worked!"
Key :danger has value "It failed."
마지막으로 편리한 inspect 메서드를 소개하겠다. 이 메소드는 요구된 객체를 표현하는 문자열을 리턴한다.
>> puts (1..5).to_a # 배열을 문자열로써 출력
1
2
3
4
5
>> puts (1..5).to_a.inspect # 배열의 리터럴(고정값)을 출력
[1, 2, 3, 4, 5]
>> puts :name, :name.inspect
name
:name
>> puts “It worked!”, “It worked!”.inspect
It worked!
“It worked!”
객체를 표시하기 위해 inspect 메소드를 사용하는 것은 매우 자주 있는 일이기 때문에 p 메소드라고 하는 숏컷 기능도 있다.
>> p :name # 'puts :name.inspect' 와 같은 의미
:name
다시 보는 CSS
그렇다면 다시 한 번 레이아웃에 스타일시트(CSS, Cascading Style Sheet)를 추가하는 코드를 확인해 보자. 지금이라면 이 코드를 이해할 수 있게 되었을 것이다. 레일즈에서는 스타일시트를 추가하기 아래 코드처럼 stylesheet_link_tag 메서드를 호출한다. 하지만 레일즈 입문자라면 이 코드가 조금 이상하게 느껴질 수 있을 것이다. 그렇게 느껴지는 이유를 하나씩 살펴보자.
<%= stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track': 'reload' %>
첫 번째로는 소괄호가 쓰이지 않고 있다. 사실 루비에서는 소괄호를 사용하지 않아도 상관없기 때문에 다음 2개의 코드는 똑같이 실행된다.
<!-- 메소드 호출 시의 소괄호는 생략가능. -->
stylesheet_link_tag('application', media: 'all',
'data-turbolinks-track': 'reload')
stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track': 'reload'
두 번째로 media 매개변수는 해시와 유사해 보이지만 중괄호를 사용하지 않고 있다. 여기서 해당 매개변수는 해시가 맞지만 루비에서는 메서드 호출의 마지막 매개변수가 해시일 때 중괄호를 생략할 수 있어 다음의 코드는 똑같은 결과를 가진다.
<!-- 마지막 파라미터가 해시일 경우, 대괄호를 생략할 수 있다. -->
stylesheet_link_tag 'application', { media: 'all',
'data-turbolinks-track': 'reload' }
stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track': 'reload'
마지막으로 코드 중에 개행이 있음에도 불구하고 정상 동작하고 있다. 루비에서는 개행과 공백을 구별하지 않아 해당 코드가 정상적으로 동작할 수 있다. 코드를 분할하는 이유는 1행에 80문자 이상 들어있는 소스 코드의 경우 읽기 어렵기 때문이다.
아래 코드를 정리해 보자면stylesheet_link_tag 메서드에는 2개의 매개변수가 넘겨지고 있는데 첫 매개변수인 문자열은 스타일시트의 경로이다. 다음 매개변수인 해시에는 2가지 요소가 있는데 첫 번째 요소는 미디어 타입을 나타내고 마지막 요소에서는 레일즈 4.0에서 추가된 turbolink 기능을 활성화시키고 있다. 이 결과 <%= %>로 감싸져 있는 코드를 실행한 결과가 ERB의 템플릿에 삽입된다. 브라우저 상에서 위 페이지를 확인하면 입력한 스타일시트가 포함되어 있는 것을 확인할 수 있다. 이때 스타일시트 파일 이름에 ?body=1과 같은 값이 추가되어 있을 때도 있다. 이것은 레일즈에 의해 삽입된 것으로 서버상에서 변경이 있는 경우 브라우저가 파일을 다시 읽어 들이기 위한 용도로 당장은 무시해도 된다.
stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track': 'reload'
클래스
루비에서 대부분 것들은 객체라는 사실은 이미 설명했지만 이번에는 실제로 객체를 정의해 보겠다. 루비에서는 많은 객체지향언어와 마찬가지로 메서드를 모아서 정의하는 것에 클래스를 사용한다. 이 클래스로부터 인스턴스가 생성되어 객체가 되는 것이다.
생성자(Constructor)
지금까지 많은 예시에서 클래스를 통해 객체의 인스턴스를 생성하였지만 그 과정을 명시적으로 설명하진 않았다. 예를 들어 큰따옴표를 사용하여 문자열 객체 생성이 가능하지만 이것은 암묵적으로 문자열 객체를 생성하는 리터럴 생성자이다. 다음 코드에서는 문자열에 class 메서드를 호출하고 있다. 해당 메서드는 객체가 소속된 클래스를 리턴하는 것을 추측할 수 있다.
>> s = "foobar" # 큰따옴표는 사실 문자열의 생성자
=> "foobar"
>> s.class
=> String
암묵적인 리터럴 생성자를 사용하는 대신 명시적으로 생성자를 사용하는 것도 가능하다. 명시적인 생성자는 클래스 이름에 대해 new 메서드를 호출한다. 동작 자체는 리터럴 생성자와 같지만 동작의 내용이 명확하게 나타난다.
>> s = String.new("foobar") # 문자열의 new를 사용한 생성자
=> "foobar"
>> s.class
=> String
>> s == "foobar"
=> true
배열도 문자열과 마찬가지로 인스턴스를 생성할 수 있다.
>> a = Array.new([1, 3, 2])
=> [1, 3, 2]
단 해시의 경우 조금 다르다. 배열의 생성자인 Array.new는 배열의 초기값을 매개변수로 넘기는 반면 Hash.new는 해시의 디폴트 값을 매개변수로 넘긴다. 이 값은 키 값이 존재하지 않을 경우의 디폴트 값이다.
>> h = Hash.new
=> {}
>> h[:foo] # 존재하지 않는 키 (:foo) 에 대한 값을 참조
=> nil
>> h = Hash.new(0) # 존재하지 않는 키의 디폴트값을 nil이 아닌 0으로 설정
=> {}
>> h[:foo]
=> 0
메서드가 클래스 자신(이 경우에는 new)에 대해 호출되었을 때 해당 메서드를 클래스 메서드 라고 한다. 클래스의 new 메서드를 호출한 결과는 해당 클래스의 객체이며 클래스의 인스턴스라고도 부른다. length처럼 인스턴스에서 대해 호출할 수 있는 메서드를 인스턴스 메서드 라고한다.
클래스의 상속
클래스에 대해 배울 때 superclass 메서드를 사용하여 클래스의 계층을 알아본다면 이해가 쉬울 것이다. 여기서는 String 클래스의 슈퍼클래스는 Object 클래스이며 Object 클래스의 슈퍼클래스는 BasicObject 클래스이다. 또한 BasicObject 클래스는 슈퍼클래스를 가지고 있지 않다는 것을 알 수 있다.
>> s = String.new("foobar")
=> "foobar"
>> s.class # 변수s의 클래스를 확인
=> String
>> s.class.superclass # String클래스의 수퍼클래스를 확인
=> Object
>> s.class.superclass.superclass # Ruby 1.9부터 BasicObject가 도입
=> BasicObject
>> s.class.superclass.superclass.superclass
=> nil
아래의 그림은 모든 루비의 객체에서 성립한다. 클래스 계층을 거슬러 올라가면 루비의 모든 클래스는 최종적으로 슈퍼 클래스가 없는 BasicObject 클래스를 상속받고 있기 때문이다. 이것이 "루비에서는 대부분 모든 것이 객체이다"라는 말의 기술적 의미이다.
클래스에 대해 깊은 이해를 하기 위해선 자신이 직접 클래스를 만들어 보는 것이 제일 좋은 방법이다. Word라는 클래스를 작성하고 그 안에 어떠한 단어를 앞에서부터 읽어도 뒤에서부터 읽어도 똑같은 말이 된다면 true를 리턴하는 palindrome? 메서드를 작성해 보자.
>> class Word
>> def palindrome?(string)
>> string == string.reverse
>> end
>> end
=> :palindrome?
Word 클래스와 메서드는 다음과 같이 사용할 수 있다. 예시가 조금 부자연스럽게 느껴진다면 꽤나 날카롭다고 할 수 있다. 이것은 일부러 부자연스럽게 작성한 메서드이기 때문이다. 문자열을 매개변수로 넘기는 메서드를 작성하기 위해 일부러 새로운 클래스를 만드는 것은 바람직하지 않다.
>> w = Word.new # Word오브젝트를 작성한다.
=> #<Word:0x22d0b20>
>> w.palindrome?("foobar")
=> false
>> w.palindrome?("level")
=> true
단어는 문자열이기 때문에 아래 코드처럼 String 클래스를 상속받는 것이 자연스럽다. 앞에서도 언급했지만 <는 상속을 위한 루비의 키워드이다.
# 코드를 입력하기 전 기존 Word 클래스의 정의를 삭제하기 위해 레일즈 콘솔을 재시작한다.
>> class Word < String # Word클래스는 String클래스를 계승합니다.
>> # 문자열이 뒤에서부터 읽어도 똑같다면, true를 리턴합니다.
>> def palindrome?
>> self == self.reverse # self는 문자열 자신을 뜻합니다.
>> end
>> end
=> :palindrome?
💡 self 키워드
Word 클래스 내부에서 자신이 가진 단어에 접근하기 위해 self 키워드를 사용하고 있다. 루비에서는 self 키워드를 사용하여 객체 자신을 지정하는 것이 가능하다. 즉 다음 코드를 사용하여 단어가 거꾸로 해도 똑같은 단어인지 확인하고 있는 것이다.
self == self.reverse
또한 String 클래스 내부에서는 메서드나 속성을 호출할 때 self를 생략할 수 있어 다음과 같은 생략 문법도 사용할 수 있다.
self == reverse
상속을 통해 Word 클래스는 palindrome? 메서드뿐만 아니라 String 클래스의 모든 메서드를 다룰 수 있게 된다.
>> s = Word.new("level") # 새로운 Word를 만들고"level" 로 초기화합니다.
=> "level"
>> s.palindrome? # Word가 거꾸로 읽어도 같은 단어인지 확인합니다.
=> true
>> s.length # Word는String 클래스의 모든 메소드를 상속받습니다.
=> 5
Word 클래스는 String 클래스를 상속받고 있기 때문에 콘솔을 사용하여 클래스 계층을 명시적으로 확인할 수도 있다.
>> s.class
=> Word
>> s.class.superclass
=> String
>> s.class.superclass.superclass
=> Object
해당 계층을 그림으로 표현하면 다음과 같다.
기본 클래스 변경
상속은 강력한 개념이지만 String 클래스 자체에 palindrome? 메서드를 추가할 수 있다면 굳이 Word 클래스를 작성하지 않고도 아래와 같이 문자열에 대해 palindrome? 메서드 실행이 가능할 것이다. 현재는 오류가 그렇게 되어있지 않기 때문에 당연히 아래와 같은 오류가 발생한다.
>> "level".palindrome?
NoMethodError: undefined method `palindrome?' for "level":String
하지만 기본 클래스를 확장하는 것이 가능할까? 놀랍게도 루비에서는 가능하다. 루비의 클래스는 자유롭게 변경할 수 있으며 클래스 설계자가 아닌 개발자라도 기본 클래스에 메서드를 추가하는 것이 가능하다. 기본 클래스의 변경은 매우 강력한 기술이지만 큰 힘에는 큰 책임이 따르기 마련이다. 정말 타당한 이유가 없는 한 기본으로 제공되는 클래스에 메서드를 추가하는 것은 바람직하지 않다.
>> class String
>> # 문자열이 거꾸로 읽어도 같은 문자열일 경우, true를 리턴
>> def palindrome?
>> self == self.reverse
>> end
>> end
=> :String
>> "deified".palindrome?
=> true
루비의 경우에는 기본 클래스의 변경을 정당화할 수 있는 이유가 몇 가지 있다. 예를 들어 웹 애플리케이션에서는 변수가 절대로 nil이 되면 안 되는 경우가 종종 있다. 그렇기 때문에 레일즈는 blank? 메서드를 루비에 추가하고 있다. 레일즈의 확장은 자동적으로 레일즈 콘솔에도 적용되기 때문에 다음과 같이 콘솔에서도 확인할 수 있다. 다음 코드는 순수한 irb에서는 작동하지 않으니 주의하자. 스페이스로 이루어진 문자열은 빈 상태(empty)로 인식되지 않지만 공백(blank)으로는 인식되고 있다. 여기서 nil은 공백이라고 인식되고 있는데 nil은 문자열이 아니기 때문에 레일즈는 blank? 메서드를 String 클래스가 아닌 그보다 더 위의 기저클래스에 추가했다는 것을 추측해 볼 수 있다. 이때 기저 클래스라는 것은 이번 장에서 설명한 Object 클래스이다.
>> "".blank?
=> true
>> " ".empty?
=> false
>> " ".blank?
=> true
>> nil.blank?
=> true
컨트롤러 클래스
지금까지 클래스나 상속에 대해 설명했다. 사실 우리는 이미 이전 장에서 작성한 컨트롤러에서 상속이나 클래스에 대해 접해보았다.
class StaticPagesController < ApplicationController
def home
end
def help
end
def about
end
end
레일즈 콘솔에는 세션마다 로컬 레일즈 환경을 읽어 들이기 때문에 콘솔 내부에서 명시적으로 컨트롤러를 작성한다거나 해당 클래스계층을 알아볼 수도 있다. 레일즈 콘솔을 통해 StaticPagesController의 계층 구조를 확인해 보자.
>> controller = StaticPagesController.new
=> #<StaticPagesController:0x22855d0>
>> controller.class
=> StaticPagesController
>> controller.class.superclass
=> ApplicationController
>> controller.class.superclass.superclass
=> ActionController::Base
>> controller.class.superclass.superclass.superclass
=> ActionController::Metal
>> controller.class.superclass.superclass.superclass.superclass
=> AbstractController::Base
>> controller.class.superclass.superclass.superclass.superclass.superclass
=> Object
상속의 계층 구조를 도식화하면 아래와 같다.
레일즈 콘솔에서는 컨트롤러의 액션을 호출하는 것도 가능하다. 다음과 같이 home 액션을 호출했을 때 home 액션의 내부에는 아무것도 존재하지 않기 때문에 nil이 리턴된다. 여기서 중요한 점은 레일즈의 액션에는 리턴값이 없다는 것이며 리턴값이 중요하지도 않다는 것이다. 3장에서 설명했다시피home 액션은 웹 페이지를 표시하기 위한 액션이지 값을 리턴하기 위한 액션은 아니다. 게다가 3장에서는 한 번도 StaticPagesController.new를 실행하지 않았다. 어째서 객체를 생성하지도 않았음에도 동작하는 것일까? 사실 레일즈는 루비로 작성되어 있으나 루비와는 큰 관계가 없다고 볼 수 있다. 레일즈의 클래스는 보통 루비 객체와 같이 동작하지만 많은 클래스에서 레일즈만의 독특한 기능들로 구성된다. 때문에 루비와는 별개의 학습이 필요한 것이다.
>> controller.home
=> nil
클래스 생성
마지막으로 완전한 클래스를 작성해 보고 이번 장을 마무리하겠다. 6장에서 사용할 User 클래스를 처음부터 만들어보자. 먼저 애플리케션의 루트 디렉터리에 example_user.rb 파일을 생성하고 아래의 코드를 작성한다. 지금까지의 코드보다 조금 어렵게 작성되어 있어 순서대로 차근차근 확인해 보겠다.
#example_user.rb
class User
attr_accessor :name, :email
def initialize(attributes = {})
@name = attributes[:name]
@email = attributes[:email]
end
def formatted_email
"#{@name} <#{@email}>"
end
end
일단 다음 코드는 속성(attribute)인 사용자 이름과 이메일에 대응하는 접근자(accessor)를 지정한다. 접근자를 작성하는 것으로 해당 데이터를 꺼내는 메서드(getter)와 설정하는 메서드(setter)를 정의할 수 있다. 구체적으로는 아래 코드를 실행함으로써 인스턴스 변수 @name와 @email에 접근할 수 있는 메서드를 레일즈가 준비해 준다. 레일즈에서는 인스턴스 변수를 컨트롤러에 선언하는 것만으로도 뷰에서 사용할 수 있다는 점이 큰 장점이지만 일반적으로는 해당 클래스 내부라면 어디서든 접근할 수 있는 변수로 사용된다. 참고로 인스턴스 변수는 항상 @ 기호를 붙여 사용하며 아직 정의되지 않은 값은 nil이다.
attr_accessor :name, :email
다음 코드에 있는 initialize는 루비의 특수한 메서드로 User.new를 실행하면 자동적으로 호출된다. 이 경우 initialize 메서드는 다음과 같이 attribute라고 하는 매개변수 1개를 필요로 한다. 코드에서 attribute 변수는 빈 해시를 디폴트값으로 가지고 있기 때문에 이름이나 메일주소가 없는 사용자를 생성하는 것이 가능하다.
def initialize(attributes = {})
@name = attributes[:name]
@email = attributes[:email]
end
마지막으로 formatted_email 메서드를 알아보자. 이 메서드는 문자열의 식전개를 이용하여 @name, @email에 삽입된 값을 사용자의 메일주소로 재구성할 수 있다. @ 기호를 사용하는 name과 email은 인스턴스 변수이기 때문에 자동적으로 formatted_email 메서드에서 사용할 수 있다.
def formatted_email
"#{@name} <#{@email}>"
end
레일즈 콘솔에서 example_user를 require 하여 사용해 보겠다. 첫 번째 코드에서 require에 있는 '.'은 Unix의 현재 디렉터리를 표현하며 ./example_user라는 경로는 현재 디렉터리에서 상대 패스로 example_uesr 파일을 찾도록 루비에게 선언하는 것이다. 다음 코드에서는 비어 있는 User 객체로서 example을 생성한다. 그리고 대응하는 속성에 수동으로 값을 대입하여 이름과 메일주소를 부여하는데 이것은 attr_accessor를 사용하는 덕분에 가능하다. 설정된 name과 email 값은 formatted_email 메서드에서 사용한다.
>> require './example_user' # example_user의 코드를 읽어들이는 방법
=> true
>> example = User.new
=> #<User:0x224ceec @email=nil, @name=nil>
>> example.name # attributes[:name]은 존재하지 않기 때문에 nil
=> nil
>> example.name = "Example User" # 이름을 대입한다.
=> "Example User"
>> example.email = "user@example.com" # 메일주소를 대입한다.
=> "user@example.com"
>> example.formatted_email
=> "Example User <user@example.com>"
앞서 제일 마지막의 해시 매개변수는 중괄호를 생략할 수 있다고 설명했다. 마찬가지로 initialize 메서드에 해시를 넘기는 것으로 새로운 사용자를 생성할 수 있다. 이것은 일반적으로 매스 어사인먼트(mass assignment)라고 불리는 방법이며 레일즈 애플리케이션에서 자주 사용된다.
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User:0x225167c @email="mhartl@example.com",
@name="Michael Hartl">
>> user.formatted_email
=> "Michael Hartl <mhartl@example.com>"
작성한 example_user.rb 파일은 이후로는 사용하지 않으니 아래의 명령어로 삭제하겠다.
rm example_user.rb
브랜치 머지
다음 장으로 넘어가기 변경 내역을 커밋(Commit)하고 마스터 브랜치(Master branch)에 머지(Merge)하도록 하겠다.
$ git commit -am "Add a full_title helper"
$ git checkout master
$ git merge rails-flavored-ruby
습관적으로 원격 레포지토리에 푸시(Push)를 하거나 배포하기 전에 테스트 코드를 실행하여 기존 동작에 영향이 없는지 확인해 보도록 하자.
$ rails test
글을 마치며
4장에서는 아래와 같은 내용을 배울 수 있었다. 내용이 많아 정리하는데 힘들었지만 그래도 다 정리하고 보니 더 기억에 남고 뿌듯하기도 하다 😊
- 루비는 문자열을 다루는 많은 메서드를 가지고 있다.
- 루비는 모든 것이 객체이다.
- 루비는 def라고 하는 키워드를 사용하여 메서드를 정의한다.
- 루비에서는 class라고 하는 키워드를 사용하여 클래스를 정의한다.
- 레일즈의 뷰에서는 정적 HTML 말고도 ERB(Embedded Ruby)를 사용할 수 있다.
- 루비의 기본 제공 클래스에는 배열 범위 해시 등이 있다.
- 루비의 블록은 (다른 기능과 비교해서) 유난한 기능으로 데이터구조보다도 자연스러운 반복문을 사용할 수 있다.
- 심볼은 라벨로 추가적인 구조를 가지고 있지 않은(대입 등을 할 수 없다) 문자열 같은 것이다.
- 루비에서는 클래스를 상속할 수 있다.
- 루비에서는 기본 제공 클래스조차도 내부를 참조한다던지 수정할 수 있다.
오늘 작성한 코드 또한 아래 GitHub에서 확인할 수 있으니 참고 바란다.
참고 자료 및 사이트