rubyonrails.org에서 더 보기:

GitHub에서 이 파일을 읽지 마세요. 가이드는 https://guides.rubyonrails.org 에서 제공됩니다.

Rails 시작하기

이 가이드는 Ruby on Rails를 설치하고 실행하는 방법을 다룹니다.

이 가이드를 읽고 나면 다음과 같은 내용을 알 수 있습니다:

  • Rails를 설치하고, 새로운 Rails 애플리케이션을 만들고, 데이터베이스에 연결하는 방법
  • Rails 애플리케이션의 전반적인 구조
  • MVC(Model, View, Controller)와 RESTful 디자인의 기본 원칙
  • Rails 애플리케이션의 시작 구성 요소를 빠르게 생성하는 방법

1 가이드 전제 조건

이 가이드는 처음부터 Rails 애플리케이션을 만들고자 하는 초보자를 위해 작성되었습니다. Rails에 대한 사전 경험이 없다고 가정합니다.

Rails는 Ruby 프로그래밍 언어로 작성된 웹 애플리케이션 프레임워크입니다. Ruby에 대한 사전 경험이 없다면, Rails를 바로 시작하는 것은 매우 가파른 학습 곡선을 경험하게 될 것입니다. Ruby 학습을 위한 엄선된 온라인 리소스 목록이 여러 개 있습니다:

일부 리소스들은 여전히 훌륭하지만 오래된 Ruby 버전을 다루고 있어, Rails로 일상적인 개발을 할 때 보게 될 일부 구문이 포함되어 있지 않을 수 있다는 점을 유의하세요.

2 Rails란 무엇인가?

Rails는 Ruby 프로그래밍 언어로 작성된 웹 애플리케이션 개발 프레임워크입니다. 모든 개발자가 시작하는데 필요한 것들에 대한 가정을 만들어 웹 애플리케이션 프로그래밍을 더 쉽게 만들도록 설계되었습니다. 다른 많은 언어와 프레임워크에 비해 적은 코드로 더 많은 것을 할 수 있게 해줍니다. 경험 많은 Rails 개발자들은 또한 웹 애플리케이션 개발이 더 재미있어진다고 말합니다.

Rails는 독선적인(opinionated) 소프트웨어입니다. 일을 처리하는 "최선의" 방법이 있다고 가정하고, 그 방법을 권장하도록 - 그리고 경우에 따라서는 대안을 discourage하도록 - 설계되어 있습니다. "Rails 방식"을 배우면 생산성이 크게 향상되는 것을 발견하게 될 것입니다. 만약 이전의 습관을

Rails 철학은 두 가지 주요 원칙을 포함합니다:

  • Don't Repeat Yourself: DRY는 "모든 지식은 시스템 내에서 단일하고, 모호하지 않으며, 신뢰할 수 있는 표현을 가져야 한다"는 소프트웨어 개발 원칙입니다. 동일한 정보를 반복해서 작성하지 않음으로써, 우리의 코드는 더 유지보수가 쉽고, 확장 가능하며, 버그가 적어집니다.
  • Convention Over Configuration: Rails는 웹 애플리케이션에서 많은 것들을 수행하는 최선의 방법에 대한 의견을 가지고 있으며, 끝없는 설정 파일을 통해 세부사항을 지정하도록 요구하는 대신 이러한 관례들을 기본값으로 사용합니다.

3 새로운 Rails 프로젝트 생성하기

사전 구성된 Dev Container 개발 환경으로 새로운 Rails 앱을 생성할 수 있습니다. 이것이 Rails를 시작하는 가장 빠른 방법입니다. 자세한 내용은 Getting Started with Dev Containers를 참조하세요.

이 가이드를 읽는 가장 좋은 방법은 단계별로 따라하는 것입니다. 모든 단계는 이 예제 애플리케이션을 실행하는데 필수적이며 추가 코드나 단계는 필요하지 않습니다.

이 가이드를 따라가면서, blog라는 (매우) 간단한 웹로그인 Rails 프로젝트를 만들게 됩니다. 애플리케이션 구축을 시작하기 전에, Rails 자체가 설치되어 있는지 확인해야 합니다.

아래 예제들은 UNIX 계열 OS의 터미널 프롬프트를 나타내기 위해 $를 사용합니다만, 다르게 커스터마이즈되어 있을 수 있습니다. Windows를 사용하고 있다면, 프롬프트는 C:\source_code>와 같은 형태일 것입니다.

3.1 Rails 설치하기

Rails를 설치하기 전에, 시스템에 필요한 prerequisites가 올바르게 설치되어 있는지 확인해야 합니다. 다음 항목들이 포함됩니다:

  • Ruby
  • SQLite3

3.1.1 Ruby 설치하기

커맨드 라인 프롬프트를 엽니다. macOS에서는 Terminal.app을 열고, Windows에서는 시작 메뉴에서 "실행"을 선택하고 cmd.exe를 입력합니다. 달러 기호 $로 시작하는 모든 명령어는 커맨드 라인에서 실행되어야 합니다. 현재 버전의 Ruby가 설치되어 있는지 확인하세요:

$ ruby --version
ruby 3.2.0

Rails는 Ruby 버전 3.2.0 이상이 필요합니다. 최신 Ruby 버전을 사용하는 것이 권장됩니다. 만약 반환된 버전 번호가 이보다 낮다면(예: 2.3.7이나 1.8.7), 새로운 Ruby를 설치해야 합니다.

Windows에서 Rails를 설치하려면, 먼저 Ruby Installer를 설치해야 합니다.

대부분의 운영체제에서 사용할 수 있는 더 많은 설치 방법을 알아보려면 ruby-lang.org를 참고하세요.

3.1.2 SQLite3 설치하기

SQLite3 데이터베이스도 설치해야 합니다. 많은 인기있는 UNIX 계열 운영체제들은 적절한 버전의 SQLite3가 함께 제공됩니다. 다른 운영체제의 경우 SQLite3 웹사이트에서 설치 방법을 확인할 수 있습니다.

올바르게 설치되었고 PATH에 포함되어 있는지 확인하세요:

$ sqlite3 --version

이 프로그램은 자신의 version을 보고해야 합니다.

3.1.3 Rails 설치하기

Rails를 설치하려면 RubyGems가 제공하는 gem install 명령어를 사용하세요:

$ gem install rails

모든 것이 올바르게 설치되었는지 확인하기 위해, 새로운 터미널에서 다음을 실행할 수 있어야 합니다:

$ rails --version
Rails 8.1.0

Rails 8.1.0과 같은 내용이 표시되면, 계속 진행할 준비가 된 것입니다.

3.2 Blog 애플리케이션 생성하기

Rails에는 특정 작업을 시작하는데 필요한 모든 것을 생성해서 개발을 더 쉽게 만들어주도록 설계된 generators라고 하는 여러 스크립트들이 함께 제공됩니다. 그 중 하나가 new application generator인데, 이는 직접 작성할 필요 없이 새로운 Rails 애플리케이션의 기초를 제공합니다.

이 generator를 사용하려면, 터미널을 열고 파일을 생성할 권한이 있는 디렉토리로 이동한 다음 다음 명령을 실행하세요:

$ rails new blog

위 번역은 bash 명령어이므로 그대로 유지됩니다.

이것은 blog 디렉토리에 Blog라는 Rails 애플리케이션을 생성하고, bundle install을 사용하여 Gemfile에 이미 명시된 gem 의존성들을 설치할 것입니다.

rails new --help를 실행하면 Rails 애플리케이션 생성기가 받아들이는 모든 명령행 옵션들을 볼 수 있습니다.

blog 애플리케이션을 생성한 후, 해당 폴더로 이동하세요:

$ cd blog

blog 디렉토리에는 Rails 애플리케이션의 구조를 이루는 여러 생성된 파일과 폴더들이 있습니다. 이 튜토리얼의 대부분의 작업은 app 폴더에서 이루어질 것이지만, Rails가 기본적으로 생성하는 각 파일과 폴더의 기능에 대한 기본적인 설명은 다음과 같습니다:

