루비 온 레일즈 v5.2 시작하기
루비 온 레일즈 v5.2 에 대한 시작하기 가이드입니다. 간단한 블로그 어플리케이션을 만들어보면서 레일즈에 대해 배웁니다. Rails Guide를 참고했습니다. 개발환경과 코드에 대해서는 아래를 참고하시기 바랍니다.
macOS Catalina
ruby 2.6.6
rails 5.2.3
hwangwoojin/rails-blog
This is my code from getting_started guide at RailsGuides v5.2 : https://guides.rubyonrails.org/v5.2/getting_started.html - hwangwoojin/rails-blog
github.com
blog 어플리케이션 시작하기
먼저 blog 어플리케이션을 새로 생성하겠습니다.
$ rails new blog
# 만약 WSL 에서 사용할 경우 --skip-spring --skip-listen 옵션 추가해서 해보기
그러면 blog 라는 이름의 폴더가 생성됩니다. 해당 폴더로 이동합니다.
$ cd blog
잘 생성되었는지 확인하기 위해 웹서버를 켜서 접속해보도록 하겠습니다.
$ bin/rails server
# 윈도우일 경우 bin/rails 가 안된다면 bin\rails 로 한번 해보세요
웹서버가 성공적으로 시작했다면 http://localhost:3000/ 에 접속했을 때 다음과 같은 화면을 볼 수 있습니다.
Hello, World! 띄우기
간단한 Hello, World! 화면을 띄우기 위해서 Ruby on Rails 에서는 컨트롤러와 뷰가 필요합니다. 먼저 컨트롤러를 생성하겠습니다. 다음 코드는 Welcome 이라는 이름과 index 라는 액션을 가진 컨트롤러를 생성합니다.
$ bin/rails generate controller Welcome index
그러면 여러 파일들이 추가됩니다. 이 파일들에 대해서는 이후에 다루어보도록 하겠습니다. 먼저 사용자가 볼 index.html 파일을 생성해야 합니다. 이때 사용하는 파일 형식은 embeded ruby 를 줄인 erb 입니다.
<!-- app/views/welcome/index.html.erb -->
<!-- 기존의 코드
<h1>Welcome#index</h1>
<p>Find me in app/views/welcome/index.html.erb</p>
-->
<h1>Hello, World!</h1>
기존의 코드는 깔끔하게 주석처리해버리고 새로 <h1>Hello, World!</h1> 를 추가했습니다. 그러면 http://localhost:3000/welcome/index 에 접속했을때 Hello, World! 가 나타납니다.
하지만 아직 http://localhost:3000/ 는 그대로인 상태입니다. 이를 변경하기 위해서는 routes.rb 파일에 root 를 추가해주어야 합니다.
# config/routes.rb
Rails.application.routes.draw do
get 'welcome/index'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
root 'welcome#index' # 새로 추가한 코드
end
그러면 이제 http://localhost:3000/ 를 접속했을 때도 Hello, World! 를 볼 수 있습니다.
Articles 만들기 (create)
Hello, World! 라는 한 문장을 보여주는 것을 blog 라고 부르지는 않죠. 보통 블로그에는 글(Articles) 이 존재합니다. 이번에는 이 Article 을 만들어보도록 하겠습니다.
Articles 은 하나의 리소스입니다. 리소스라는건 만들고, 읽고, 쓰고 삭제할 수 있는 데이터라는 것이죠. 이를 다른말로 CRUD 할 수 있다고도 합니다. 각각 create, read, update, delete 를 말합니다. Ruby on Rails 는 매우 RESTful 하기 때문에 다음과 같이 routes 에 resources 를 선언하면 됩니다.
# config/routes.rb
Rails.application.routes.draw do
get 'welcome/index'
resources :articles # 새로 추가한 코드
root 'welcome#index'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
Articles 을 위한 컨트롤러도 생성해줍니다. 그러면 또 이것저것 생성이 됩니다.
$ bin/rails generate controller Articles
Articles 를 위한 메서드는 articles_controller.rb 에 정의되어 있습니다. 처음에는 아무것도 없습니다. 여기에 새로 new 라는 메서드를 선언하도록 하겠습니다.
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
# 새로 추가한 메서드
def new
end
end
뷰도 만들어줍니다.
<!-- app/views/articles/new.html -->
<h1>New Article</h1>
그러면 http://localhost:3000/articles/new 에 접속했을 때 New Article 이 정상적으로 나타납니다.
Ruby on Rails 는 form 이란 것을 지원합니다. 이것은 form_with 라는 것을 통해 페이지를 아주 쉽게쉽게 만들 수 있는 일종의 템플릿같은 개념입니다. 아까 new.html 을 만든 같은 위치에 new.html.erb 파일을 생성하고 다음 코드를 입력합니다.
<!-- app/views/articles/new.html.erb -->
<%= form_with scope: :article, url: articles_path, local: true do |form| %>
<p>
<%= form.label :title %><br>
<%= form.text_field :title %>
</p>
<p>
<%= form.label :text %><br>
<%= form.text_area :text %>
</p>
<p>
<%= form.submit %>
</p>
<% end %>
그러면 다음과 같은 페이지가 짠 하고 나타납니다.
하지만 아직 Save Article 버튼은 제대로 동작하지 않습니다. 이를 제대로 동작시키기 위해서는 create 메서드를 만들어주어야 합니다. 다음과 같이 만들어주도록 하겠습니다.
# app/controllers/articles_controller.rb 에 메서드 추가
def create
@article = Article.new(params.require(:article).permit(:title, :text))
@article.save # article 이 저장되었는지에 대한 boolean 값
redirect_to @article
end
이 코드는 article 의 정보를 Article 이라는 클래스에 저장하는 코드입니다. 여기서 눈여겨보아야 할 점은 permit 입니다. Ruby on Rails 에서는 데이터를 전달할 때 모든 파라미터를 동시에 전달하는 것을 막습니다. 왜냐하면 보안적인 문제도 있고, 코드가 불필요한 파라미터를 받아서 동작을 멈출수도 있기 때문입니다. 그래서 permit 으로 원하는 파라미터만 가져오도록 합니다. 이를 Strong Parameter 이라고도 합니다. 이 코드를 좀 근사하게 쓰면 이렇게도 가능합니다.
def create
@article = Article.new(article_params)
@article.save
redirect_to @article
end
private
def article_params
params.require(:article).permit(:title, :text)
end
Article 클래스는 모델을 생성하면 자동으로 같이 생성됩니다. 모델을 만드는 코드는 다음과 같습니다. 주의해야 할게 있는데, 클래스 이름은 상수, 모델 이름은 단수, 해당 데이터베이스 테이블은 복수라는 것입니다. 생성된 모델은 app/models/article.rb 에 저장됩니다.
$ bin/rails generate model Article title:string text:text
그 다음 마이그레이션을 해주면 됩니다.
$ bin/rails db:migrate
(외부 서버에 마이그레이션할때는 아래 코드로)
$ bin/rails db:migrate RAILS_ENV=production
Article 보기 (read)
아직 article 을 볼 수는 없습니다. 왜냐하면 아직 구현하지 않았기 때문입니다. CRUD 에서 R(read) 에 해당하는 부분입니다. 이는 show 메서드로 구현할 수 있습니다. show 메서드는 다음과 같이 구현합니다.
# app/controllers/articles_controller.rb 에 메서드 추가
def show
@article = Article.find(params[:id])
end
뷰도 생성해줍시다.
<!-- app/views/articles/show.html.erb -->
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
그러면 생성한 article 들이 이렇게 나타납니다. 주소는 생성한 article 의 순서에 따라 http://localhost:3000/articles/1 이나 http://localhost:3000/articles/2 와 같습니다.
전체 글 보기도 있어야겠죠? 이는 index 메서드로 만들 수 있습니다.
# app/controllers/articles_controller.rb
# index 메서드는 가급적 젤 위에 선언해주자.
def index
@articles = Article.all
end
뷰도 만들어야 합니다. 뭐 당연하겠죠?
<!-- app/views/articles/index.html.erb -->
<h1>Listing articles</h1>
<table>
<tr>
<th>Title</th>
<th>Text</th>
<th></th>
</tr>
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.text %></td>
<td><%= link_to 'Show', article_path(article) %></td>
</tr>
<% end %>
</table>
다음과 같은 화면이 나타나면 성공입니다.
만들려고 했던 페이지는 모두 만들었습니다. 이제 페이지간 이동을 위해 링크를 만들어주겠습니다. 간단하니까 코드를 보시기 바래요.
<!-- app/views/welcome/index.html/erb 에 추가 -->
<!-- /articles 로 이동 -->
<%= link_to 'My Blog', controller: 'articles' %>
controller: 는 생략가능합니다. Ruby on Rails 는 똑똑하기 때문이죠.
<!-- app/views/articles/index.html.erb 에 추가 -->
<!-- 누르면 articles/new 로 이동 -->
<%= link_to 'New article', new_article_path %>
<!-- app/views/articles/show.html.erb 에 추가 -->
<!-- 이전 페이지로 갑니다 -->
<%= link_to 'Back', articles_path %>
<!-- app/views/articles/new.html.erb 에 추가 -->
<!-- 이전 페이지로 갑니다 -->
<%= link_to 'Back', articles_path %>
여기까지 Article 을 간단하게 만들어 보았습니다.
Article 수정하기 (update)
위에서 Article 을 만들고 읽는 것을 했었죠. CRUD 에서 C(create) 와 R(read) 에 해당하는 부분들이죠. 이제 나머지 U(update) 와 D(delete) 를 할 차례입니다.
먼저 만들어 볼 것은 article 을 수정하는 것입니다. article 을 수정할 때 입력하는 페이지를 만들어야 합니다. 지금까지 했던것과 크게 다르지 않습니다. 컨트롤러에 edit 메서드를 추가하면 됩니다. 해당 컨트롤러에 대한 뷰도 만들구요.
# app/controllers/articles_controller.rb 에 추가
def edit
@article = Article.find(params[:id])
end
<!-- app/views/articles/edit.html.erb -->
<h1>Edit article</h1>
<%= form_with(model: @article, local: true) do |form| %>
<% if @article.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(@article.errors.count, "error") %> prohibited
this article from being saved:
</h2>
<ul>
<% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= form.label :title %><br>
<%= form.text_field :title %>
</p>
<p>
<%= form.label :text %><br>
<%= form.text_area :text %>
</p>
<p>
<%= form.submit %>
</p>
<% end %>
<%= link_to 'Back', articles_path %>
그럼 이제 update 메서드를 구현하면 됩니다. 여기서는 만약 업데이트가 되었다면 해당 글로 이동하고, 아니면 다시 edit 페이지로 이동하는 방법을 사용하겠습니다.
def update
@article = Article.find(params[:id])
if @article.update(article_params)
redirect_to @article
else
render 'edit'
end
end
거의 다 완성되었습니다. 이제 article 을 보여주는 index 뷰에 edit 을 위한 링크를 추가해주면 됩니다.
<!-- app/views/articles/index.html.erb -->
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.text %></td>
<td><%= link_to 'Show', article_path(article) %></td>
<!-- 추가한 코드 -->
<td><%= link_to 'Edit', edit_article_path(article) %></td>
</tr>
<% end %>
show 뷰에도 링크를 추가해주겠습니다.
<!-- app/views/articles/show.html.erb -->
...
<%= link_to 'Edit', edit_article_path(@article) %>
여기까지 아무 문제없이 수행됩니다. 그런데 edit.html.erb 를 잘 보면 new.html.erb 파일과 되게 비슷합니다. 그도 그럴것이 new 나 edit 이나 어쨋든 article 에 대한 정보를 입력하는 부분이었으니까요. 루비의 중요한 철학중 하나는 같은 코드는 딱 한번만 사용하자 입니다. 그런 의미에서 edit 과 new 를 하나로 합쳐보겠습니다. 이를 위해서는 _form.html.erb 파일이 필요합니다.
<!-- app/views/articles/_form.html.erb -->
<%= form_with model: @article, local: true do |form| %>
<% if @article.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(@article.errors.count, "error") %> prohibited
this article from being saved:
</h2>
<ul>
<% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= form.label :title %><br>
<%= form.text_field :title %>
</p>
<p>
<%= form.label :text %><br>
<%= form.text_area :text %>
</p>
<p>
<%= form.submit %>
</p>
<% end %>
new 와 edit 의 중복된 코드가 _form.html.erb 에 들어갔습니다. 그러면 이제 new.html.erb 와 edit.html.erb 코드를 다음과 같이 바꾸어버리면 됩니다. 긴 코드는 다 지워버리자구요.
<!-- app/views/articles/new.html.erb -->
<h1>New article</h1>
<%= render 'form' %>
<%= link_to 'Back', articles_path %>
<!-- app/views/articles/edit.html.erb -->
<h1>Edit article</h1>
<%= render 'form' %>
<%= link_to 'Back', articles_path %>
어떄요, Ruby on Rails 재밌죠?
Article 삭제하기 (delete)
Article 을 삭제하려면 destroy 메서드를 선언하면 됩니다. 당연히 뷰도 만들어야겠죠? 간단하므로 코드로 남기겠습니다.
# app/controllers/articles_controller.rb
...
def destroy
@article = Article.find(params[:id])
@article.destroy
end
<!-- app/views/articles/index.html.erb -->
<% @articles.each do |article| %>
<tr>
...
<!-- 추가한 destroy 코드 -->
<td><%= link_to 'Destroy', article_path(article),
method: :delete,
data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
Article 검사하기
Article 에는 아직 구현해야 할 부분이 남아있습니다. 그 중에서는 값의 조건을 확인해보고 검증해야 할 필요가 있을 수도 있죠. 이에 대해 한번 알아보도록 하겠습니다.
articles/new 에서 Article 을 추가할 때 사용자가 Title 을 비워두는 경우가 있을 수 있습니다. 혹은 Title 의 길이제한을 두는 경우도 가능합니다. 컨트롤러에서 validates 를 사용해서 이를 제한할 수 있습니다.
# app/models/article.rb
class Article < ApplicationRecord
validates :title, presence: true, length: { minimum: 5 }
end
이 코드는 title 이 반드시 존재해야 하며, 길이는 최소 5 가 되어야 한다는 것을 의미합니다. 이제 이 제한을 만족하지 않는 Article 은 저장되지 않습니다.
그럼 이제 Article 이 문제가 없다면 해당 글을 저장한 후 보여주고, 문제가 있다면 현재 화면을 다시 로드하는 코드를 구현하는 것도 가능합니다. 이를 위해 먼저 view 를 추가하겠습니다.
<!-- app/views/articles/new.html.erb 에 추가 -->
<!-- Error 처리 -->
<% if @article.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(@article.errors.count, "error") %> prohibited
this article from being saved:
</h2>
<ul>
<% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
위에서 @article.save 가 article 이 저장되었는지에 대한 boolean 값을 저장한다고 했었죠. 이를 응용하면 근사한 컨트롤러를 만들 수 있습니다. redirect_to 가 아닌 render 를 사용하는 것에 반드시 주의하세요. redirect_to 를 사용하면 @articles 에 대한 정보가 남아있게 되어 페이지가 터집니다 문제가 발생할 수 있습니다.
# app/controllers/articles_controller.rb
# new 를 처음 눌렀을 때 Article 이 없는 경우 에러가 나는 것을 방지
def new
@article = Article.new
end
def create
@article = Article.new(params.require(:article).permit(:title, :text))
if @article.save
redirect_to @article
else
render 'new'
end
end
그러면 이제 에러가 발생했을 때 근사한 메세지를 보여줄 수 있게 됩니다.
Comment 추가하기
위에서는 Article 모델을 통해 글을 생성하고 보는 등의 작업을 수행했습니다. 글만 있으면 재미없으니까 이제 새로운 모델인 댓글을 추가하도록 하겠습니다. 터미널에 다음 명령어를 입력해줍니다.
$ bin/rails generate model Comment commenter:string body:text article:references
그러면 Comment 모델을 위한 여러 파일들이 추가됩니다. 마이그레이션도 해줍시다.
$ bin/rails db:migrate
Comment 모델을 생성할 때 article: references 가 의미하는 것은 Comment 가 Article 에 속한다는 것입니다. 쉽게 말하면 댓글은 글에 속한다는 의미입니다. 이를 코드로도 확인할 수 있습니다. app/models/comments.rb 를 보면 다음과 같이 구현되어 있습니다.
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :article
end
단 우리는 하나의 글이 여러 댓글을 가질 수 있도록 만들 예정입니다. 그래서 Article 모델을 다음과 같이 조금 바꾸어주도록 하겠습니다.
# app/models/article.rb
class Article < ApplicationRecord
has_many :comments
...
end
이정도면 모델 생성은 완료되었습니다. 이제 남은 것은 resources 와 컨트롤러를 만들어주는 것이죠. 코드는 다음과 같습니다.
# config/routes.rb
Rails.application.routes.draw do
...
# article 과 comments 의 관계에 주의
resources :articles do
resources :comments
end
...
end
# 컨트롤러 생성
$ bin/rails generate controller Comments
다음으로는 뷰를 생성해주어야 합니다. 방법은 Article 의 뷰를 생성하는 것과 똑같습니다. views 폴더에 .html.erb 파일로 html 템플릿을 만들고 controller 폴더에 .rb 파일로 메서드를 선언해 주는 것이죠. 코드는 아래와 같습니다.
<!-- app/views/articles/show.html.erb 에 추가 -->
...
<h2>Comments</h2>
<% @article.comments.each do |comment| %>
<p>
<strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
<strong>Comment:</strong>
<%= comment.body %>
</p>
<% end %>
<h2>Add a comment:</h2>
<%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %>
<p>
<%= form.label :commenter %><br>
<%= form.text_field :commenter %>
</p>
<p>
<%= form.label :body %><br>
<%= form.text_area :body %>
</p>
<p>
<%= form.submit %>
</p>
<% end %>
...
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
# 댓글이 어느 article 에 대한 것인지를 인스턴수 변수로 저장합니다.
@article = Article.find(params[:article_id])
@comment = @article.comments.create(comment_params)
redirect_to article_path(@article)
end
private
def comment_params
params.require(:comment).permit(:commenter, :body)
end
end
그러면 이제 글에 댓글도 볼 수 있습니다.
코드가 다소 지저분하다고 느꼈다면 칭찬을 드리겠습니다. 이 코드를 정리할 필요성이 좀 있는데 이를 근사한 말로 리팩토링한다고 말합니다. comment 에 대한 html 코드가 article 에 있는 것은 다소 이상하죠? 이를 먼저 고치는 걸로 걸로 합시다. 먼저 comment 를 생성하는 코드를 리팩토링합니다.
<!-- app/views/comments/_comment.html.erb -->
<p>
<strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
<strong>Comment:</strong>
<%= comment.body %>
</p>
<!-- app/views/articles/show.html.erb -->
...
<h2>Comments</h2>
<!-- 원래 여기에 있던 코드는 다 지우고 _comments.html.erb 를 render 해줍니다. -->
<%= render @article.comments %>
...
comment 를 보여주는 코드도 다음과 같이 리팩토링해주도록 하겠습니다.
<!-- app/views/comments/_form,html.erb -->
<%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %>
<p>
<%= form.label :commenter %><br>
<%= form.text_field :commenter %>
</p>
<p>
<%= form.label :body %><br>
<%= form.text_area :body %>
</p>
<p>
<%= form.submit %>
</p>
<% end %>
<!-- app/views/articles/show.html.erb -->
...
<h2>Comments</h2>
<!-- 원래 코드는 다 지우고 새로 만든 _form.html.erb 를 render 해줍니다. -->
<%= render @article.comments %>
...
코드가 훨씬 깔끔해졌습니다.
Comment 삭제하기
이번에는 comment 를 삭제하는 것을 구현해보도록 하겠습니다. article 을 삭제하는 것과 큰 차이점이 없습니다. 설명 대신 코드로 보여드리도록 하겠습니다.
<!-- app/views/comments/_comment.html.erb 에 추가 -->
...
<p>
<%= link_to 'Destroy Comment', [comment.article, comment],
method: :delete,
data: { confirm: 'Are you sure?' } %>
</p>
...
# app/controllers/comments_controller.rb 에 메서드 추가
...
def destroy
# 댓글이 속한 글을 찾는다.
@article = Article.find(params[:article_id])
# 삭제하고자 하는 댓글을 찾는다.
@comment = @article.comments.find(params[:id])
# 삭제
@comment.destroy
redirect_to article_path(@article)
end
...
위에서 작성했던 것과 크게 다를 건 없어보입니다. 동작하는 것도 문제가 없어 보이지만 사실 보이지 않는 심각한 문제가 하나 존재합니다. 바로 글이 삭제되었을 때 그 글에 속한 댓글이 삭제되지 않고 데이터베이스에 불필요하게 남아있다는 것입니다. 그러면 언젠가는 저장소에 문제가 터지겠죠? 이를 위해서는 Article 클래스의 dependent 를 설정해주어야 합니다.
# app/models/article.rb 에 dependent 추가
class Article < ApplicationRecord
has_many :comments, dependent: :destroy
...
end
그럼 이제 글을 삭제했을 때 댓글도 삭제됩니다. 정확하게 말하면, Article 의 destroy 메서드를 수행했을 때 belongs_to 관계에 있는 Comment 에서도 destroy 메서드도 함께 수행됩니다. 가이드에는 없는 내용이지만, destroy 대신 delete_all 를 사용하면 Article 이 삭제될 때 관련 Comment 들이 destroy 메서드 호출 없이 그냥 다 삭제됩니다. 참고하세요.
보안
마지막으로 보안 관련해서 몇가지 코드를 수정하고 글을 마무리하겠습니다. 지금까지 만든 blog 어플리케이션을 온라인으로 공개하면 아무나 글을 쓰거나 삭제할 수 있어서 문제가 발생할 수도 있겠죠. Ruby on Rails 에서는 이를 방지하기 위해 여러 훌륭한 옵션들을 제공합니다. 엄청 많지만 그 중에서 http_basic_authenticate_with 에 대해서 알아보겠습니다.
http_basic_authenticate_with 는 메서드를 수행할 때 이름과 비밀번호를 요구하는 간단한 보안 옵션입니다. Article 컨트롤러와 Comment 컨트롤러에 선언해주면 됩니다. Article 은 index 와 show 를 제외한 모든 것에 보안 증명이 필요하고, Comment 는 destroy 에만 설정하면 되겠죠? 코드는 다음과 같습니다.
# app/controllers/articles_controller.rb 에 한줄 추가
class ArticlesController < ApplicationController
# index 와 show 를 제외한 모든 메서드를 수행했을 때 간단한 보안 증명을 요구합니다.
http_basic_authenticate_with name: "name", password: "password", except: [:index, :show]
...
end
# app/controllers/comments_controller.rb 에 한 줄 추가
class CommentsController < ApplicationController
# index 와 show 를 제외한 모든 메서드를 수행했을 때 간단한 보안 증명을 요구합니다.
http_basic_authenticate_with name: "name", password: "password", only: :destroy
...
end