파일/폴더 용도
app/ 애플리케이션의 controllers, models, views, helpers, mailers, jobs 및 assets를 포함합니다. 이 가이드의 나머지 부분에서는 이 폴더에 초점을 맞출 것입니다.
bin/ 애플리케이션을 시작하는 rails 스크립트와 애플리케이션을 설정, 업데이트, 배포 또는 실행하는 데 사용하는 다른 스크립트들이 포함됩니다.
config/ 애플리케이션의 routes, database 및 기타 설정이 포함됩니다. 이는 Configuring Rails Applications에서 더 자세히 다룹니다.
config.ru 애플리케이션을 시작하는 데 사용되는 Rack 기반 서버를 위한 Rack 설정입니다. Rack에 대한 자세한 내용은 Rack website를 참조하세요.
db/ 현재 데이터베이스 스키마와 데이터베이스 migrations를 포함합니다.
Dockerfile Docker를 위한 설정 파일입니다.
Gemfile
Gemfile.lock
이 파일들을 통해 Rails 애플리케이션에 필요한 gem 의존성을 지정할 수 있습니다. 이 파일들은 Bundler gem에 의해 사용됩니다. Bundler에 대한 자세한 내용은 Bundler website를 참조하세요.
lib/ 애플리케이션의 확장 모듈입니다.
log/ 애플리케이션 로그 파일입니다.
public/ 정적 파일과 컴파일된 assets를 포함합니다. 앱이 실행 중일 때 이 디렉토리는 있는 그대로 노출됩니다.
Rakefile 이 파일은 명령줄에서 실행할 수 있는 작업을 찾아서 로드합니다. 작업 정의는 Rails의 컴포넌트 전체에 정의되어 있습니다. Rakefile을 직접 수정하는 대신, 애플리케이션의 lib/tasks 디렉토리에 파일을 추가하여 자신만의 작업을 추가해야 합니다.
README.md 애플리케이션에 대한 간단한 설명서입니다. 이 파일을 편집하여 다른 사람들에게 애플리케이션이 무엇을 하는지, 어떻게 설정하는지 등을 알려주어야 합니다.
script/ 일회성 또는 범용 scriptsbenchmarks를 포함합니다.
storage/ Disk Service를 위한 Active Storage 파일입니다. 이는 Active Storage Overview에서 다룹니다.
test/ 단위 테스트, fixtures 및 기타 테스트 도구입니다. 이는 Testing Rails Applications에서 다룹니다.
tmp/ 임시 파일(캐시 및 pid 파일과 같은)입니다.
vendor/ 모든 third-party 코드를 위한 장소입니다. 일반적인 Rails 애플리케이션에서 이는 vendored gems를 포함합니다.
.dockerignore 이 파일은 Docker에게 컨테이너에 복사하지 말아야 할 파일들을 알려줍니다.
.gitattributes 이 파일은 git 저장소의 특정 경로에 대한 메타데이터를 정의합니다. 이 메타데이터는 git과 다른 도구들이 동작을 향상시키는 데 사용될 수 있습니다. 자세한 내용은 gitattributes documentation를 참조하세요.
.github/ GitHub 특정 파일들을 포함합니다.
.gitignore 이 파일은 git에게 무시해야 할 파일(또는 패턴)을 알려줍니다. 파일 무시에 대한 자세한 내용은 GitHub - Ignoring files를 참조하세요.
.rubocop.yml 이 파일은 RuboCop의 설정을 포함합니다.
.ruby-version 이 파일은 기본 Ruby 버전을 포함합니다.

4 Hello, Rails!

우선, 화면에 빠르게 텍스트를 표시해보겠습니다. 이를 위해서는 Rails 애플리케이션 서버를 실행해야 합니다.

4.1 웹 서버 시작하기

지금 이미 동작하는 Rails 애플리케이션이 있습니다. 이를 확인하려면 개발 머신에서 웹 서버를 시작해야 합니다. blog 디렉토리에서 다음 명령어를 실행하여 시작할 수 있습니다:

$ bin/rails server

Windows를 사용하는 경우, bin 폴더 아래의 스크립트를 Ruby 인터프리터에 직접 전달해야 합니다. 예: ruby bin\rails server.

JavaScript 애셋 압축을 위해서는 시스템에 JavaScript 런타임이 설치되어 있어야 합니다. 런타임이 없는 경우 애셋 압축 도중 execjs 에러가 발생할 것입니다. 일반적으로 macOS와 Windows는 JavaScript 런타임이 기본적으로 설치되어 있습니다. JRuby 사용자의 경우 therubyrhino가 권장 런타임이며, JRuby에서 생성된 앱의 Gemfile에 기본적으로 추가됩니다. 지원되는 모든 런타임은 ExecJS에서 확인할 수 있습니다.

이렇게 하면 Rails의 기본 웹 서버인 Puma가 시작됩니다. 애플리케이션이 동작하는 것을 확인하려면, 브라우저 창을 열고 http://localhost:3000로 이동하세요. Rails 기본 정보 페이지가 표시될 것입니다:

Rails startup page screenshot

웹 서버를 중지하고 싶을 때는, 서버가 실행 중인 터미널 창에서 Ctrl+C를 누르세요. 개발 환경에서는 일반적으로 서버를 재시작할 필요가 없습니다. 파일을 수정하면 서버가 자동으로 변경 사항을 감지합니다.

Rails 시작 페이지는 새로운 Rails 애플리케이션의 스모크 테스트 입니다: 페이지를 제공하기 위한 소프트웨어가 올바르게 구성되어 있는지 확인합니다.

4.2 "Hello"라고 말하는 Rails

Rails가 "Hello"라고 말하게 하려면 최소한 route, action이 있는 controller, 그리고 view가 필요합니다. route는 request를 controller action에 매핑합니다. controller action은 request를 처리하는데 필요한 작업을 수행하고, view를 위한 데이터를 준비합니다. view는 데이터를 원하는 형식으로 표시합니다.

구현 측면에서: Routes는 Ruby DSL(Domain-Specific Language)로 작성된 규칙입니다. Controllers는 Ruby 클래스이며, 그들의 public 메서드가 actions입니다. 그리고 views는 일반적으로 HTML과 Ruby가 혼합된 템플릿입니다.

먼저 config/routes.rb 파일의 Rails.application.routes.draw 블록 맨 위에 route를 추가해보겠습니다:

Rails.application.routes.draw do
  get "/articles", to: "articles#index"

  # 이 파일에서 사용 가능한 DSL에 대한 자세한 내용은 https://guides.rubyonrails.org/routing.html 을 참조하세요
end

위의 라우트는 GET /articles 요청이 ArticlesControllerindex 액션에 매핑된다는 것을 선언합니다.

ArticlesController와 그것의 index 액션을 생성하기 위해, controller generator를 실행할 것입니다 (우리는 이미 적절한 라우트를 가지고 있기 때문에 --skip-routes 옵션과 함께):

$ bin/rails generate controller Articles index --skip-routes

Rails는 여러분을 위해 다음과 같은 파일들을 생성할 것입니다:

create  app/controllers/articles_controller.rb
invoke  erb
create    app/views/articles
create    app/views/articles/index.html.erb
invoke  test_unit
create    test/controllers/articles_controller_test.rb
invoke  helper  
create    app/helpers/articles_helper.rb
invoke    test_unit

이들 중 가장 중요한 것은 controller 파일인 app/controllers/articles_controller.rb 입니다. 이 파일을 살펴보겠습니다:

class ArticlesController < ApplicationController
  def index
  end
end

index 액션은 비어있습니다. 액션이 view를 명시적으로 렌더링하지 않을 때(또는 다른 HTTP 응답을 트리거하지 않을 때), Rails는 컨트롤러와 액션 이름이 일치하는 view를 자동으로 렌더링합니다. 이것이 바로 설정보다 관습입니다! View들은 app/views 디렉토리에 위치합니다. 따라서 index 액션은 기본적으로 app/views/articles/index.html.erb를 렌더링할 것입니다.

app/views/articles/index.html.erb를 열고, 내용을 다음과 같이 교체해보겠습니다:

<h1>안녕하세요, Rails!</h1>

이전에 controller generator를 실행하기 위해 web server를 중지했다면, bin/rails server로 다시 시작하세요. 이제 http://localhost:3000/articles를 방문하면 우리가 작성한 텍스트가 표시됩니다!

4.3 애플리케이션 홈 페이지 설정하기

현재, http://localhost:3000은 여전히 Ruby on Rails 로고가 있는 페이지를 보여줍니다. http://localhost:3000에서도 우리의 "Hello, Rails!" 텍스트를 표시해보겠습니다. 이를 위해 애플리케이션의 root path를 적절한 controller와 action에 매핑하는 route를 추가할 것입니다.

config/routes.rb를 열고, Rails.application.routes.draw 블록의 맨 위에 다음과 같은 root route를 추가해봅시다:

Rails.application.routes.draw do
  root "articles#index"

  get "/articles", to: "articles#index" 
end

http://localhost:3000에 접속하면 "Hello, Rails!" 텍스트를 볼 수 있습니다. 이는 root 라우트가 ArticlesControllerindex 액션에도 매핑되어 있다는 것을 확인해줍니다.

라우팅에 대해 더 자세히 알아보려면 외부에서 보는 Rails 라우팅을 참조하세요.

5 Autoloading

Rails 애플리케이션은 애플리케이션 코드를 로드하기 위해 require사용하지 않습니다.

ArticlesControllerApplicationController를 상속받고 있지만 app/controllers/articles_controller.rb에는 다음과 같은 내용이 없다는 것을 알 수 있습니다.

require "application_controller" # 이렇게 하지 마세요.

app 내의 애플리케이션 클래스와 모듈은 어디서나 사용할 수 있으므로 require로 로드할 필요가 없으며 그래서도 안 됩니다. 이 기능을 autoloading이라고 하며, Autoloading and Reloading Constants에서 자세히 알아볼 수 있습니다.

require가 필요한 경우는 다음 두 가지입니다:

  • lib 디렉토리 아래의 파일들을 로드할 때
  • Gemfilerequire: false가 설정된 gem 의존성을 로드할 때

6 MVC와 당신

지금까지 routes, controllers, actions, views에 대해 논의했습니다. 이들은 모두 MVC (Model-View-Controller) 패턴을 따르는 웹 애플리케이션의 전형적인 구성 요소입니다. MVC는 애플리케이션의 책임을 나누어 이해하기 쉽게 만드는 디자인 패턴입니다. Rails는 이 디자인 패턴을 관례적으로 따릅니다.

controller와 view를 다뤄봤으니, 이제 다음 구성 요소인 model을 생성해보겠습니다.

6.1 Model 생성하기

model은 데이터를 표현하는데 사용되는 Ruby 클래스입니다. 또한 model은 Active Record라고 불리는 Rails의 기능을 통해 애플리케이션의 데이터베이스와 상호작용할 수 있습니다.

model을 정의하기 위해, 우리는 model generator를 사용할 것입니다:

$ bin/rails generate model Article title:string body:text

Model 이름은 단수형입니다. 인스턴스화된 model이 단일 데이터 레코드를 나타내기 때문입니다. 이 규칙을 기억하기 쉽게 하기 위해, model의 생성자를 어떻게 호출할지 생각해보세요: Articles.new(...)아닌 Article.new(...)로 작성하고 싶을 것입니다.

이는 여러 파일들을 생성할 것입니다:

invoke  active_record
create    db/migrate/<timestamp>_create_articles.rb
create    app/models/article.rb
invoke    test_unit
create      test/models/article_test.rb
create      test/fixtures/articles.yml

위와 같이 파일들이 생성됩니다. 가장 중요한 파일은 migration과 모델 파일입니다.

우리가 초점을 맞출 두 파일은 migration 파일(db/migrate/<timestamp>_create_articles.rb)과 model 파일(app/models/article.rb)입니다.

6.2 데이터베이스 마이그레이션

Migration은 애플리케이션의 데이터베이스 구조를 변경하는 데 사용됩니다. Rails 애플리케이션에서 migration은 데이터베이스에 구애받지 않도록 Ruby로 작성됩니다.

새로운 migration 파일의 내용을 살펴보겠습니다:

class CreateArticles < ActiveRecord::Migration[8.1]
  def change
    create_table :articles do |t|
      t.string :title
      t.text :body

      t.timestamps
    end
  end
end

create_table 호출은 articles 테이블이 어떻게 구성되어야 하는지 지정합니다. 기본적으로 create_table 메서드는 자동 증가하는 기본 키로 id 컬럼을 추가합니다. 따라서 테이블의 첫 번째 레코드는 id가 1이 되고, 다음 레코드는 id가 2가 되는 식입니다.

create_table 블록 내부에는 titlebody 두 개의 컬럼이 정의되어 있습니다. 이것들은 generate 명령어(bin/rails generate model Article title:string body:text)에 포함시켰기 때문에 generator에 의해 추가되었습니다.

블록의 마지막 줄에는 t.timestamps 호출이 있습니다. 이 메서드는 created_atupdated_at이라는 두 개의 추가 컬럼을 정의합니다. 앞으로 보게 되겠지만, Rails는 모델 객체를 생성하거나 업데이트할 때 이 값들을 자동으로 관리합니다.

다음 명령어로 migration을 실행해봅시다:

$ bin/rails db:migrate

명령어를 실행하면 테이블이 생성되었다는 내용이 출력됩니다:

==  CreateArticles: 마이그레이션 중 ===================================
-- create_table(:articles)
   -> 0.0018s
==  CreateArticles: 마이그레이션 완료 (0.0018s) ==========================

migrations에 대해 더 알아보려면 Active Record Migrations를 참조하세요.

이제 우리는 모델을 사용하여 테이블과 상호작용할 수 있습니다.

6.3 모델을 사용하여 데이터베이스와 상호작용하기

모델을 가지고 좀 더 실험해보기 위해, console이라고 불리는 Rails의 기능을 사용해보겠습니다. console은 irb와 비슷한 대화형 코딩 환경이지만, Rails와 우리의 애플리케이션 코드를 자동으로 불러온다는 점이 다릅니다.

다음 명령어로 console을 실행해봅시다:

$ bin/rails console

rails console 프롬프트가 다음과 같이 보일 것입니다:

개발 환경 로딩 중 (Rails 8.1.0) 
blog(dev)>

이 프롬프트에서 새로운 Article 객체를 초기화할 수 있습니다:

blog(dev)> article = Article.new(title: "Hello Rails", body: "I am on Rails!")

이 객체를 단지 초기화했다는 점을 주목하는 것이 중요합니다. 이 객체는 아직 데이터베이스에 전혀 저장되지 않았습니다. 현재는 콘솔에서만 사용할 수 있습니다. 객체를 데이터베이스에 저장하려면 save를 호출해야 합니다:

blog(dev)> article.save
(0.1ms)  트랜잭션 시작
Article Create (0.4ms)  "articles" 테이블에 ("title", "body", "created_at", "updated_at") 값을 (?, ?, ?, ?) 로 INSERT  [["title", "Hello Rails"], ["body", "I am on Rails!"], ["created_at", "2020-01-18 23:47:30.734416"], ["updated_at", "2020-01-18 23:47:30.734416"]]
(0.9ms)  트랜잭션 커밋
=> true

위의 출력에는 INSERT INTO "articles" ... 데이터베이스 쿼리가 표시됩니다. 이는 article이 우리의 테이블에 삽입되었음을 나타냅니다. 그리고 article 객체를 다시 살펴보면, 흥미로운 일이 일어났음을 알 수 있습니다:

blog(dev)> article
=> #<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">

객체의 id, created_at, updated_at 속성들이 이제 설정되었습니다. Rails는 우리가 객체를 저장할 때 이것들을 자동으로 처리했습니다.

데이터베이스에서 이 article을 가져오고 싶을 때는, model에서 find를 호출하고 id를 인자로 전달하면 됩니다:

blog(dev)> Article.find(1)
=> #<Article id: 1, title: "Hello Rails", body: "Rails 시작하기!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">

그리고 database에서 모든 article을 가져오고 싶을 때는 model에서 all을 호출하면 됩니다:

blog(dev)> Article.all
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Hello Rails", body: "Rails에 있어요!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">]>

이 메서드는 ActiveRecord::Relation 객체를 반환하는데, 이는 강화된 배열이라고 생각하시면 됩니다.

모델에 대해 더 자세히 알아보려면 Active Record BasicsActive Record Query Interface를 참고하세요.

모델은 MVC 퍼즐의 마지막 조각입니다. 다음으로, 우리는 모든 조각들을 서로 연결할 것입니다.

6.4 게시글 목록 표시하기

app/controllers/articles_controller.rb 컨트롤러로 돌아가서, index action을 데이터베이스로부터 모든 게시글을 가져오도록 수정해봅시다:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end 
end

컨트롤러의 인스턴스 변수는 view에서 접근할 수 있습니다. 이는 app/views/articles/index.html.erb에서 @articles를 참조할 수 있다는 의미입니다. 이 파일을 열고 내용을 다음과 같이 바꿔봅시다:

<h1>게시글</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= article.title %>
    </li>
  <% end %>
</ul>

위 코드는 HTML과 ERB의 혼합입니다. ERB는 Embedded Ruby의 약자로, 문서에 포함된 Ruby 코드를 평가하는 템플릿 시스템입니다. 여기서 두 가지 유형의 ERB 태그를 볼 수 있습니다: <% %><%= %>. <% %> 태그는 "포함된 Ruby 코드를 평가하라"는 의미입니다. <%= %> 태그는 "포함된 Ruby 코드를 평가하고, 반환된 값을 출력하라"는 의미입니다. 일반적인 Ruby 프로그램에서 작성할 수 있는 모든 것을 이러한 ERB 태그 안에 넣을 수 있지만, 가독성을 위해 ERB 태그의 내용을 짧게 유지하는 것이 좋습니다.

@articles.each가 반환하는 값을 출력하고 싶지 않기 때문에, 해당 코드를 <% %>로 감쌌습니다. 하지만 각 article에 대해 article.title이 반환하는 값을 출력하고 싶기 때문에, 해당 코드는 <%= %>로 감쌌습니다.

http://localhost:3000을 방문하여 최종 결과를 볼 수 있습니다. (bin/rails server가 실행 중이어야 합니다!) 이를 실행할 때 일어나는 일은 다음과 같습니다:

  1. 브라우저가 요청을 보냅니다: GET http://localhost:3000
  2. Rails 애플리케이션이 이 요청을 받습니다.
  3. Rails 라우터가 루트 경로를 ArticlesControllerindex 액션에 매핑합니다.
  4. index 액션이 Article 모델을 사용하여 데이터베이스의 모든 article을 가져옵니다.
  5. Rails가 자동으로 app/views/articles/index.html.erb 뷰를 렌더링합니다.
  6. 뷰의 ERB 코드가 평가되어 HTML을 출력합니다.
  7. 서버가 HTML을 포함한 응답을 브라우저에 보냅니다.

이제 모든 MVC 조각들을 연결했고, 첫 번째 컨트롤러 액션이 완성되었습니다! 다음으로, 두 번째 액션으로 넘어가겠습니다.

7 CRUDit Where CRUDit Is Due

거의 모든 웹 애플리케이션은 CRUD (Create, Read, Update, Delete) 작업을 포함합니다. 여러분의 애플리케이션이 수행하는 대부분의 작업이 CRUD라는 것을 발견할 수도 있습니다. Rails는 이를 인정하고, CRUD를 수행하는 코드를 단순화하는데 도움이 되는 많은 기능을 제공합니다.

애플리케이션에 더 많은 기능을 추가하면서 이러한 기능들을 살펴보겠습니다.

7.1 단일 Article 표시

현재 우리는 데이터베이스의 모든 article들을 나열하는 view를 가지고 있습니다. 이제 하나의 article의 제목과 본문을 보여주는 새로운 view를 추가해보겠습니다.

먼저 새로운 controller action(이는 다음에 추가할 예정입니다)에 매핑되는 새로운 route를 추가하는 것부터 시작합니다. config/routes.rb를 열고, 여기 보이는 마지막 route를 삽입하세요:

Rails.application.routes.draw do
  root "articles#index" 

  get "/articles", to: "articles#index"
  get "/articles/:id", to: "articles#show"
end

새로운 route는 또 다른 get route이지만, 경로에 추가적인 :id가 있습니다. 이는 route parameter를 지정합니다. route parameter는 요청 경로의 일부를 캡처하고, 그 값을 controller action에서 접근할 수 있는 params Hash에 넣습니다. 예를 들어, GET http://localhost:3000/articles/1와 같은 요청을 처리할 때, 1:id의 값으로 캡처되어 ArticlesControllershow action에서 params[:id]로 접근할 수 있게 됩니다.

이제 app/controllers/articles_controller.rbindex action 아래에 show action을 추가해봅시다:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id]) 
  end
end

show 액션은 라우트 파라미터로 캡처된 ID와 함께 Article.find(이전에 언급됨)를 호출합니다. 반환된 article은 @article 인스턴스 변수에 저장되어 뷰에서 접근할 수 있게 됩니다. 기본적으로 show 액션은 app/views/articles/show.html.erb를 렌더링할 것입니다.

app/views/articles/show.html.erb를 다음 내용으로 생성해 봅시다:

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

이제 http://localhost:3000/articles/1을 방문하면 article을 볼 수 있습니다!

마무리하기 위해, article 페이지로 이동하는 편리한 방법을 추가해보겠습니다. app/views/articles/index.html.erb에서 각 article의 제목을 해당 페이지로 링크할 것입니다:

<h1>게시물</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <a href="/articles/<%= article.id %>">
        <%= article.title %>
      </a>
    </li>
  <% end %>
</ul>

7.2 Resourceful 라우팅

지금까지 우리는 CRUD의 "R"(Read)을 다뤘습니다. 앞으로 "C"(Create), "U"(Update), "D"(Delete)를 다룰 예정입니다. 짐작하셨겠지만, 새로운 route, controller action, view를 추가하면서 이를 진행할 것입니다. 이렇게 entity에 대해 CRUD 작업을 수행하기 위해 함께 작동하는 route, controller action, view의 조합이 있을 때마다, 우리는 그 entity를 resource라고 부릅니다. 예를 들어, 우리 애플리케이션에서 article은 resource라고 할 수 있습니다.

Rails는 resources라는 라우팅 메서드를 제공하는데, 이는 article과 같은 리소스 컬렉션에 대한 모든 관례적인 route를 매핑합니다. 따라서 "C", "U", "D" 섹션으로 진행하기 전에, config/routes.rb의 두 개의 get route를 resources로 교체해보겠습니다:

Rails.application.routes.draw do
  root "articles#index"

  resources :articles
end

bin/rails routes 명령을 실행하여 어떤 라우트가 매핑되어 있는지 확인할 수 있습니다:

$ bin/rails routes
      Prefix Verb   URI 패턴                     Controller#Action
        root GET    /                            articles#index
    articles GET    /articles(.:format)          articles#index
 new_article GET    /articles/new(.:format)      articles#new
     article GET    /articles/:id(.:format)      articles#show
             POST   /articles(.:format)          articles#create
edit_article GET    /articles/:id/edit(.:format) articles#edit
             PATCH  /articles/:id(.:format)      articles#update
             PUT    /articles/:id(.:format)      articles#update
             DELETE /articles/:id(.:format)      articles#destroy

resources 메서드는 특정한 라우트 설정에 의존하지 않도록 URL과 path 헬퍼 메서드도 설정합니다. 위 "Prefix" 열의 값에 _url 또는 _path 접미사를 붙이면 이러한 헬퍼들의 이름이 됩니다. 예를 들어, article이 주어졌을 때 article_path 헬퍼는 "/articles/#{article.id}"를 반환합니다. 이것을 사용하여 app/views/articles/index.html.erb의 링크를 깔끔하게 정리할 수 있습니다:

<h1>게시글</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <a href="<%= article_path(article) %>">
        <%= article.title %>
      </a>
    </li>
  <% end %>
</ul>

하지만 우리는 link_to helper를 사용하여 한 단계 더 나아가보겠습니다. link_to helper는 첫 번째 인자를 링크의 텍스트로, 두 번째 인자를 링크의 목적지로 하여 링크를 렌더링합니다. 두 번째 인자로 model 객체를 전달하면, link_to는 해당 객체를 path로 변환하기 위해 적절한 path helper를 호출합니다. 예를 들어, article을 전달하면, link_toarticle_path를 호출합니다. 따라서 app/views/articles/index.html.erb는 다음과 같이 됩니다:

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= link_to article.title, article %>
    </li>
  <% end %>
</ul>

훌륭합니다!

라우팅에 대해 더 자세히 알아보려면, Rails Routing from the Outside In을 참조하세요.

7.3 새로운 Article 생성하기

이제 우리는 CRUD의 "C"(Create)로 넘어갑니다. 일반적으로 웹 애플리케이션에서 새로운 resource를 생성하는 것은 여러 단계의 프로세스입니다. 먼저, 사용자가 양식을 작성하기 위해 요청합니다. 그런 다음, 사용자가 양식을 제출합니다. 오류가 없다면 resource가 생성되고 어떤 종류의 확인이 표시됩니다. 그렇지 않으면, 양식이 오류 메시지와 함께 다시 표시되고, 이 프로세스가 반복됩니다.

Rails 애플리케이션에서 이러한 단계들은 일반적으로 controller의 newcreate action들로 처리됩니다. app/controllers/articles_controller.rbshow action 아래에 이러한 action들의 일반적인 구현을 추가해보겠습니다:

class ArticlesController < ApplicationController
  def index 
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])  
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(title: "...", body: "...") 

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end
end

new action은 새로운 article을 인스턴스화하지만 저장하지는 않습니다. 이 article은 폼을 만들 때 view에서 사용됩니다. 기본적으로 new action은 app/views/articles/new.html.erb를 렌더링하며, 이는 다음에 만들 것입니다.

create action은 title과 body 값을 가진 새로운 article을 인스턴스화하고 저장을 시도합니다. article이 성공적으로 저장되면, 액션은 브라우저를 "http://localhost:3000/articles/#{@article.id}"의 article 페이지로 리다이렉트합니다. 그렇지 않으면, 액션은 422 Unprocessable Entity 상태 코드와 함께 app/views/articles/new.html.erb를 렌더링하여 폼을 다시 표시합니다. 여기서 title과 body는 임시 값입니다. 폼을 생성한 후에 이것들을 변경할 것입니다.

redirect_to는 브라우저가 새로운 요청을 하도록 하는 반면, render는 현재 요청에 대해 지정된 view를 렌더링합니다. 데이터베이스나 애플리케이션 상태를 변경한 후에는 redirect_to를 사용하는 것이 중요합니다. 그렇지 않으면, 사용자가 페이지를 새로고침할 때 브라우저가 동일한 요청을 하게 되어 변경이 반복됩니다.

7.3.1 Form Builder 사용하기

폼을 만들기 위해 Rails의 form builder 기능을 사용할 것입니다. form builder를 사용하면 Rails 규칙을 따르고 완전히 구성된 폼을 출력하기 위한 최소한의 코드만 작성하면 됩니다.

다음 내용으로 app/views/articles/new.html.erb를 생성해보겠습니다:

<h1>새 글</h1>

<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </div>

  <div>
    <%= form.submit %>
  </div>  
<% end %>

form_with helper 메서드는 form builder를 인스턴스화합니다. form_with 블록 내에서 우리는 form builder의 labeltext_field 같은 메서드를 호출하여 적절한 form 요소들을 출력합니다.

우리의 form_with 호출로부터 생성되는 출력은 다음과 같습니다:

<form action="/articles" accept-charset="UTF-8" method="post">
  <input type="hidden" name="authenticity_token" value="...">

  <div>
    <label for="article_title">제목</label><br> 
    <input type="text" name="article[title]" id="article_title">
  </div>

  <div>
    <label for="article_body">본문</label><br>
    <textarea name="article[body]" id="article_body"></textarea>
  </div>

  <div>
    <input type="submit" name="commit" value="게시글 작성" data-disable-with="게시글 작성">
  </div>
</form>

Form builder에 대해 더 자세히 알아보려면 Action View Form Helpers를 참고하세요.

7.3.2 Strong Parameters 사용하기

제출된 form 데이터는 route parameter와 함께 params Hash에 저장됩니다. 따라서 create 액션은 제출된 title을 params[:article][:title]을 통해, body를 params[:article][:body]를 통해 접근할 수 있습니다. 이 값들을 개별적으로 Article.new에 전달할 수 있지만, 그렇게 하면 장황하고 오류가 발생하기 쉽습니다. 그리고 필드를 더 추가하면 상황은 더 나빠질 것입니다.

대신, 값들을 포함하는 하나의 Hash를 전달할 것입니다. 하지만, 해당 Hash에서 어떤 값들이 허용되는지 명시해야 합니다. 그렇지 않으면, 악의적인 사용자가 추가 form 필드를 제출하여 private 데이터를 덮어쓸 수 있습니다. 실제로 필터링되지 않은 params[:article] Hash를 직접 Article.new에 전달하면, Rails는 문제를 알리기 위해 ForbiddenAttributesError를 발생시킵니다. 따라서 우리는 params를 필터링하기 위해 Rails의 Strong Parameters 기능을 사용할 것입니다. 이것은 params에 대한 strong typing이라고 생각하면 됩니다.

app/controllers/articles_controller.rb 하단에 params를 필터링하는 article_params라는 private 메서드를 추가하고, create를 이를 사용하도록 변경해보겠습니다:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id]) 
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  private
    def article_params
      params.expect(article: [:title, :body])
    end
end

Strong Parameters에 대해 자세히 알아보려면 Action Controller Overview § Strong Parameters를 참조하세요.

7.3.3 Validations과 에러 메시지 표시하기

우리가 살펴본 것처럼, resource를 생성하는 것은 여러 단계의 프로세스입니다. 유효하지 않은 사용자 입력을 처리하는 것도 그 프로세스의 또 다른 단계입니다. Rails는 유효하지 않은 사용자 입력을 처리하는 데 도움이 되는 validations라는 기능을 제공합니다. Validations은 model 객체가 저장되기 전에 확인되는 규칙입니다. 검사 중 하나라도 실패하면 저장이 중단되고, 적절한 에러 메시지가 model 객체의 errors 속성에 추가됩니다.

app/models/article.rb에 몇 가지 validations을 추가해 보겠습니다:

class Article < ApplicationRecord
  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

첫 번째 validation은 title 값이 존재해야 함을 선언합니다. title이 문자열이기 때문에, 이는 title 값에 최소한 하나의 공백이 아닌 문자가 포함되어야 한다는 의미입니다.

두 번째 validation은 body 값도 존재해야 함을 선언합니다. 추가적으로, body 값은 최소 10자 이상이어야 한다고 선언합니다.

titlebody 속성이 어디에 정의되어 있는지 궁금할 수 있습니다. Active Record는 모든 테이블 컬럼에 대해 모델 속성을 자동으로 정의하므로, 모델 파일에서 이러한 속성들을 선언할 필요가 없습니다.

validation을 설정했으니, app/views/articles/new.html.erb를 수정하여 titlebody에 대한 오류 메시지를 표시해보겠습니다:

<h1>새 글</h1>

<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
    <% @article.errors.full_messages_for(:title).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.textarea :body %><br>
    <% @article.errors.full_messages_for(:body).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

full_messages_for 메서드는 지정된 attribute에 대한 사용자 친화적인 에러 메시지 배열을 반환합니다. 해당 attribute에 대한 에러가 없다면 빈 배열이 반환됩니다.

이 모든 것이 어떻게 함께 작동하는지 이해하기 위해 newcreate 컨트롤러 액션을 다시 한 번 살펴보겠습니다:

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

http://localhost:3000/articles/new를 방문하면 GET /articles/new 요청이 new 액션으로 매핑됩니다. new 액션은 @article을 저장하려 하지 않습니다. 따라서 유효성 검증이 실행되지 않으며, 에러 메시지도 표시되지 않습니다.

폼을 제출하면 POST /articles 요청이 create 액션으로 매핑됩니다. create 액션은 @article을 저장하려 합니다. 따라서 유효성 검증이 실행됩니다. 유효성 검증이 실패하면 @article은 저장되지 않고, 에러 메시지와 함께 app/views/articles/new.html.erb가 렌더링됩니다.

유효성 검증에 대해 자세히 알아보려면 Active Record Validations를 참조하세요. 유효성 검증 에러 메시지에 대해 자세히 알아보려면 Active Record Validations § Working with Validation Errors를 참조하세요.

7.3.4 마무리하기

이제 http://localhost:3000/articles/new를 방문해서 article을 생성할 수 있습니다. 마무리하기 위해 app/views/articles/index.html.erb 하단에 해당 페이지로의 링크를 추가해보겠습니다:

<h1>게시글</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= link_to article.title, article %>
    </li>
  <% end %>
</ul>

<%= link_to "새 게시글", new_article_path %>

7.4 Article 업데이트하기

CRUD의 "CR"을 다뤘습니다. 이제 "U"(Update)로 넘어가보겠습니다. 리소스를 업데이트하는 것은 리소스를 생성하는 것과 매우 유사합니다. 둘 다 여러 단계의 프로세스입니다. 먼저, 사용자가 데이터를 편집하기 위한 폼을 요청합니다. 그런 다음 사용자가 폼을 제출합니다. 오류가 없다면 리소스가 업데이트됩니다. 그렇지 않으면 폼이 에러 메시지와 함께 다시 표시되고, 프로세스가 반복됩니다.

이러한 단계들은 일반적으로 controller의 editupdate action으로 처리됩니다. app/controllers/articles_controller.rbcreate action 아래에 이러한 action들의 일반적인 구현을 추가해보겠습니다:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = Article.find(params[:id])
  end

  def update
    @article = Article.find(params[:id])

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private
    def article_params
      params.expect(article: [:title, :body])
    end
end

이 코드는 ArticlesController 클래스를 정의하며, ApplicationController를 상속받습니다. 여기에는 다음과 같은 액션들이 포함됩니다:

  • index: 모든 article을 가져옵니다
  • show: 특정 article을 보여줍니다
  • new: 새 article을 생성하기 위한 폼을 표시합니다
  • create: 새 article을 생성합니다
  • edit: 기존 article을 수정하기 위한 폼을 표시합니다
  • update: 기존 article을 수정합니다

private 메소드 article_params는 허용된 파라미터들을 필터링합니다.

editupdate 액션이 newcreate 액션과 유사하다는 점에 주목하세요.

edit 액션은 데이터베이스에서 article을 가져와서 @article에 저장하여 폼을 만들 때 사용할 수 있도록 합니다. 기본적으로 edit 액션은 app/views/articles/edit.html.erb를 렌더링합니다.

update 액션은 데이터베이스에서 article을 (다시) 가져와서 article_params로 필터링된 제출된 폼 데이터로 업데이트를 시도합니다. 유효성 검사에 실패하지 않고 업데이트가 성공하면 액션은 브라우저를 해당 article의 페이지로 리다이렉트합니다. 그렇지 않으면 액션은 app/views/articles/edit.html.erb를 렌더링하여 에러 메시지와 함께 폼을 다시 표시합니다.

7.4.1 View 코드 공유를 위한 Partial 사용하기

우리의 edit 폼은 new 폼과 동일하게 보일 것입니다. Rails form builder와 resourceful routing 덕분에 코드도 동일할 것입니다. form builder는 모델 객체가 이전에 저장되었는지 여부에 따라 적절한 종류의 요청을 만들도록 폼을 자동으로 구성합니다.

코드가 동일하기 때문에, 이를 partial이라고 하는 공유 view로 분리할 것입니다. 다음 내용으로 app/views/articles/_form.html.erb를 생성해 보겠습니다:

<%= form_with model: article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
    <% article.errors.full_messages_for(:title).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.textarea :body %><br>
    <% article.errors.full_messages_for(:body).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

이 코드는 form_with helper를 사용하여 article 모델에 대한 form을 생성하는 것입니다. form은 title과 body 필드를 포함하며, 각 필드에 오류가 있는 경우 해당 오류 메시지를 표시합니다. 각 필드는 label과 입력 필드로 구성되어 있습니다.

위 코드는 app/views/articles/new.html.erb에 있는 form과 동일하지만, @article이 나오는 모든 부분이 article로 대체되었습니다. Partial은 공유되는 코드이므로, controller action에서 설정한 특정 instance 변수에 의존하지 않는 것이 가장 좋은 방법입니다. 대신 article을 local 변수로 partial에 전달할 것입니다.

render를 사용하여 app/views/articles/new.html.erb를 업데이트해봅시다:

<h1>새 글</h1>

<%= render "form", article: @article %>

partial의 파일명은 반드시 언더스코어로 시작해야 합니다. 예를 들어 _form.html.erb와 같이 작성합니다. 하지만 렌더링할 때는 언더스코어 없이 참조됩니다. 예를 들어 render "form"과 같이 작성합니다.

이제 매우 비슷한 app/views/articles/edit.html.erb를 만들어보겠습니다:

<h1>게시글 수정</h1>

<%= render "form", article: @article %>

partial에 대해 더 알아보려면 Rails의 레이아웃과 렌더링 § Partial 사용하기를 참조하세요.

7.4.2 마무리

이제 article의 edit 페이지를 방문하여 수정할 수 있습니다. (예: http://localhost:3000/articles/1/edit) 마무리를 위해 app/views/articles/show.html.erb 하단에 edit 페이지로 연결되는 링크를 추가해보겠습니다:

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "수정", edit_article_path(@article) %></li>
</ul>

7.5 Article 삭제하기

마지막으로 CRUD의 "D" (Delete)에 도달했습니다. 리소스를 삭제하는 것은 생성이나 수정보다 간단한 프로세스입니다. 라우트와 컨트롤러 액션만 필요합니다. 그리고 우리의 resourceful routing(resources :articles)이 이미 라우트를 제공하며, 이는 DELETE /articles/:id 요청을 ArticlesControllerdestroy 액션에 매핑합니다.

그러면 일반적인 destroy 액션을 app/controllers/articles_controller.rbupdate 액션 아래에 추가해보겠습니다:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article 
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = Article.find(params[:id])
  end

  def update
    @article = Article.find(params[:id])

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @article = Article.find(params[:id])
    @article.destroy

    redirect_to root_path, status: :see_other
  end

  private
    def article_params
      params.expect(article: [:title, :body])
    end
end

destroy 액션은 데이터베이스에서 article을 가져와서 해당 객체의 destroy를 호출합니다. 그런 다음, 브라우저를 303 See Other 상태 코드와 함께 root path로 리다이렉트합니다.

우리는 articles의 주요 접근점이기 때문에 root path로 리다이렉트하기로 선택했습니다. 하지만 다른 상황에서는 예를 들어 articles_path로 리다이렉트하도록 선택할 수도 있습니다.

이제 app/views/articles/show.html.erb의 하단에 링크를 추가하여 해당 페이지에서 article을 삭제할 수 있도록 만들어보겠습니다:

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "수정", edit_article_path(@article) %></li>
  <li><%= link_to "삭제", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "정말로 삭제하시겠습니까?"
                  } %></li>
</ul>

위 코드에서 우리는 data 옵션을 사용하여 "Destroy" 링크의 data-turbo-methoddata-turbo-confirm HTML 속성을 설정합니다. 이러한 속성들은 새로운 Rails 애플리케이션에 기본적으로 포함되어 있는 Turbo와 연동됩니다. data-turbo-method="delete"는 링크가 GET 요청 대신 DELETE 요청을 하도록 만듭니다. data-turbo-confirm="Are you sure?"는 링크를 클릭했을 때 확인 대화상자가 나타나도록 합니다. 사용자가 대화상자를 취소하면 요청이 중단됩니다.

이게 전부입니다! 이제 우리는 article을 목록화, 조회, 생성, 수정 그리고 삭제할 수 있습니다! InCRUDable!

8 두 번째 Model 추가하기

이제 애플리케이션에 두 번째 model을 추가할 시간입니다. 두 번째 model은 article에 대한 댓글을 처리할 것입니다.

8.1 Model 생성하기

이전에 Article model을 생성할 때 사용했던 것과 동일한 generator를 보겠습니다. 이번에는 article에 대한 참조를 담을 Comment model을 생성할 것입니다. 터미널에서 다음 명령어를 실행하세요:

$ bin/rails generate model Comment commenter:string body:text article:references

이 명령어는 4개의 파일을 생성합니다:

파일 목적
db/migrate/<timestamp>_create_comments.rb 데이터베이스에 comments 테이블을 생성하는 마이그레이션
app/models/comment.rb Comment 모델
test/models/comment_test.rb comment 모델을 위한 테스트 하네스
test/fixtures/comments.yml 테스트에 사용할 샘플 comments

먼저 app/models/comment.rb를 살펴보겠습니다:

class Comment < ApplicationRecord
  belongs_to :article
end

이는 앞서 본 Article 모델과 매우 유사합니다. 차이점은 Active Record association을 설정하는 belongs_to :article 라인입니다. 이 가이드의 다음 섹션에서 association에 대해 자세히 알아볼 것입니다.

셸 명령어에서 사용된 (:references) 키워드는 모델을 위한 특별한 데이터 타입입니다. 이는 제공된 모델 이름에 _id를 붙여 정수 값을 저장할 수 있는 새로운 컬럼을 데이터베이스 테이블에 생성합니다. 더 잘 이해하기 위해서는 마이그레이션을 실행한 후 db/schema.rb 파일을 분석해보세요.

모델 외에도 Rails는 해당하는 데이터베이스 테이블을 생성하기 위한 마이그레이션도 만들었습니다:

class CreateComments < ActiveRecord::Migration[8.1]
  def change
    create_table :comments do |t|
      t.string :commenter
      t.text :body
      t.references :article, null: false, foreign_key: true

      t.timestamps 
    end
  end
end

t.references 라인은 article_id라는 이름의 integer 컬럼과 해당 컬럼에 대한 인덱스, 그리고 articles 테이블의 id 컬럼을 가리키는 foreign key 제약조건을 생성합니다. migration을 실행해보세요:

$ bin/rails db:migrate

Rails는 현재 데이터베이스에서 아직 실행되지 않은 migration만 실행할 만큼 똑똑하므로, 이 경우 다음과 같이만 표시됩니다:

==  CreateComments: 마이그레이션 실행중 =================================================
-- create_table(:comments)
   -> 0.0115s
==  CreateComments: 마이그레이션 완료 (0.0119s) ========================================

8.2 모델 연관짓기

Active Record associations를 사용하면 두 모델 간의 관계를 쉽게 선언할 수 있습니다. comments와 articles의 경우, 관계를 다음과 같이 작성할 수 있습니다:

  • 각 comment는 하나의 article에 속합니다.
  • 하나의 article은 여러 comments를 가질 수 있습니다.

실제로 이것은 Rails가 이 association을 선언하는데 사용하는 구문과 매우 유사합니다. 이미 Comment 모델(app/models/comment.rb) 내부에서 각 comment가 Article에 속하도록 하는 코드를 보셨을 것입니다:

class Comment < ApplicationRecord
  belongs_to :article
end

app/models/article.rb를 수정하여 association의 다른 쪽을 추가해야 합니다:

class Article < ApplicationRecord
  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

이 두 가지 선언으로 많은 자동화된 동작이 가능해집니다. 예를 들어, article을 포함하는 인스턴스 변수 @article이 있다면, @article.comments를 사용하여 해당 article에 속한 모든 comment들을 배열로 가져올 수 있습니다.

Active Record associations에 대한 자세한 내용은 Active Record Associations 가이드를 참조하세요.

8.3 Comments를 위한 라우트 추가하기

articles 컨트롤러와 마찬가지로, Rails가 comments를 어디서 볼 수 있는지 알 수 있도록 라우트를 추가해야 합니다. config/routes.rb 파일을 다시 열고 다음과 같이 수정하세요:

Rails.application.routes.draw do
  root "articles#index"

  resources :articles do
    resources :comments
  end
end

이는 articles 내에 중첩 리소스comments를 생성합니다. 이것은 article과 comment 사이에 존재하는 계층적 관계를 표현하는 또 다른 방법입니다.

라우팅에 대한 자세한 내용은 Rails Routing 가이드를 참조하세요.

8.4 Controller 생성하기

모델이 준비되었으므로, 이제 그에 맞는 controller를 생성하는데 집중할 수 있습니다. 다시 한번 이전에 사용했던 generator를 사용하겠습니다:

$ bin/rails generate controller Comments

이것은 세 개의 파일과 하나의 빈 디렉토리를 생성합니다:

파일/디렉토리 용도
app/controllers/comments_controller.rb Comments 컨트롤러
app/views/comments/ 컨트롤러의 view들이 저장되는 곳
test/controllers/comments_controller_test.rb 컨트롤러를 위한 테스트
app/helpers/comments_helper.rb view 헬퍼 파일

모든 블로그와 마찬가지로, 독자들은 글을 읽은 직후 댓글을 작성하고, 댓글을 추가한 후에는 자신이 작성한 댓글이 목록에 표시된 것을 확인할 수 있도록 글 상세 페이지로 다시 이동하게 됩니다. 이러한 이유로 CommentsController는 댓글을 생성하고 스팸 댓글이 달렸을 때 삭제하는 메서드를 제공합니다.

먼저, Article 상세 템플릿(app/views/articles/show.html.erb)에 새 댓글을 작성할 수 있도록 연결하겠습니다:

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "수정", edit_article_path(@article) %></li>
  <li><%= link_to "삭제", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "삭제하시겠습니까?"
                  } %></li>
</ul>

<h2>댓글 추가:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

이것은 Article show 페이지에 CommentsControllercreate 액션을 호출하여 새 comment를 생성하는 폼을 추가합니다. 여기서 form_with 호출은 배열을 사용하는데, 이는 /articles/1/comments와 같은 중첩된 라우트를 생성합니다.

이제 app/controllers/comments_controller.rb에서 create를 연결해 보겠습니다:

class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id]) 
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  private
    def comment_params
      params.expect(comment: [:commenter, :body])
    end
end

articles controller보다 복잡한 코드를 볼 수 있습니다. 이는 설정한 nesting의 부작용입니다. comment에 대한 각 요청은 comment가 첨부된 article을 추적해야 하므로, 해당 article을 가져오기 위해 Article 모델의 find 메서드를 먼저 호출합니다.

또한 이 코드는 association에서 사용할 수 있는 메서드들을 활용합니다. comment를 생성하고 저장하기 위해 @article.commentscreate 메서드를 사용합니다. 이렇게 하면 comment가 해당 article에 자동으로 연결됩니다.

새 comment를 생성한 후에는 article_path(@article) 헬퍼를 사용하여 사용자를 원래 article로 다시 보냅니다. 이전에 보았듯이, 이는 ArticlesControllershow 액션을 호출하고 이어서 show.html.erb 템플릿을 렌더링합니다. 여기가 comment를 표시하고 싶은 곳이므로, app/views/articles/show.html.erb에 이를 추가해보겠습니다.

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "수정", edit_article_path(@article) %></li>
  <li><%= link_to "삭제", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "삭제하시겠습니까?"
                  } %></li>
</ul>

<h2>댓글</h2>
<% @article.comments.each do |comment| %>
  <p>
    <strong>작성자:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>댓글:</strong>
    <%= comment.body %>
  </p>
<% end %>

<h2>댓글 작성:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

이제 블로그에 article과 comment를 추가할 수 있고 적절한 위치에서 보여줄 수 있습니다.

Article with Comments

9 Refactoring

이제 article과 comment가 작동하니 app/views/articles/show.html.erb 템플릿을 살펴보세요. 이 템플릿이 길어지고 다루기 어려워지고 있습니다. partial을 사용하여 깔끔하게 정리할 수 있습니다.

9.1 Partial 컬렉션 렌더링하기

먼저, article의 모든 comment를 보여주기 위한 comment partial을 만들어보겠습니다. app/views/comments/_comment.html.erb 파일을 생성하고 다음 내용을 넣어주세요:

<p>
  <strong>작성자:</strong>
  <%= comment.commenter %>
</p>

<p>
  <strong>댓글:</strong>
  <%= comment.body %>
</p>

그리고 app/views/articles/show.html.erb를 다음과 같이 변경할 수 있습니다:

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "수정", edit_article_path(@article) %></li>
  <li><%= link_to "삭제", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "정말로 삭제하시겠습니까?"
                  } %></li>
</ul>

<h2>댓글</h2>
<%= render @article.comments %>

<h2>댓글 추가:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

이제 app/views/comments/_comment.html.erb partial이 @article.comments collection에 있는 각 comment마다 한 번씩 렌더링됩니다. render 메소드가 @article.comments collection을 순회하면서 각 comment를 partial과 같은 이름의 로컬 변수(이 경우 comment)에 할당하며, 이는 우리가 보여줄 수 있도록 partial 내에서 사용 가능합니다.

9.2 Partial Form 렌더링하기

새 댓글 섹션을 별도의 partial로 분리해봅시다. 마찬가지로 app/views/comments/_form.html.erb 파일을 생성하고 다음 내용을 작성합니다:

<%= form_with model: [ article, article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

그리고 app/views/articles/show.html.erb를 다음과 같이 작성합니다:

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "수정", edit_article_path(@article) %></li>
  <li><%= link_to "삭제", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "삭제하시겠습니까?"
                  } %></li>
</ul>

<h2>댓글</h2>
<%= render @article.comments %>

<h2>댓글 작성:</h2>
<%= render "comments/form", article: @article %>

두 번째 render는 우리가 렌더링하고자 하는 partial 템플릿인 comments/form을 정의합니다. Rails는 이 문자열의 슬래시를 감지하고 app/views/comments 디렉토리에 있는 _form.html.erb 파일을 렌더링하려 한다는 것을 이해할 만큼 똑똑합니다.

9.3 Concerns 사용하기

Concerns는 큰 controller나 model을 더 쉽게 이해하고 관리할 수 있게 해주는 방법입니다. 또한 여러 model(또는 controller)이 동일한 concern을 공유할 때 재사용성이라는 장점이 있습니다. Concerns는 model이나 controller가 담당하는 기능의 잘 정의된 부분을 나타내는 메서드들을 포함하는 module을 사용하여 구현됩니다. 다른 프로그래밍 언어에서는 이러한 module을 흔히 mixin이라고 부릅니다.

controller나 model에서 다른 module을 사용하는 것과 동일한 방식으로 concern을 사용할 수 있습니다. rails new blog로 앱을 처음 생성했을 때, app/ 디렉토리 안에 다른 폴더들과 함께 두 개의 폴더가 생성되었습니다:

app/controllers/concerns
app/models/concerns

아래 예제에서는 concern을 사용하면 좋은 새로운 블로그 기능을 구현할 것입니다. 그 다음, concern을 만들고 이를 사용하도록 코드를 리팩토링하여 코드를 더 DRY하고 유지보수하기 좋게 만들 것입니다.

블로그 게시글은 다양한 상태를 가질 수 있습니다. 예를 들어 모든 사람이 볼 수 있거나(public), 작성자만 볼 수 있거나(private), 또는 모든 사람에게 숨겨져 있지만 여전히 검색할 수 있는 상태(archived)일 수 있습니다. 댓글도 마찬가지로 숨겨져 있거나 보일 수 있습니다. 이는 각 모델에서 status 컬럼을 사용하여 표현할 수 있습니다.

먼저, ArticlesCommentsstatus를 추가하기 위해 다음 마이그레이션을 실행해보겠습니다:

$ bin/rails generate migration AddStatusToArticles status:string
$ bin/rails generate migration AddStatusToComments status:string

이제 생성된 migration으로 데이터베이스를 업데이트해보겠습니다:

$ bin/rails db:migrate

기존 articles와 comments의 status를 선택하기 위해서는 생성된 migration 파일에 default: "public" 옵션을 추가하여 기본값을 설정할 수 있습니다. migration 파일을 수정한 후에는 이전 단계를 다시 실행할 수 있습니다:

$ bin/rails db:migrate:redo

rails console에서 Article.update_all(status: "public")Comment.update_all(status: "public")을 호출할 수도 있습니다.

migration에 대해 더 자세히 알아보려면 Active Record Migrations를 참조하세요.

또한 app/controllers/articles_controller.rb에서 strong parameter의 일부로 :status 키를 허용해야 합니다:

  private
    def article_params
      params.expect(article: [:title, :body, :status])
    end

그리고 app/controllers/comments_controller.rb에서:

  private
    def comment_params
      params.expect(comment: [:commenter, :body, :status])
    end

article 모델에서, bin/rails db:migrate 명령어를 사용하여 status 컬럼을 추가하는 마이그레이션을 실행한 후에는 다음과 같이 추가하면 됩니다:

class Article < ApplicationRecord
  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }

  VALID_STATUSES = [ "public", "private", "archived" ]

  validates :status, inclusion: { in: VALID_STATUSES }

  def archived? 
    status == "archived"
  end
end

그리고 Comment 모델 내부에서:

class Comment < ApplicationRecord
  belongs_to :article

  VALID_STATUSES = [ "public", "private", "archived" ]

  validates :status, inclusion: { in: VALID_STATUSES } 

  def archived?
    status == "archived"
  end
end

그런 다음 index 액션 템플릿(app/views/articles/index.html.erb)에서 archived? 메서드를 사용하여 archive된 article이 표시되지 않도록 할 수 있습니다:

<h1>게시글</h1>

<ul>
  <% @articles.each do |article| %>
    <% unless article.archived? %>
      <li>
        <%= link_to article.title, article %>
      </li>
    <% end %>
  <% end %>
</ul>

<%= link_to "새 게시글", new_article_path %>

비슷하게, comment partial view (app/views/comments/_comment.html.erb)에서 archived된 comment를 표시하지 않기 위해 archived? 메서드를 사용합니다:

<% unless comment.archived? %>
  <p>
    <strong>작성자:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>댓글:</strong>
    <%= comment.body %>
  </p>
<% end %>

하지만 지금 우리의 모델들을 다시 보면 로직이 중복되어 있다는 것을 알 수 있습니다. 나중에 블로그의 기능을 확장할 때 - 예를 들어 개인 메시지를 포함한다면 - 우리는 로직을 또다시 중복해야 할 수 있습니다. 이럴 때 concern이 유용하게 사용됩니다.

concern은 모델의 책임 중 특정 부분에만 집중합니다. 우리의 concern에 있는 메서드들은 모두 모델의 가시성과 관련되어 있을 것입니다. 새로운 concern(모듈)을 Visible이라고 부르겠습니다. app/models/concerns 안에 visible.rb라는 새 파일을 만들고, 모델들에서 중복되었던 모든 status 메서드들을 여기에 저장할 수 있습니다.

app/models/concerns/visible.rb

module Visible
  def archived?
    status == "archived"
  end
end

우리는 status validation을 concern에 추가할 수 있지만, validation이 클래스 레벨에서 호출되는 메서드이기 때문에 이는 약간 더 복잡합니다. ActiveSupport::Concern (API Guide)은 이들을 포함시키는 더 간단한 방법을 제공합니다:

module Visible
  extend ActiveSupport::Concern

  VALID_STATUSES = [ "public", "private", "archived" ]

  included do
    validates :status, inclusion: { in: VALID_STATUSES }
  end

  def archived?
    status == "archived" 
  end
end

app/models/article.rb에서 각 모델의 중복된 로직을 제거하고 새로운 Visible module을 대신 include할 수 있습니다:

class Article < ApplicationRecord
  include Visible

  has_many :comments

  validates :title, presence: true 
  validates :body, presence: true, length: { minimum: 10 }
end

그리고 app/models/comment.rb에서:

class Comment < ApplicationRecord
  include Visible

  belongs_to :article
end

클래스 메서드도 concern에 추가할 수 있습니다. 만약 메인 페이지에서 public article이나 comment의 수를 표시하고 싶다면, 다음과 같이 Visible에 클래스 메서드를 추가할 수 있습니다:

module Visible
  extend ActiveSupport::Concern

  VALID_STATUSES = [ "public", "private", "archived" ]

  included do
    validates :status, inclusion: { in: VALID_STATUSES }
  end

  class_methods do 
    def public_count
      where(status: "public").count
    end
  end

  def archived?
    status == "archived"
  end
end

그리고 view에서는 이것을 클래스 메소드처럼 호출할 수 있습니다:

<h1>게시글</h1>

우리 블로그에는 <%= Article.public_count %>개의 게시글이 있습니다!

<ul>
  <% @articles.each do |article| %>
    <% unless article.archived? %>
      <li>
        <%= link_to article.title, article %>
      </li>
    <% end %>
  <% end %>
</ul>

<%= link_to "새 게시글", new_article_path %>

마무리하기 위해, form에 select box를 추가하고 사용자가 새로운 article을 생성하거나 새로운 comment를 작성할 때 status를 선택할 수 있도록 하겠습니다. 객체의 status를 선택하거나, 아직 설정되지 않은 경우 기본값으로 public을 설정할 수 있습니다. app/views/articles/_form.html.erb에 다음을 추가할 수 있습니다:

<div>
  <%= form.label :status %><br>
  <%= form.select :status, Visible::VALID_STATUSES, selected: article.status || 'public' %>
</div>

그리고 app/views/comments/_form.html.erb에서:

<p>
  <%= form.label :status %><br>
  <%= form.select :status, Visible::VALID_STATUSES, selected: 'public' %>
</p>

10 Deleting Comments

블로그의 또 다른 중요한 기능은 스팸 댓글을 삭제할 수 있다는 것입니다. 이를 위해서는 view에 링크를 구현하고 CommentsControllerdestroy action을 구현해야 합니다.

먼저 app/views/comments/_comment.html.erb partial에 삭제 링크를 추가해보겠습니다:

<% unless comment.archived? %>
  <p>
    <strong>작성자:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>댓글:</strong>
    <%= comment.body %>
  </p>

  <p>
    <%= link_to "댓글 삭제", [comment.article, comment], data: {
                  turbo_method: :delete,
                  turbo_confirm: "정말로 삭제하시겠습니까?"
                } %>
  </p>
<% end %>

새로운 "Destroy Comment" 링크를 클릭하면 CommentsControllerDELETE /articles/:article_id/comments/:id가 전송되며, 이를 통해 삭제하려는 comment를 찾을 수 있습니다. 그러므로 controller에 destroy 액션을 추가해보겠습니다(app/controllers/comments_controller.rb):

class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id]) 
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  def destroy
    @article = Article.find(params[:article_id])
    @comment = @article.comments.find(params[:id]) 
    @comment.destroy
    redirect_to article_path(@article), status: :see_other
  end

  private
    def comment_params
      params.expect(comment: [:commenter, :body, :status])
    end
end

destroy 액션은 우리가 보고 있는 article을 찾고, @article.comments 컬렉션에서 해당 comment를 찾은 다음, 데이터베이스에서 이를 제거하고 article의 show 액션으로 되돌아갑니다.

10.1 연관된 객체 삭제하기

article을 삭제하면 연관된 comment들도 삭제해야 합니다. 그렇지 않으면 데이터베이스에서 불필요한 공간만 차지하게 됩니다. Rails는 이를 위해 association의 dependent 옵션을 사용할 수 있게 해줍니다. Article 모델 app/models/article.rb를 다음과 같이 수정하세요:

class Article < ApplicationRecord
  include Visible

  has_many :comments, dependent: :destroy # article이 삭제되면 연관된 comment도 삭제

  validates :title, presence: true # title은 반드시 있어야 함
  validates :body, presence: true, length: { minimum: 10 } # body는 반드시 있어야 하고 최소 10자 이상이어야 함
end

11 Security

11.1 기본 인증

블로그를 온라인에 올리면 누구나 게시글을 추가하고, 수정하고, 삭제하거나 댓글을 삭제할 수 있게 됩니다.

Rails는 이러한 상황에서 유용하게 사용할 수 있는 HTTP 인증 시스템을 제공합니다.

ArticlesController에서는 인증되지 않은 사용자의 다양한 액션 접근을 차단할 수 있는 방법이 필요합니다. 여기서 Rails의 http_basic_authenticate_with 메서드를 사용할 수 있는데, 이 메서드는 허용된 경우에만 요청된 액션에 대한 접근을 허용합니다.

인증 시스템을 사용하기 위해, app/controllers/articles_controller.rbArticlesController 맨 위에 명시합니다. 우리의 경우에는 indexshow를 제외한 모든 액션에서 사용자 인증이 필요하므로, 다음과 같이 작성합니다:

class ArticlesController < ApplicationController
  http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]

  def index
    @articles = Article.all
  end

  # 간단한 예시를 위해 생략
end

인증된 사용자만 comment를 삭제할 수 있도록 허용하기 위해, CommentsController (app/controllers/comments_controller.rb)에 다음과 같이 작성합니다:

class CommentsController < ApplicationController
  http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy

  def create
    @article = Article.find(params[:article_id]) 
    # ...
  end

  # 간결성을 위한 코드 생략
end

이제 새로운 article을 생성하려고 하면, 기본적인 HTTP Authentication 인증 요청을 보게 될 것입니다:

Basic HTTP Authentication Challenge

올바른 username과 password를 입력하면, 다른 username과 password가 필요하거나 브라우저를 닫을 때까지 인증된 상태로 유지됩니다.

Rails 애플리케이션에는 다른 인증 방법들도 있습니다. Rails에서 널리 사용되는 두 가지 인증 애드온은 Devise rails engine과 Authlogic gem이며, 이 외에도 많은 다른 방법들이 있습니다.

11.2 기타 보안 고려사항

웹 애플리케이션의 보안은 광범위하고 세부적인 영역입니다. Rails 애플리케이션의 보안에 대해서는 Ruby on Rails Security Guide에서 더 자세히 다루고 있습니다.

12 다음은 무엇인가요?

이제 첫 번째 Rails 애플리케이션을 보았으니, 자유롭게 업데이트하고 직접 실험해보세요.

모든 것을 혼자 해결할 필요는 없다는 것을 기억하세요. Rails를 시작하고 실행하는 데 도움이 필요하다면 다음의 지원 리소스를 자유롭게 참고하세요:

13 Configuration 관련 주의사항

Rails로 작업하는 가장 쉬운 방법은 모든 외부 데이터를 UTF-8로 저장하는 것입니다. 그렇게 하지 않으면, Ruby 라이브러리와 Rails가 네이티브 데이터를 UTF-8로 변환할 수 있지만, 이것이 항상 안정적으로 작동하는 것은 아니므로 모든 외부 데이터가 UTF-8인지 확인하는 것이 좋습니다.

이 영역에서 실수를 했다면, 가장 일반적인 증상은 브라우저에 물음표가 있는 검은 다이아몬드가 나타나는 것입니다. 또 다른 일반적인 증상은 "ü" 대신 "ü"와 같은 문자가 나타나는 것입니다. Rails는 자동으로 감지하고 수정할 수 있는 이러한 문제들의 일반적인 원인을 완화하기 위해 여러 내부 단계를 거칩니다. 하지만 UTF-8로 저장되지 않은 외부 데이터가 있는 경우, Rails가 자동으로 감지하고 수정할 수 없는 이러한 종류의 문제가 발생할 수 있습니다.

UTF-8이 아닌 데이터의 두 가지 매우 일반적인 소스:

  • 텍스트 에디터: 대부분의 텍스트 에디터(TextMate와 같은)는 기본적으로 파일을 UTF-8로 저장합니다. 텍스트 에디터가 그렇지 않은 경우, 템플릿에 입력한 특수 문자(예: é)가 브라우저에서 물음표가 있는 다이아몬드로 나타날 수 있습니다. 이는 i18n 번역 파일에도 적용됩니다. UTF-8을 기본값으로 사용하지 않는 대부분의 에디터(Dreamweaver의 일부 버전과 같은)는 기본값을 UTF-8로 변경하는 방법을 제공합니다. 그렇게 하세요.
  • 데이터베이스: Rails는 기본적으로 데이터베이스의 데이터를 경계에서 UTF-8로 변환합니다. 하지만 데이터베이스가 내부적으로 UTF-8을 사용하지 않는 경우, 사용자가 입력한 모든 문자를 저장하지 못할 수 있습니다. 예를 들어, 데이터베이스가 내부적으로 Latin-1을 사용하고 있고 사용자가 러시아어를

Hebrew나 Japanese 문자와 같이 non-ASCII 데이터가 데이터베이스에 한번 들어가면, 그 데이터는 영원히 손실됩니다. 가능하다면 데이터베이스의 내부 저장소로 UTF-8을 사용하세요.



맨 위로