1 소개
Ruby on Rails에 오신 것을 환영합니다! 이 가이드에서는 Rails로 웹 애플리케이션을 만드는 핵심 개념들을 살펴볼 것입니다. 이 가이드를 따라하기 위해 Rails 경험이 필요하지 않습니다.
Rails는 Ruby 프로그래밍 언어로 만들어진 웹 프레임워크입니다. Rails는 Ruby의 많은 기능을 활용하므로, 이 튜토리얼에서 보게 될 기본 용어와 어휘를 이해할 수 있도록 Ruby의 기초를 배우실 것을 강력히 추천합니다.
2 Rails 철학
Rails는 Ruby 프로그래밍 언어로 작성된 웹 애플리케이션 개발 프레임워크입니다. 모든 개발자가 시작하는 데 필요한 것들에 대한 가정을 만들어 웹 애플리케이션 프로그래밍을 더 쉽게 만들도록 설계되었습니다. 다른 많은 언어와 프레임워크보다 더 적은 코드로 더 많은 것을 수행할 수 있게 해줍니다. 경험 많은 Rails 개발자들은 웹 애플리케이션 개발이 더 재미있어진다고도 말합니다.
Rails는 독선적인 소프트웨어입니다. 일을 처리하는 "최선의" 방법이 있다고 가정하고, 그 방법을 권장하도록 설계되었으며 - 어떤 경우에는 대안을 억제하기도 합니다. "Rails 방식"을 배우면 생산성이 엄청나게 향상되는 것을 발견하게 될 것입니다. 다른 언어에서의 오래된 습관을 Rails 개발에 가져오거나 다른 곳에서 배운 패턴을 사용하려고 고집한다면, 그다지 행복한 경험을 하지 못할 수 있습니다.
Rails 철학은 두 가지 주요 지침 원칙을 포함합니다:
- Don't Repeat Yourself: DRY는 "시스템 내에서 모든 지식은 단일하고, 모호하지 않으며, 신뢰할 수 있는 표현을 가져야 한다"라고 명시하는 소프트웨어 개발 원칙입니다. 동일한 정보를 반복해서 작성하지 않음으로써, 우리의 코드는 더 유지보수가 용이하고, 확장성이 있으며, 버그가 적어집니다.
- Convention Over Configuration: Rails는 웹 애플리케이션에서 많은 것들을 수행하는 최선의 방법에 대한 의견을 가지고 있으며, 무수한 configuration 파일을 통해 직접 정의하도록 요구하는 대신 이러한 convention 세트를 기본값으로 사용합니다.
3 새로운 Rails 앱 만들기
우리는 store
라는 프로젝트를 만들 것입니다 - Rails의 여러 내장 기능을 보여주는 간단한 e-commerce 앱입니다.
달러 기호 $
가 앞에 붙은 명령어는 터미널에서 실행해야 합니다.
3.1 준비사항
이 프로젝트를 위해서는 다음이 필요합니다:
- Ruby 3.2 이상
- Rails 8.1.0 이상
- 코드 에디터
Ruby나 Rails를 설치해야 하는 경우 Ruby on Rails 설치 가이드를 따라주세요.
올바른 Rails 버전이 설치되어 있는지 확인해봅시다. 현재 버전을 표시하기 위해, 터미널을 열고 다음을 실행하세요. 버전 번호가 출력되어야 합니다:
$ rails --version
Rails 8.1.0
표시된 버전은 Rails 8.1.0 이상이어야 합니다.
3.2 첫 Rails 앱 만들기
Rails는 작업을 더 쉽게 만들어주는 여러 명령어들을 제공합니다. rails --help
를 실행하면 모든 명령어를 볼 수 있습니다.
rails new
는 새로운 Rails 애플리케이션의 기초를 생성해주므로, 여기서부터 시작해보겠습니다.
우리의 store
애플리케이션을 만들기 위해, 터미널에서 다음 명령어를 실행하세요:
$ rails new store
option flag를 사용하여 Rails가 생성하는 애플리케이션을 커스터마이즈할 수 있습니다. 이러한 옵션들을 보려면 rails new --help
를 실행하세요.
새로운 애플리케이션이 생성된 후, 해당 디렉토리로 이동하세요:
$ cd store
3.3 디렉토리 구조
새로운 Rails 애플리케이션에 포함된 파일과 디렉토리를 간단히 살펴보겠습니다. 코드 에디터에서 이 폴더를 열거나 터미널에서 ls -la
를 실행하여 파일과 디렉토리를 확인할 수 있습니다.
파일/폴더 | 용도 |
---|---|
app/ | 애플리케이션의 controllers, models, views, helpers, mailers, jobs 및 assets를 포함합니다. 이 가이드의 나머지 부분에서는 주로 이 폴더에 집중할 것입니다. |
bin/ | 앱을 시작하는 rails 스크립트와 애플리케이션을 설정, 업데이트, 배포 또는 실행하는 데 사용하는 다른 스크립트들이 포함됩니다. |
config/ | 애플리케이션의 routes, database 및 기타 설정이 포함됩니다. 이는 Configuring Rails Applications에서 더 자세히 다룹니다. |
config.ru | 애플리케이션을 시작하는 데 사용되는 Rack 기반 서버를 위한 Rack 설정입니다. |
db/ | 현재 데이터베이스 스키마와 데이터베이스 마이그레이션이 포함됩니다. |
Dockerfile | Docker를 위한 설정 파일입니다. |
Gemfile Gemfile.lock |
이 파일들을 통해 Rails 애플리케이션에 필요한 gem 의존성을 지정할 수 있습니다. 이 파일들은 Bundler gem에 의해 사용됩니다. |
lib/ | 애플리케이션의 확장 모듈입니다. |
log/ | 애플리케이션 로그 파일입니다. |
public/ | 정적 파일과 컴파일된 assets를 포함합니다. 앱이 실행 중일 때 이 디렉토리는 있는 그대로 노출됩니다. |
Rakefile | 명령줄에서 실행할 수 있는 작업을 찾아 로드하는 파일입니다. 작업 정의는 Rails의 컴포넌트 전반에 걸쳐 정의됩니다. Rakefile 을 수정하는 대신 애플리케이션의 lib/tasks 디렉토리에 파일을 추가하여 자체 작업을 추가해야 합니다. |
README.md | 애플리케이션에 대한 간단한 사용 설명서입니다. 이 파일을 편집하여 다른 사람들에게 애플리케이션의 기능, 설정 방법 등을 알려주어야 합니다. |
script/ | 일회성 또는 일반 용도의 스크립트와 벤치마크를 포함합니다. |
storage/ | Disk Service를 위한 SQLite 데이터베이스와 Active Storage 파일을 포함합니다. 이는 Active Storage Overview에서 다룹니다. |
test/ | 단위 테스트, fixtures 및 기타 테스트 도구들을 포함합니다. 이는 Testing Rails Applications에서 다룹니다. |
tmp/ | 임시 파일(캐시 및 pid 파일과 같은)입니다. |
vendor/ | 모든 서드파티 코드를 위한 장소입니다. 일반적인 Rails 애플리케이션에서는 vendored gems를 포함합니다. |
.dockerignore | 이 파일은 Docker에게 컨테이너에 복사하지 않아야 할 파일들을 알려줍니다. |
.gitattributes | 이 파일은 Git 저장소의 특정 경로에 대한 메타데이터를 정의합니다. 이 메타데이터는 Git 및 다른 도구들이 동작을 향상시키는 데 사용될 수 있습니다. 자세한 내용은 gitattributes documentation를 참조하세요. |
.git/ | Git 저장소 파일들을 포함합니다. |
.github/ | GitHub 관련 파일들을 포함합니다. |
.gitignore | 이 파일은 Git에게 무시해야 할 파일(또는 패턴)을 알려줍니다. 파일 무시에 대한 자세한 내용은 GitHub - Ignoring files를 참조하세요. |
.kamal/ | Kamal secrets와 배포 hooks를 포함합니다. |
.rubocop.yml | 이 파일은 RuboCop의 설정을 포함합니다. |
.ruby-version | 이 파일은 기본 Ruby 버전을 포함합니다. |
3.4 Model-View-Controller 기초
Rails 코드는 Model-View-Controller (MVC) 아키텍처를 사용하여 구성됩니다. MVC에서는 대부분의 코드가 존재하는 세 가지 주요 개념이 있습니다:
- Model - 애플리케이션의 데이터를 관리합니다. 일반적으로 데이터베이스 테이블입니다.
- View - HTML, JSON, XML 등 다양한 형식의 응답 렌더링을 처리합니다.
- Controller - 사용자 상호작용과 각 요청에 대한 로직을 처리합니다.
이제 MVC에 대한 기본적인 이해를 했으니, Rails에서 어떻게 사용되는지 살펴보겠습니다.
4 Hello, Rails!
쉽게 시작하기 위해 Rails 서버를 처음으로 부팅해보겠습니다.
터미널에서 store
디렉토리에 다음 명령어를 실행하세요:
$ bin/rails server
이것은 Puma라는 웹 서버를 시작하여 정적 파일과 Rails 애플리케이션을 제공합니다:
=> Puma 부팅중
=> Rails 8.1.0 어플리케이션이 development 모드로 시작됩니다
=> 더 많은 시작 옵션을 보려면 `bin/rails server --help`를 실행하세요
Puma가 단일 모드로 시작 중...
* Puma 버전: 6.4.3 (ruby 3.3.5-p100) ("The Eagle of Durango")
* 최소 스레드 수: 3
* 최대 스레드 수: 3
* 환경: development
* PID: 12345
* http://127.0.0.1:3000 에서 리스닝 중
* http://[::1]:3000 에서 리스닝 중
중지하려면 Ctrl-C를 누르세요
당신의 Rails 어플리케이션을 보려면, 브라우저에서 http://localhost:3000을 여세요. Rails 기본 환영 페이지가 보일 것입니다:
작동했습니다!
이 페이지는 새로운 Rails 어플리케이션의 smoke test로, 페이지를 제공하기 위한 모든 백그라운드 작업이 정상적으로 동작하는지 확인합니다.
Rails 서버를 언제든지 중단하려면, 터미널에서 Ctrl-C
를 누르세요.
4.1 Development에서의 Autoloading
개발자의 행복은 Rails의 핵심 철학이며, development 환경에서 자동 코드 리로딩을 통해 이를 실현할 수 있습니다.
Rails 서버를 시작하면 새로운 파일이나 기존 파일의 변경사항이 감지되어 필요에 따라 자동으로 로드되거나 리로드됩니다. 이를 통해 매번 변경사항이 있을 때마다 Rails 서버를 재시작할 필요 없이 개발에만 집중할 수 있습니다.
또한 다른 프로그래밍 언어에서 볼 수 있는 것과 달리 Rails 애플리케이션에서는 require
구문을 거의 사용하지 않는다는 것을 알 수 있습니다. Rails는 명명 규칙을 사용하여 파일을 자동으로 require하므로 애플리케이션 코드 작성에만 집중할 수 있습니다.
자세한 내용은 Autoloading and Reloading Constants를 참조하세요.
5 Database Model 생성하기
Active Record는 관계형 데이터베이스를 Ruby 코드에 매핑하는 Rails의 기능입니다. 테이블과 레코드의 생성, 업데이트, 삭제와 같은 데이터베이스와의 상호작용을 위한 structured query language(SQL)를 생성하는 데 도움을 줍니다. 우리의 애플리케이션은 Rails의 기본값인 SQLite를 사용하고 있습니다.
간단한 e-commerce 스토어에 상품을 추가하기 위해 우리의 Rails 애플리케이션에 데이터베이스 테이블을 추가하는 것부터 시작해보겠습니다.
$ bin/rails generate model Product name:string
이 명령어는 Rails에게 데이터베이스에 string
타입의 name
컬럼을 가진 Product
라는 이름의 model을 생성하도록 지시합니다. 이후에 다른 컬럼 타입을 추가하는 방법을 배우게 될 것입니다.
터미널에서 다음과 같은 내용이 표시됩니다:
invoke active_record
create db/migrate/20240426151900_create_products.rb
create app/models/product.rb
invoke test_unit
create test/models/product_test.rb
create test/fixtures/products.yml
이 명령은 다음과 같이 여러 가지를 생성합니다.
db/migrate
폴더에 migration을 생성합니다.app/models/product.rb
에 Active Record model을 생성합니다.- 이 model에 대한 테스트와 test fixtures를 생성합니다.
Model 이름은 단수입니다. 이는 인스턴스화된 model이 데이터베이스의 단일 레코드를 나타내기 때문입니다(즉, 데이터베이스에 추가할 product 하나를 생성하는 것입니다.).
5.1 데이터베이스 마이그레이션
migration은 데이터베이스에 적용하고자 하는 변경사항들의 집합입니다.
마이그레이션을 정의함으로써, 우리는 Rails에게 데이터베이스의 테이블, 컬럼 또는 다른 속성들을 추가, 변경, 삭제하기 위해 데이터베이스를 어떻게 변경할지 알려줍니다. 이는 개발 과정에서 만든 변경사항들(우리 컴퓨터에서만)을 추적하여 프로덕션(라이브, 온라인!)에 안전하게 배포할 수 있도록 도와줍니다.
코드 에디터에서 Rails가 생성한 마이그레이션을 열어 마이그레이션이 어떤 작업을 수행하는지 살펴보겠습니다. 이는 db/migrate/<timestamp>_create_products.rb
에 위치해 있습니다:
class CreateProducts < ActiveRecord::Migration[8.1]
def change
create_table :products do |t|
t.string :name
t.timestamps
end
end
end
이 마이그레이션은 Rails에게 products
라는 새로운 데이터베이스 테이블을 생성하라고 지시합니다.
위의 모델과는 대조적으로, Rails는 데이터베이스 테이블 이름을 복수형으로 만듭니다. 데이터베이스가 각 모델의 모든 인스턴스를 보유하기 때문입니다(즉, products 데이터베이스를 생성하는 것입니다).
create_table
블록은 이 데이터베이스 테이블에 어떤 컬럼과 타입이 정의되어야 하는지를 정의합니다.
t.string :name
은 Rails에게 products
테이블에 name
이라는 컬럼을 생성하고 타입을 string
으로 설정하라고 지시합니다.
t.timestamps
는 모델에 두 개의 컬럼을 정의하는 단축어입니다: created_at:datetime
과 updated_at:datetime
입니다. 이러한 컬럼들은 Rails의 대부분의 Active Record 모델에서 볼 수 있으며, 레코드를 생성하거나 업데이트할 때 Active Record에 의해 자동으로 설정됩니다.
5.2 Migration 실행하기
이제 데이터베이스에 어떤 변경사항을 적용할지 정의했으니, 다음 명령어를 사용하여 migration을 실행할 수 있습니다:
$ bin/rails db:migrate
이 명령어는 새로운 migration이 있는지 확인하고 데이터베이스에 적용합니다. 출력은 다음과 같습니다:
== 20240426151900 CreateProducts: 마이그레이트 중 ===================================
-- create_table(:products)
-> 0.0030s
== 20240426151900 CreateProducts: 마이그레이트 완료 (0.0031s) ==========================
실수를 했다면 bin/rails db:rollback
을 실행하여 마지막 migration을 취소할 수 있습니다.
6 Rails Console
이제 products 테이블을 생성했으니 Rails에서 상호작용을 해볼 수 있습니다. 한번 시도해보겠습니다.
이를 위해 console이라고 하는 Rails 기능을 사용할 것입니다. console은 Rails 애플리케이션에서 코드를 테스트하는데 도움이 되는 대화형 도구입니다.
$ bin/rails console
개발 환경 로딩 중 (Rails 8.1.0)
store(dev)>
여기서 Enter
를 칠 때 실행될 코드를 입력할 수 있습니다. Rails 버전을 출력해보겠습니다:
store(dev)> Rails.version
=> "8.1.0"
It works!
7 Active Record 모델 기본사항
Product
모델을 생성하기 위해 Rails model generator를 실행했을 때, app/models/product.rb
에 파일이 생성되었습니다. 이 파일은 products
데이터베이스 테이블과 상호작용하기 위해 Active Record를 사용하는 클래스를 생성합니다.
class Product < ApplicationRecord
end
이 클래스에 코드가 없다는 것에 놀랄 수 있습니다. Rails는 이 모델을 어떻게 정의하는지 어떻게 알까요?
Product
모델이 사용될 때, Rails는 데이터베이스 테이블에서 컬럼 이름과 타입을 조회하여 이러한 속성들에 대한 코드를 자동으로 생성합니다. Rails는 우리가 이런 상용구 코드를 작성하지 않아도 되게 해주며, 대신 우리가 애플리케이션 로직에만 집중할 수 있도록 뒤에서 이 작업을 처리해줍니다.
Rails console을 사용해서 Rails가 Product 모델에 대해 감지한 컬럼들을 확인해봅시다.
다음을 실행하세요:
store(dev)> Product.column_names
그리고 다음과 같이 보일 것입니다:
=> ["id", "name", "created_at", "updated_at"]
Rails는 위에서 데이터베이스에 컬럼 정보를 요청했고, 그 정보를 사용하여 Product
클래스의 attributes를 동적으로 정의했기 때문에 각각을 수동으로 정의할 필요가 없습니다. 이는 Rails가 어떻게 개발을 수월하게 만드는지 보여주는 한 예시입니다.
7.1 레코드 생성하기
다음 코드로 새로운 Product 레코드를 인스턴스화할 수 있습니다:
store(dev)> product = Product.new(name: "T-Shirt")
=> #<Product:0x000000012e616c30 id: nil, name: "T-Shirt", created_at: nil, updated_at: nil>
product
변수는 Product
의 인스턴스입니다. 데이터베이스에 저장되지 않았기 때문에 ID, created_at, updated_at 타임스탬프가 없습니다.
데이터베이스에 레코드를 작성하기 위해 save
를 호출할 수 있습니다.
store(dev)> product.save
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Store'*/
Product Create (0.9ms) "products" 테이블에 ("name", "created_at", "updated_at") VALUES ('T-Shirt', '2024-11-09 16:35:01.117836', '2024-11-09 16:35:01.117836') 값을 삽입하여 "id"를 반환 /*application='Store'*/
TRANSACTION (0.9ms) COMMIT TRANSACTION /*application='Store'*/
=> true
save
가 호출되면 Rails는 메모리에 있는 attributes를 가져와서 이 record를 데이터베이스에 삽입하는 INSERT
SQL query를 생성합니다.
Rails는 또한 데이터베이스 record의 id
와 함께 created_at
및 updated_at
timestamps를 메모리의 객체에 업데이트합니다. 이는 product
변수를 출력해보면 확인할 수 있습니다.
store(dev)> product
=> #<Product:0x00000001221f6260 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">
save
와 비슷하게, create
를 사용하여 한 번의 호출로 Active Record 객체를 인스턴스화하고 저장할 수 있습니다.
store(dev)> Product.create(name: "Pants")
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Store'*/
Product Create (0.4ms) "products" 테이블에 ("name", "created_at", "updated_at") VALUES ('Pants', '2024-11-09 16:36:01.856751', '2024-11-09 16:36:01.856751') RETURNING "id" INSERT 실행 /*application='Store'*/
TRANSACTION (0.1ms) COMMIT TRANSACTION /*application='Store'*/
=> #<Product:0x0000000120485c80 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">
7.2 레코드 쿼리하기
데이터베이스에서 Active Record 모델을 사용하여 레코드를 조회할 수 있습니다.
데이터베이스의 모든 Product 레코드를 찾으려면 all
메서드를 사용할 수 있습니다.
이것은 클래스 메서드이기 때문에 Product에서 사용할 수 있습니다(위의 save
와 같이 product 인스턴스에서 호출하는 인스턴스 메서드와는 다릅니다).
store(dev)> Product.all
Product Load (0.1ms) SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=> [#<Product:0x0000000121845158 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">,
#<Product:0x0000000121845018 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">]
이것은 products
테이블로부터 모든 레코드를 로드하기 위한 SELECT
SQL 쿼리를 생성합니다. 각 레코드는 자동으로 우리의 Product Active Record 모델의 인스턴스로 변환되어 Ruby에서 쉽게 작업할 수 있습니다.
all
메서드는 필터링, 정렬 및 다른 데이터베이스 작업을 실행하는 기능을 가진 데이터베이스 레코드의 Array와 유사한 컬렉션인 ActiveRecord::Relation
객체를 반환합니다.
7.3 Filtering & Ordering Records
데이터베이스의 결과를 필터링하고 싶다면 어떻게 해야 할까요? column별로 records를 필터링하기 위해서는 where
를 사용할 수 있습니다.
store(dev)> Product.where(name: "Pants")
Product Load (1.5ms) SELECT "products".* FROM "products" WHERE "products"."name" = 'Pants' /* loading for pp */ LIMIT 11 /*application='Store'*/
=> [#<Product:0x000000012184d858 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">]
이것은 SELECT
SQL 쿼리를 생성하고 WHERE
절을 추가하여 name
이 "Pants"
와 일치하는 레코드를 필터링합니다. 동일한 이름을 가진 레코드가 여러 개 있을 수 있으므로 이 역시 ActiveRecord::Relation
을 반환합니다.
order(name: :asc)
를 사용하여 name
을 기준으로 알파벳 오름차순으로 레코드를 정렬할 수 있습니다.
store(dev)> Product.order(name: :asc)
Product Load (0.3ms) SELECT "products".* FROM "products" /* loading for pp */ ORDER BY "products"."name" ASC LIMIT 11 /*application='Store'*/
=> [#<Product:0x0000000120e02a88 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">,
#<Product:0x0000000120e02948 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">]
7.4 레코드 찾기
특정 레코드를 찾고 싶다면 어떻게 해야 할까요?
클래스 메서드 find
를 사용하여 ID로 단일 레코드를 찾을 수 있습니다. 다음 코드로 메서드를 호출하고 특정 ID를 전달하세요:
store(dev)> Product.find(1)
Product Load (0.2ms) SELECT "products".* FROM "products" WHERE "products"."id" = 1 LIMIT 1 /*application='Store'*/
=> #<Product:0x000000012054af08 id: 1, name: "티셔츠", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">
This generates a SELECT
query but specifies a WHERE
for the id
column
matching the ID of 1
that was passed in. It also adds a LIMIT
to only return
a single record.
This time, we get a Product
instance instead of an ActiveRecord::Relation
since we're only retrieving a single record from the database.
7.5 레코드 업데이트하기
레코드는 2가지 방법으로 업데이트할 수 있습니다: update
를 사용하거나 속성을 할당하고 save
를 호출하는 방법입니다.
Product 인스턴스에서 update
를 호출하고 데이터베이스에 저장할 새로운 속성들의 Hash를 전달할 수 있습니다. 이는 속성들을 할당하고, 유효성 검사를 실행하며, 데이터베이스에 변경사항을 저장하는 것을 하나의 메소드 호출로 수행합니다.
store(dev)> product = Product.find(1)
store(dev)> product.update(name: "Shoes")
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Store'*/
Product Update (0.3ms) UPDATE "products" SET "name" = 'Shoes', "updated_at" = '2024-11-09 22:38:19.638912' WHERE "products"."id" = 1 /*application='Store'*/
TRANSACTION (0.4ms) COMMIT TRANSACTION /*application='Store'*/
=> true
이것은 데이터베이스에서 "T-Shirt" 제품의 이름을 "Shoes"로 업데이트했습니다.
이를 확인하려면 Product.all
을 다시 실행하세요.
store(dev)> Product.all
Shoes와 Pants 두 개의 제품이 보일 것입니다.
Product Load (0.3ms) SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=>
[#<Product:0x000000012c0f7300
id: 1,
name: "신발",
created_at: "2024-12-02 20:29:56.303546000 +0000",
updated_at: "2024-12-02 20:30:14.127456000 +0000">,
#<Product:0x000000012c0f71c0
id: 2,
name: "바지",
created_at: "2024-12-02 20:30:02.997261000 +0000",
updated_at: "2024-12-02 20:30:02.997261000 +0000">]
또는 우리는 attributes를 할당하고 데이터베이스에 변경사항을 유효성 검사 및 저장할 준비가 되었을 때 save
를 호출할 수 있습니다.
"Shoes"라는 이름을 다시 "T-Shirt"로 변경해 보겠습니다.
store(dev)> product = Product.find(1)
store(dev)> product.name = "T-Shirt"
=> "T-Shirt"
store(dev)> product.save
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Store'*/
Product Update (0.2ms) UPDATE "products" SET "name" = 'T-Shirt', "updated_at" = '2024-11-09 22:39:09.693548' WHERE "products"."id" = 1 /*application='Store'*/
TRANSACTION (0.0ms) COMMIT TRANSACTION /*application='Store'*/
=> true
7.6 레코드 삭제하기
destroy
메서드를 사용하여 데이터베이스에서 레코드를 삭제할 수 있습니다.
store(dev)> product.destroy
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Store'*/
Product Destroy (0.4ms) DELETE FROM "products" WHERE "products"."id" = 1 /*application='Store'*/
TRANSACTION (0.1ms) COMMIT TRANSACTION /*application='Store'*/
=> #<Product:0x0000000125813d48 id: 1, name: "T-Shirt", created_at: "2024-11-09 22:39:38.498730000 +0000", updated_at: "2024-11-09 22:39:38.498730000 +0000">
이것은 데이터베이스에서 T-Shirt 상품을 삭제했습니다. 우리는 Product.all
을 통해 Pants만 반환되는 것을 확인할 수 있습니다.
store(dev)> Product.all
Product Load (1.9ms) SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=>
[#<Product:0x000000012abde4c8
id: 2,
name: "Pants",
created_at: "2024-11-09 22:33:19.638912000 +0000",
updated_at: "2024-11-09 22:33:19.638912000 +0000">]
7.7 Validations
Active Record는 데이터베이스에 삽입되는 데이터가 특정 규칙을 준수하도록 하는 validations을 제공합니다.
Product 모델에 presence
validation을 추가하여 모든 제품에 name
이 있어야 하도록 해보겠습니다.
class Product < ApplicationRecord
validates :name, presence: true
end
Rails는 개발 중에 변경사항을 자동으로 리로드한다는 것을 기억하실 것입니다. 하지만 콘솔이 실행 중일 때 코드를 수정하면, 수동으로 새로고침을 해야 합니다. 그러니 지금 'reload!'를 실행하여 이를 수행해보겠습니다.
store(dev)> reload!
다시 로드 중...
Rails console에서 name이 없는 Product를 생성해봅시다.
store(dev)> product = Product.new
store(dev)> product.save
=> false
이번에는 name
속성이 지정되지 않았기 때문에 save
가 false
를 반환합니다.
Rails는 유효한 입력을 보장하기 위해 create, update, save 작업 중에 자동으로 validation을 수행합니다. Validation에 의해 생성된 오류 목록을 보려면 인스턴스에서 errors
를 호출할 수 있습니다.
store(dev)> product.errors
=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=name, type=blank, options={}>]>
이는 어떤 에러가 존재하는지 정확히 알려줄 수 있는 ActiveModel::Errors
객체를 반환합니다.
또한 사용자 인터페이스에서 사용할 수 있는 친숙한 에러 메시지를 생성할 수 있습니다.
store(dev)> product.errors.full_messages
=> ["Name은(는) 비어있을 수 없습니다"]
이제 Products를 위한 웹 인터페이스를 만들어보겠습니다.
콘솔 작업은 이제 끝났으니 exit
명령어를 실행해서 빠져나올 수 있습니다.
8 Rails에서 Request가 처리되는 과정
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가 혼합된 템플릿입니다.
이것이 간단한 설명이지만, 다음으로 이러한 각 단계를 더 자세히 살펴보겠습니다.
9 Routes
Rails에서 route는 들어오는 HTTP request를 적절한 controller와 action으로 연결하는 URL의 일부입니다. 먼저 URL과 HTTP Request methods에 대해 간단히 복습해보겠습니다.
9.1 URL의 구성 요소
URL의 다양한 부분을 살펴보겠습니다:
http://example.org/products?sale=true&sort=asc
이 URL에서 각 부분은 다음과 같은 이름을 가집니다:
https
는 protocol입니다example.org
는 host입니다/products
는 path입니다?sale=true&sort=asc
는 query parameters입니다
9.2 HTTP Methods와 그 목적
HTTP 요청은 methods를 사용하여 서버에게 주어진 URL에 대해 어떤 동작을 수행해야 하는지 알려줍니다. 가장 일반적인 methods는 다음과 같습니다:
GET
요청은 서버에게 주어진 URL에 대한 데이터를 검색하도록 지시합니다(예: 페이지 로딩 또는 레코드 가져오기).POST
요청은 처리를 위해 URL에 데이터를 제출합니다(일반적으로 새로운 레코드 생성).PUT
또는PATCH
요청은 기존 레코드를 업데이트하기 위해 URL에 데이터를 제출합니다.DELETE
요청은 서버에게 레코드를 삭제하도록 지시합니다.
9.3 Rails Routes
Rails에서 route
는 HTTP Method와 URL 경로를 연결하는 코드를 의미합니다. route는 또한 Rails에게 어떤 controller
와 action
이 요청에 응답해야 하는지 알려줍니다.
Rails에서 route를 정의하기 위해서는 코드 에디터로 돌아가서 config/routes.rb
에 다음 route를 추가해보겠습니다.
Rails.application.routes.draw do
get "/products", to: "products#index"
end
이 route는 Rails에게 /products
경로에 대한 GET 요청을 찾도록 지시합니다. 이 예제에서는 요청을 라우팅할 위치로 "products#index"
를 지정했습니다.
Rails가 일치하는 요청을 발견하면, 그 요청을 ProductsController
와 그 컨트롤러 내부의 index
action으로 보냅니다. 이것이 Rails가 요청을 처리하고 브라우저에 응답을 반환하는 방식입니다.
route에서 프로토콜, 도메인, 또는 query params를 지정할 필요가 없다는 것을 알 수 있습니다. 이는 기본적으로 프로토콜과 도메인이 요청이 서버에 도달하도록 보장하기 때문입니다. 그 이후에 Rails가 요청을 받아서 정의된 route를 기반으로 요청에 응답하기 위해 어떤 경로를 사용할지 알게 됩니다. query params는 Rails가 요청에 적용할 수 있는 옵션과 같아서, 일반적으로 컨트롤러에서 데이터를 필터링하는 데 사용됩니다.
다른 예제를 살펴보겠습니다. 이전 route 다음에 다음 줄을 추가하세요:
post "/products", to: "products#create"
products URL로 POST 요청이 오면 products 컨트롤러의 create 액션으로 라우팅합니다.
여기서는 Rails에게 "/products"로 들어오는 POST 요청을 ProductsController
의 create
액션으로 처리하도록 지시했습니다.
라우트는 또한 특정 패턴을 가진 URL과 매칭되어야 할 수도 있습니다. 이것은 어떻게 작동할까요?
get "/products/:id", to: "products#show"
이 라우트는 /products/1
같은 요청을 Products
controller의 show
action으로 매핑합니다. 요청 URL의 :id
부분이 params[:id]
를 통해 controller에서 사용할 수 있게 됩니다.
이 라우트에는 :id
가 포함되어 있습니다. 이를 parameter
라고 하며, URL의 일부를 캡처하여 나중에 요청을 처리하는 데 사용합니다.
사용자가 /products/1
을 방문하면 :id
파라미터는 1
로 설정되며, 컨트롤러 액션에서 ID가 1인 Product 레코드를 조회하고 표시하는 데 사용할 수 있습니다. /products/2
는 ID가 2인 Product를 표시하는 식입니다.
라우트 파라미터가 반드시 Integer일 필요는 없습니다.
예를 들어, 블로그에 게시글이 있고 다음 라우트로 /blog/hello-world
를 매칭할 수 있습니다:
get "/blog/:title", to: "blog#show"
/blog/:title 경로에 대해서 blog controller의 show action으로 route를 지정합니다.
Rails는 /blog/hello-world
에서 hello-world
를 캡처하고 이를 일치하는 제목의 블로그 포스트를 찾는데 사용할 수 있습니다.
get "/blog/:slug", to: "blog#show"
/blog/의 뒤에 어떤 값이 와도 이 route로 매칭됩니다. 예를 들어, /blog/first-post와 /blog/12345가 모두 매칭되어 params[:slug]에 해당 값이 전달됩니다.
9.3.1 CRUD Routes
리소스에 대해 일반적으로 필요한 4가지 공통 액션이 있습니다: Create, Read, Update, Delete (CRUD). 이는 7가지 일반적인 라우트로 변환됩니다:
- Index - 모든 레코드를 보여줍니다
- New - 새로운 레코드를 생성하기 위한 폼을 렌더링합니다
- Create - 새로운 폼 제출을 처리하고, 에러를 처리하며 레코드를 생성합니다
- Show - 특정 레코드를 보기 위해 렌더링합니다
- Edit - 특정 레코드를 업데이트하기 위한 폼을 렌더링합니다
- Update - 수정 폼 제출을 처리하고, 에러를 처리하며 레코드를 업데이트합니다
- Destroy - 특정 레코드 삭제를 처리합니다
다음과 같이 이러한 CRUD 액션에 대한 라우트를 추가할 수 있습니다:
get "/products", to: "products#index" # products의 목록 표시
get "/products/new", to: "products#new" # 새로운 product를 생성하기 위한 HTML 양식 표시
post "/products", to: "products#create" # product 생성
get "/products/:id", to: "products#show" # 특정 product 표시
get "/products/:id/edit", to: "products#edit" # product 수정을 위한 HTML 양식 표시
patch "/products/:id", to: "products#update" # product 수정
put "/products/:id", to: "products#update" # product 수정
delete "/products/:id", to: "products#destroy" # product 삭제
9.3.2 Resource Routes
이러한 라우트들을 매번 입력하는 것은 번거롭기 때문에, Rails는 이들을 정의하기 위한 단축 방법을 제공합니다. 동일한 CRUD 라우트를 모두 생성하려면 위의 라우트들을 다음의 한 줄로 대체하세요:
resources :products
이는 서버 안에서 많은 유용한 routes를 자동으로 선언합니다. "products"라는 이름의 Resource를 생성하기 위한 모든 가능한 standard routes를 자동으로 만들어줍니다.
만약 이러한 모든 CRUD 액션이 필요하지 않다면, 필요한 것만 정확히 지정할 수 있습니다. 자세한 내용은 routing 가이드를 확인하세요.
9.4 Routes 명령어
Rails는 애플리케이션이 응답하는 모든 route를 표시하는 명령어를 제공합니다.
터미널에서 다음 명령어를 실행하세요.
$ bin/rails routes
resources :products
에 의해 생성된 라우트들을 출력된 내용에서 볼 수 있습니다.
Prefix Verb URI 패턴 Controller#Action
products GET /products(.:format) products#index
POST /products(.:format) products#create
new_product GET /products/new(.:format) products#new
edit_product GET /products/:id/edit(.:format) products#edit
product GET /products/:id(.:format) products#show
PATCH /products/:id(.:format) products#update
PUT /products/:id(.:format) products#update
DELETE /products/:id(.:format) products#destroy
또한 health check와 같은 다른 Rails 내장 기능의 라우트도 볼 수 있습니다.
10 Controllers & Actions
이제 Products에 대한 라우트를 정의했으니, 이러한 URL에 대한 요청을 처리하는 controller와 action을 구현해보겠습니다.
이 명령어는 index action이 있는 ProductsController
를 생성합니다. 이미 라우트를 설정했기 때문에, flag를 사용하여 generator의 해당 부분을 건너뛸 수 있습니다.
$ bin/rails generate controller Products index --skip-routes
create app/controllers/products_controller.rb
invoke erb
create app/views/products
create app/views/products/index.html.erb
invoke test_unit
create test/controllers/products_controller_test.rb
invoke helper
create app/helpers/products_helper.rb
invoke test_unit
이 명령어는 컨트롤러를 위한 여러 파일들을 생성합니다:
- 컨트롤러 자체
- 생성된 컨트롤러를 위한 views 폴더
- 컨트롤러 생성 시 지정한 액션을 위한 view 파일
- 이 컨트롤러를 위한 test 파일
- view에서 로직을 분리하기 위한 helper 파일
app/controllers/products_controller.rb
에 정의된 ProductsController를 살펴보겠습니다. 코드는 다음과 같습니다:
class ProductsController < ApplicationController
def index
end
end
You may notice the file name products_controller.rb
is an underscored
version of the Class this file defines, ProductsController
. This pattern helps
Rails to automatically load code without having to use require
like you may
have seen in other languages.
The index
method here is an Action. Even though it's an empty method, Rails
will default to rendering a template with the matching name.
The index
action will render app/views/products/index.html.erb
. If we open
up that file in our code editor, we'll see the HTML it renders.
<h1>Products#index</h1>
<p>app/views/products/index.html.erb에서 저를 찾으세요</p>
10.1 요청하기
브라우저에서 한번 살펴보겠습니다. 먼저, 터미널에서 bin/rails server
를 실행하여 Rails 서버를 시작하세요. 그런 다음 http://localhost:3000 을 열면 Rails 환영 페이지가 표시됩니다.
브라우저에서 http://localhost:3000/products 를 열면 Rails가 products index HTML을 렌더링할 것입니다.
우리의 브라우저가 /products
를 요청했고 Rails는 이 route를 products#index
와 매칭했습니다. Rails는 요청을 ProductsController
로 보내고 index
action을 호출했습니다. 이 action이 비어있었기 때문에, Rails는 app/views/products/index.html.erb
에 있는 매칭되는 template을 렌더링하여 브라우저로 반환했습니다. 꽤 멋지죠!
config/routes.rb
를 열어보면, 다음 line을 추가하여 root route가 Products index action을 렌더링하도록 Rails에 지시할 수 있습니다:
root "products#index"
"products#index"를 애플리케이션의 루트 경로로 설정합니다.
http://localhost:3000을 방문하면, Rails는 Products#index를 렌더링할 것입니다.
10.2 Instance Variables
이를 한 단계 더 진행하여 데이터베이스의 일부 레코드를 렌더링해보겠습니다.
index
action에서 데이터베이스 쿼리를 추가하고 이를 instance variable에 할당해보겠습니다. Rails는 instance variable(@ 기호로 시작하는 변수)을 사용하여 view와 데이터를 공유합니다.
class ProductsController < ApplicationController
def index
@products = Product.all
end
end
app/views/products/index.html.erb
에서 HTML을 다음 ERB로 대체할 수 있습니다:
<%= debug @products %>
ERB는 Embedded Ruby의 약자이며 Ruby 코드를 실행하여 Rails로 HTML을 동적으로 생성할 수 있게 해줍니다. <%= %>
태그는 ERB에게 내부의 Ruby 코드를 실행하고 반환값을 출력하도록 지시합니다. 우리의 경우, 이는 @products
를 가져와서 YAML로 변환한 다음 YAML을 출력합니다.
이제 브라우저에서 http://localhost:3000/ 을 새로고침하면 출력이 변경된 것을 볼 수 있습니다. 보이는 것은 데이터베이스의 레코드들이 YAML 형식으로 표시된 것입니다.
debug
헬퍼는 디버깅을 돕기 위해 변수들을 YAML 형식으로 출력합니다. 예를 들어, 만약 여러분이 주의를 기울이지 않아서 복수형 @products
대신 단수형 @product
를 입력했다면, debug 헬퍼는 컨트롤러에서 변수가 올바르게 설정되지 않았다는 것을 식별하는데 도움을 줄 수 있습니다.
사용 가능한 더 많은 헬퍼들을 보려면 Action View Helpers 가이드를 확인하세요.
모든 제품 이름을 렌더링하도록 app/views/products/index.html.erb
를 업데이트해 봅시다.
<h1>Products</h1>
<div id="products">
<% @products.each do |product| %>
<div>
<%= product.name %>
</div>
<% end %>
</div>
ERB를 사용하면 이 코드는 @products
ActiveRecord::Relation
객체의 각 product를 순회하면서 product 이름이 포함된 <div>
태그를 렌더링합니다.
이번에는 새로운 ERB 태그도 사용했습니다. <% %>
는 Ruby 코드를 평가하지만 반환값을 출력하지 않습니다. 이는 @products.each
의 출력을 무시하므로, HTML에 포함하고 싶지 않은 배열이 출력되는 것을 방지합니다.
10.3 CRUD Actions
개별 products에 접근할 수 있어야 합니다. 이는 리소스를 읽는 CRUD의 R에 해당합니다.
우리는 이미 resources :products
route로 개별 products의 route를 정의했습니다. 이는 products#show
를 가리키는 route로 /products/:id
를 생성합니다.
이제 ProductsController
에 해당 action을 추가하고 호출될 때 어떤 일이 발생하는지 정의해야 합니다.
10.4 개별 Product 보여주기
Products controller를 열고 show
액션을 다음과 같이 추가하세요:
class ProductsController < ApplicationController
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
end
show
액션은 단일 레코드를 데이터베이스에서 불러오기 때문에 단수형 @product
를 정의합니다. 다시 말해서: 이 하나의 product를 보여줍니다. 여러 products를 불러오는 index
에서는 복수형 @products
를 사용합니다.
데이터베이스를 조회하기 위해, request parameters에 접근하는 params
를 사용합니다. 이 경우에는 /products/:id
라우트의 :id
를 사용합니다. /products/1
을 방문할 때, params 해시는 {id: 1}
을 포함하고 있으며, 이는 show
액션에서 Product.find(1)
을 호출하여 ID가 1
인 Product를 데이터베이스에서 불러오게 됩니다.
다음으로 show 액션을 위한 view가 필요합니다. Rails 명명 규칙에 따라, ProductsController
는 app/views
의 products
라는 하위 폴더에 있는 view를 기대합니다.
show
액션은 app/views/products/show.html.erb
파일을 기대합니다. 에디터에서 해당 파일을 생성하고 다음 내용을 추가해봅시다:
<h1><%= @product.name %></h1>
<%= link_to "뒤로", products_path %>
app/views/products/index.html.erb
view를 업데이트하여 각 product의 show 페이지로 연결되는 링크를 추가하면 클릭하여 탐색할 수 있어 유용할 것입니다. show
action으로 향하는 경로를 앵커 태그로 사용하여 이 새로운 페이지로 연결할 수 있습니다.
<h1>Products</h1>
<div id="products">
<% @products.each do |product| %>
<div>
<a href="/products/<%= product.id %>">
<%= product.name %>
</a>
</div>
<% end %>
</div>
브라우저에서 이 페이지를 새로고침하면 잘 작동하는 것을 볼 수 있지만, 더 개선할 수 있습니다.
Rails는 path와 URL을 생성하기 위한 helper 메서드를 제공합니다. bin/rails routes
를 실행하면 Prefix 컬럼을 볼 수 있습니다. 이 prefix는 Ruby 코드로 URL을 생성할 때 사용할 수 있는 helper와 일치합니다.
Prefix Verb URI Pattern Controller#Action
products GET /products(.:format) products#index
product GET /products/:id(.:format) products#show
이러한 route prefix들은 다음과 같은 helper들을 제공합니다:
products_path
는"/products
"`를 생성합니다products_url
은"http://localhost:3000/products
"`를 생성합니다product_path(1)
는"/products/1"
를 생성합니다product_url(1)
은"http://localhost:3000/products/1"
를 생성합니다
_path
는 브라우저가 현재 도메인에 대한 것으로 인식하는 상대 경로를 반환합니다.
_url
은 프로토콜, 호스트, 포트를 포함한 전체 URL을 반환합니다.
URL helper들은 브라우저 외부에서 볼 이메일을 렌더링할 때 유용합니다.
link_to
helper와 함께 사용하면, anchor 태그를 생성하고 URL helper를 사용해 Ruby에서 이를 깔끔하게 처리할 수 있습니다. link_to
는 링크에 표시될 내용(product.name
)과 href
속성에 사용할 경로 또는 URL(product
)을 인자로 받습니다.
이제 이러한 helper들을 사용하도록 리팩토링해보겠습니다:
<h1>Products</h1>
<div id="products">
<% @products.each do |product| %>
<div>
<%= link_to product.name, product %>
</div>
<% end %>
</div>
10.5 상품 생성하기
지금까지는 Rails console에서 상품을 생성해야 했지만, 이제는 브라우저에서 동작하도록 만들어보겠습니다.
생성을 위해 두 가지 action이 필요합니다:
- 상품 정보를 수집하기 위한 new product form
- 상품을 저장하고 에러를 체크하기 위한 controller의 create action
controller action부터 시작해보겠습니다.
class ProductsController < ApplicationController
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
def new
@product = Product.new
end
end
new
action은 form 필드를 표시하는 데 사용할 새로운 Product
를 인스턴스화합니다.
app/views/products/index.html.erb
를 업데이트하여 new action에 대한 링크를 추가할 수 있습니다.
<h1>Products</h1>
<%= link_to "새 상품", new_product_path %>
<div id="products">
<% @products.each do |product| %>
<div>
<%= link_to product.name, product %>
</div>
<% end %>
</div>
app/views/products/new.html.erb
파일을 생성하여 새로운 Product
를 위한 form을 렌더링해보겠습니다.
<h1>새 상품</h1>
<%= form_with model: @product do |form| %>
<div>
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
이 view에서는 Rails의 form_with
헬퍼를 사용하여 product를 생성하기 위한 HTML form을 생성합니다. 이 헬퍼는 form builder를 사용하여 CSRF 토큰 처리, model:
로 제공된 URL 생성, 심지어 모델에 맞는 제출 버튼 텍스트 조정까지 처리합니다.
브라우저에서 이 페이지를 열고 소스 보기를 하면 form의 HTML은 다음과 같이 보입니다:
<form action="/products" accept-charset="UTF-8" method="post">
<input type="hidden" name="authenticity_token" value="UHQSKXCaFqy_aoK760zpSMUPy6TMnsLNgbPMABwN1zpW-Jx6k-2mISiF0ulZOINmfxPdg5xMyZqdxSW1UK-H-Q" autocomplete="off">
<div>
<label for="product_name">이름</label>
<input type="text" name="product[name]" id="product_name">
</div>
<div>
<input type="submit" name="commit" value="상품 생성" data-disable-with="상품 생성">
</div>
</form>
form builder는 보안을 위한 CSRF 토큰을 포함시키고, UTF-8을 지원하도록 form을 설정하며, input field 이름을 설정하고 submit 버튼의 비활성화 상태까지 추가했습니다.
새로운 Product
인스턴스를 form builder에 전달했기 때문에, 새로운 레코드를 생성하기 위한 기본 라우트인 /products
로 POST
요청을 보내도록 form이 자동으로 구성되었습니다.
이를 처리하기 위해서는 먼저 controller에 create
액션을 구현해야 합니다.
class ProductsController < ApplicationController
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
def new
@product = Product.new
end
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new, status: :unprocessable_entity
end
end
private
def product_params
params.expect(product: [ :name ])
end
end
10.5.1 Strong Parameters
The create
action handles the data submitted by the form, but it needs to be
filtered for security. That's where the product_params
method comes into play.
In product_params
, we tell Rails to inspect the params and ensure there is a
key named :product
with an array of parameters as the value. The only
permitted parameters for products is :name
and Rails will ignore any other
parameters. This protects our application from malicious users who might try to
hack our application.
10.5.2 Handling Errors
After assigning these params to the new Product
, we can try to save it to the
database. @product.save
tells Active Record to run validations and save the
record to the database.
If save
is successful, we want to redirect to the new product. When
redirect_to
is given an Active Record object, Rails generates a path for that
record's show action.
redirect_to @product
그리고 Rails는 해당 경로를 생성하기 위해 @product 객체의 내용을 검사합니다. 다음의 경우와 같이 작성됩니다:
@product
는 Product
인스턴스이기 때문에, Rails는 모델 이름을 복수형으로 만들고 리다이렉트를 위해 객체의 ID를 경로에 포함하여 "/products/2"
와 같은 형태로 만듭니다.
save
가 실패하고 레코드가 유효하지 않을 때, 우리는 사용자가 잘못된 데이터를 수정할 수 있도록 폼을 다시 렌더링하기를 원합니다. else
절에서, 우리는 Rails에게 render :new
를 지시합니다. Rails는 우리가 Products
컨트롤러에 있다는 것을 알고 있으므로, app/views/products/new.html.erb
를 렌더링해야 합니다. create
에서 @product
변수를 설정했기 때문에, 데이터베이스에 저장되지 못했더라도 해당 템플릿을 렌더링하고 폼에 우리의 Product
데이터를 채울 수 있습니다.
또한 HTTP 상태를 422 Unprocessable Entity로 설정하여 브라우저에게 이 POST 요청이 실패했음을 알리고 그에 따라 처리하도록 합니다.
10.6 제품 수정하기
레코드를 수정하는 프로세스는 레코드를 생성하는 것과 매우 유사합니다. new
와 create
action 대신에, edit
과 update
를 사용하게 됩니다.
다음과 같이 controller에서 이를 구현해봅시다:
class ProductsController < ApplicationController
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
def new
@product = Product.new
end
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new, status: :unprocessable_entity
end
end
def edit
@product = Product.find(params[:id])
end
def update
@product = Product.find(params[:id])
if @product.update(product_params)
redirect_to @product
else
render :edit, status: :unprocessable_entity
end
end
private
def product_params
params.expect(product: [ :name ])
end
end
이제 app/views/products/show.html.erb
에 Edit 링크를 추가할 수 있습니다:
<h1><%= @product.name %></h1>
<%= link_to "뒤로", products_path %>
<%= link_to "수정", edit_product_path(@product) %>
10.6.1 Before Actions
show
와 마찬가지로 edit
와 update
는 기존 데이터베이스 레코드가 필요하므로 이를 before_action
으로 중복을 제거할 수 있습니다.
before_action
을 사용하면 액션들 간에 공유되는 코드를 추출하여 액션 이전에 실행할 수 있습니다. 위의 컨트롤러 코드에서 @product = Product.find(params[:id])
는 세 가지 메서드에서 정의되어 있습니다. 이 쿼리를 set_product
라는 before action으로 추출하면 각 액션의 코드를 깔끔하게 정리할 수 있습니다.
이는 DRY(Don't Repeat Yourself) 철학이 실제로 적용된 좋은 예시입니다.
class ProductsController < ApplicationController
before_action :set_product, only: %i[ show edit update ]
def index
@products = Product.all
end
def show
end
def new
@product = Product.new
end
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @product.update(product_params)
redirect_to @product
else
render :edit, status: :unprocessable_entity
end
end
private
def set_product
@product = Product.find(params[:id])
end
def product_params
params.expect(product: [ :name ])
end
end
10.6.2 Extracting Partials
We've already written a form for creating new products. Wouldn't it be nice if we could reuse that for edit and update? We can, using a feature called "partials" that allows you to reuse a view in multiple places.
We can move the form into a file called app/views/products/_form.html.erb
. The
filename starts with an underscore to denote this is a partial.
We also want to replace any instance variables with a local variable, which we
can define when we render the partial. We'll do this by replacing @product
with product
.
<%= form_with model: product do |form| %>
<div>
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
로컬 변수를 사용하면 partials를 페이지 내에서 매번 다른 값으로 여러 번 재사용할 수 있습니다. 이는 index 페이지와 같이 항목 리스트를 렌더링할 때 유용합니다.
app/views/products/new.html.erb
view에서 이 partial을 사용하기 위해, form을 render 호출로 대체할 수 있습니다:
<h1>새 상품</h1>
<%= render "form", product: @product %>
<%= link_to "취소", products_path %>
edit view는 form partial 덕분에 거의 똑같아집니다.
다음과 같이 app/views/products/edit.html.erb
를 생성해보겠습니다:
<h1>상품 수정</h1>
<%= render "form", product: @product %>
<%= link_to "취소", @product %>
뷰 partial에 대해 더 자세히 알아보려면 Action View Guide를 참고하세요.
10.7 상품 삭제하기
구현해야 할 마지막 기능은 상품 삭제입니다. ProductsController에 destroy
action을 추가하여 DELETE /products/:id
요청을 처리할 것입니다.
before_action :set_product
에 destroy
를 추가하면 다른 action들과 같은 방식으로 @product
instance 변수를 설정할 수 있습니다.
class ProductsController < ApplicationController
before_action :set_product, only: %i[ show edit update destroy ]
def index
@products = Product.all
end
def show
end
def new
@product = Product.new
end
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @product.update(product_params)
redirect_to @product
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@product.destroy
redirect_to products_path
end
private
def set_product
@product = Product.find(params[:id])
end
def product_params
params.expect(product: [ :name ])
end
end
이를 동작하게 만들기 위해서는 app/views/products/show.html.erb
에 Delete 버튼을 추가해야 합니다:
<h1><%= @product.name %></h1>
<%= link_to "뒤로", products_path %>
<%= link_to "수정", edit_product_path(@product) %>
<%= button_to "삭제", @product, method: :delete, data: { turbo_confirm: "삭제하시겠습니까?" } %>
button_to
는 "Delete" 텍스트가 있는 단일 버튼이 포함된 폼을 생성합니다.
이 버튼을 클릭하면 폼이 제출되어 /products/:id
로 DELETE
요청을 보내고, 이는 컨트롤러의 destroy
액션을 실행시킵니다.
turbo_confirm
데이터 속성은 Turbo JavaScript 라이브러리에게 폼을 제출하기 전에 사용자에게 확인을 요청하도록 지시합니다. 이에 대해서는 곧 자세히 살펴보겠습니다.
11 인증 추가하기
누구나 제품을 수정하거나 삭제할 수 있는 것은 안전하지 않습니다. 제품을 관리하기 위해서는 사용자 인증이 필요하도록 보안을 추가해보겠습니다.
Rails에는 사용할 수 있는 인증 제너레이터가 있습니다. 이는 User와 Session 모델을 생성하고, 애플리케이션에 로그인하는 데 필요한 컨트롤러와 뷰를 생성합니다.
터미널로 돌아가서 다음 명령어를 실행하세요:
$ bin/rails generate authentication
그런 다음 데이터베이스를 마이그레이션하여 User와 Session 테이블을 추가합니다.
$ bin/rails db:migrate
Rails console을 열어서 User를 생성합니다.
$ bin/rails console
Rails console에서 User를 생성하려면 User.create!
메소드를 사용하세요. 예제 대신 자신의 이메일과 비밀번호를 사용해도 됩니다.
store(dev)> User.create! email_address: "you@example.org", password: "s3cr3t", password_confirmation: "s3cr3t"
Rails 서버를 재시작하여 generator가 추가한 bcrypt
gem을 적용하세요. BCrypt는 인증을 위한 비밀번호를 안전하게 해싱하는 데 사용됩니다.
$ bin/rails server
어떤 페이지를 방문하면 Rails는 사용자 이름과 비밀번호를 요구할 것입니다. User record를 생성할 때 사용했던 이메일과 비밀번호를 입력하세요.
http://localhost:3000/products/new 를 방문해서 시도해보세요.
올바른 사용자 이름과 비밀번호를 입력하면 접근이 허용됩니다. 브라우저는 이후 요청을 위해 이 인증 정보를 저장하므로 페이지를 볼 때마다 입력할 필요가 없습니다.
11.1 Log Out 추가하기
애플리케이션에서 로그아웃하기 위해, app/views/layouts/application.html.erb
의 상단에 버튼을 추가할 수 있습니다. 이 layout은 header나 footer와 같이 모든 페이지에 포함시키고 싶은 HTML을 넣는 곳입니다.
<body>
안에 Home 링크와 Log out 버튼이 있는 작은 <nav>
섹션을 추가하고, yield
를 <main>
태그로 감싸주세요.
<!DOCTYPE html>
<html>
<!-- ... -->
<body>
<nav>
<%= link_to "홈", root_path %>
<%= button_to "로그아웃", session_path, method: :delete if authenticated? %>
</nav>
<main>
<%= yield %>
</main>
</body>
</html>
이는 사용자가 인증된 경우에만 Log out 버튼을 표시합니다. 클릭하면 session path로 DELETE 요청을 보내서 사용자를 로그아웃시킵니다.
11.2 미인증 접근 허용하기
하지만 우리 스토어의 상품 index와 show 페이지는 모든 사람이 접근할 수 있어야 합니다. 기본적으로 Rails authentication generator는 모든 페이지를 인증된 사용자만 접근할 수 있도록 제한합니다.
게스트가 상품을 볼 수 있도록 하기 위해, 우리는 controller에서 미인증 접근을 허용할 수 있습니다.
class ProductsController < ApplicationController
allow_unauthenticated_access only: %i[ index show ]
# ...
end
로그아웃하고 products index와 show 페이지를 방문하여 인증 없이도 접근 가능한지 확인해보세요.
11.3 인증된 사용자에게만 링크 표시하기
로그인한 사용자만 product를 생성할 수 있으므로, app/views/products/index.html.erb
view를 수정하여 인증된 사용자에게만 새 product 링크를 표시할 수 있습니다.
<%= link_to "새 상품", new_product_path if authenticated? %>
Log out 버튼을 클릭하면 New 링크가 숨겨진 것을 볼 수 있습니다. http://localhost:3000/session/new 에서 로그인하면 index 페이지에서 New 링크를 볼 수 있습니다.
선택적으로, 인증되지 않은 경우 Login 링크를 추가하기 위해 navbar에 이 route에 대한 링크를 포함할 수 있습니다.
<%= link_to "로그인", new_session_path unless authenticated? %>
app/views/products/show.html.erb
뷰의 Edit와 Destroy 링크도 인증된 경우에만 표시되도록 업데이트할 수 있습니다.
<h1><%= @product.name %></h1>
<%= link_to "Back", products_path %>
<% if authenticated? %>
<%= link_to "Edit", edit_product_path(@product) %>
<%= button_to "Destroy", @product, method: :delete, data: { turbo_confirm: "정말 삭제하시겠습니까?" } %>
<% end %>
12 Caching Products
Sometimes caching specific parts of a page can improve performance. Rails simplifies this process with Solid Cache, a database-backed cache store that comes included by default.
Using the cache
method, we can store HTML in the cache. Let's cache the header
in app/views/products/show.html.erb
.
<% cache @product do %>
<h1><%= @product.name %></h1>
<% end %>
@product
를 cache
에 전달하면 Rails는 해당 product에 대한 고유한 cache key를 생성합니다. Active Record 객체들은 "products/1"
과 같은 문자열을 반환하는 cache_key
메서드를 가지고 있습니다. view의 cache
helper는 이것을 template digest와 결합하여 이 HTML에 대한 고유한 key를 생성합니다.
development 환경에서 caching을 활성화하려면 터미널에서 다음 명령어를 실행하세요.
$ bin/rails dev:cache
상품의 show action을 방문할 때(예: /products/2
), Rails 서버 로그에서 새로운 캐싱 내용이 표시됩니다:
fragment views/products/show:a5a585f985894cd27c8b3d49bb81de3a/products/1-20240918154439539125 읽기 (1.6ms)
fragment views/products/show:a5a585f985894cd27c8b3d49bb81de3a/products/1-20240918154439539125 쓰기 (4.0ms)
처음 이 페이지를 열 때, Rails는 cache key를 생성하고 cache store에 존재하는지 묻습니다. 이것이 Read fragment
라인입니다.
첫 번째 페이지 뷰이므로 cache가 존재하지 않아 HTML이 생성되고 cache에 쓰여집니다. 이것을 로그에서 Write fragment
라인으로 확인할 수 있습니다.
페이지를 새로고침하면 로그에 더 이상 Write fragment
가 없는 것을 볼 수 있습니다.
Read fragment 조회 views/products/show:a5a585f985894cd27c8b3d49bb81de3a/products/1-20240918154439539125 (1.3ms)
캐시 엔트리는 마지막 요청에 의해 작성되었으므로, Rails는 두 번째 요청에서 캐시 엔트리를 찾습니다. 또한 Rails는 오래된 캐시 데이터를 절대 렌더링하지 않도록 레코드가 업데이트될 때 캐시 키를 변경합니다.
자세한 내용은 Rails의 캐싱 가이드에서 확인하세요.
13 Action Text를 사용한 Rich Text Fields
많은 애플리케이션에서 embeds(즉, 멀티미디어 요소)가 포함된 rich text가 필요하며, Rails는 Action Text를 통해 이 기능을 기본적으로 제공합니다.
Action Text를 사용하려면 먼저 installer를 실행해야 합니다:
$ bin/rails action_text:install
$ bundle install
$ bin/rails db:migrate
Rails 서버를 재시작하여 모든 새로운 기능이 로딩되었는지 확인하세요.
이제 product에 rich text description 필드를 추가해보겠습니다.
먼저 다음을 Product
모델에 추가하세요:
class Product
has_rich_text :description
validates :name, presence: true
end
이제 app/views/products/_form.html.erb
의 submit 버튼 앞에 description을 편집할 수 있는 rich text field를 포함하도록 form을 업데이트할 수 있습니다.
<%= form_with model: product do |form| %>
<%# ... %>
<div>
<%= form.label :description, style: "display: block" %>
<%= form.rich_text_area :description %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
Our controller also needs to permit this new parameter when the form is
submitted, so we'll update the permitted params to include description in
app/controllers/products_controller.rb
# Only allow a list of trusted parameters through.
def product_params
params.expect(product: [ :name, :description ])
end
설명을 표시하기 위해 app/views/products/show.html.erb
의 show view도 업데이트해야 합니다:
<% cache @product do %>
<h1><%= @product.name %></h1>
<%= @product.description %>
<% end %>
다음과 같은 Rails가 생성한 캐시 키는 view가 수정될 때도 변경됩니다. 이는 캐시가 최신 버전의 view 템플릿과 동기화 되도록 해줍니다.
새로운 product를 생성하고 bold와 italic 텍스트가 있는 설명을 추가해보세요. show 페이지가 포맷된 텍스트를 표시하고 product를 편집할 때 이 rich text가 text area에서 유지되는 것을 보실 수 있습니다.
자세히 알아보시려면 Action Text Overview를 확인해보세요.
14 File Uploads with Active Storage
Action Text는 파일 업로드를 쉽게 만들어주는 Rails의 또 다른 기능인 Active Storage를 기반으로 만들어졌습니다.
product를 편집하고 rich text 에디터에 이미지를 드래그한 다음 레코드를 업데이트 해보세요. Rails가 이 이미지를 업로드하고 rich text 에디터 안에 렌더링하는 것을 보실 수 있습니다. 멋지죠?!
Active Storage를 직접 사용할 수도 있습니다. Product
모델에 featured image를 추가해봅시다.
class Product
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
end
그리고 submit 버튼 앞에 파일 업로드 필드를 product form에 추가할 수 있습니다:
<%= form_with model: product do |form| %>
<%# ... %>
<div>
<%= form.label :featured_image, style: "display: block" %>
<%= form.file_field :featured_image, accept: "image/*" %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
app/controllers/products_controller.rb
에서 :featured_image
를 permitted parameter로 추가하세요
# 신뢰할 수 있는 파라미터 목록만 허용
def product_params
params.expect(product: [ :name, :description, :featured_image ])
end
마지막으로, app/views/products/show.html.erb
에서 product의 featured image를 표시하려고 합니다. 상단에 다음을 추가하세요.
<%= image_tag @product.featured_image if @product.featured_image.attached? %>
상품의 이미지를 업로드해보고 저장 후 show 페이지에서 이미지가 표시되는 것을 확인해보세요.
더 자세한 내용은 Active Storage Overview를 참고하세요.
15 국제화 (I18n)
Rails는 앱을 다른 언어로 번역하기 쉽게 만들어줍니다.
view에서 사용하는 translate
또는 t
헬퍼는 이름으로 번역을 찾아 현재 로케일의 텍스트를 반환합니다.
app/products/index.html.erb
에서 header 태그를 번역을 사용하도록 업데이트해보겠습니다.
<h1><%= t "hello" %></h1>
페이지를 새로고침하면 이제 Hello world
가 헤더 텍스트로 표시되는 것을 볼 수 있습니다. 이것은 어디서 온 걸까요?
기본 언어가 영어이기 때문에 Rails는 (rails new
중에 생성된) config/locales/en.yml
에서 해당 locale에 맞는 키를 찾습니다.
en:
hello: "안녕하세요 세상"
에디터에서 스페인어를 위한 새로운 locale 파일을 생성하고 config/locales/es.yml
에 번역을 추가해봅시다.
es:
hello: "안녕하세요 세상"
Rails에게 어떤 locale을 사용할지 알려줄 필요가 있습니다. 가장 간단한 방법은 URL의 locale 파라미터를 찾는 것입니다. app/controllers/application_controller.rb
에서 다음과 같이 설정할 수 있습니다:
class ApplicationController < ActionController::Base
# ...
around_action :switch_locale
def switch_locale(&action)
locale = params[:locale] || I18n.default_locale
I18n.with_locale(locale, &action)
end
end
이는 모든 request를 실행하고 params에서 locale
을 찾거나 기본 locale로 대체합니다. request에 대한 locale을 설정하고 완료된 후 재설정합니다.
- http://localhost:3000/products?locale=en 방문 시, 영어 번역을 보게 됩니다.
- http://localhost:3000/products?locale=es 방문 시, 스페인어 번역을 보게 됩니다.
- locale param 없이 http://localhost:3000/products 방문 시, 영어로 대체됩니다.
이제 "Hello world"
대신 실제 번역을 사용하도록 index 헤더를 업데이트해봅시다.
<h1><%= t ".title" %></h1>
title
앞의 .
을 보셨나요? 이것은 Rails에게 상대적인 locale 조회를 사용하라고 알려줍니다. 상대적 조회는 controller와 action을 key에 자동으로 포함시키므로 매번 입력할 필요가 없습니다. 영어 locale을 사용하는 .title
의 경우, en.products.index.title
을 찾게 됩니다.
config/locales/en.yml
에서는 우리의 controller, view, 그리고 translation 이름과 일치하도록 products
와 index
아래에 title
key를 추가해야 합니다.
en:
hello: "안녕하세요 세계"
products:
index:
title: "제품들"
Spanish 로케일 파일에서도 동일하게 적용할 수 있습니다:
es:
hello: "안녕하세요 세계"
products:
index:
title: "제품"
Rails 영어 지역을 볼 때는 "Products"가 표시되고 스페인어 지역을 볼 때는 "Productos"가 표시됩니다.
Rails 국제화(I18n) API에 대해 자세히 알아보세요.
16 재입고 알림 추가하기
e커머스 스토어의 일반적인 기능 중 하나는 상품이 재입고되었을 때 알림을 받기 위한 이메일 구독입니다. 이제 Rails의 기본 사항을 살펴보았으니 이 기능을 우리 스토어에 추가해 보겠습니다.
16.1 기본 재고 추적
먼저, 재고를 추적할 수 있도록 Product model에 재고 수량을 추가해보겠습니다. 다음 command를 사용하여 migration을 생성할 수 있습니다:
$ bin/rails generate migration AddInventoryCountToProducts inventory_count:integer
그리고 migration을 실행해봅시다.
$ bin/rails db:migrate
app/views/products/_form.html.erb
에서 product form에 inventory count를 추가해야 합니다.
<%= form_with model: product do |form| %>
<%# ... %>
<div>
<%= form.label :inventory_count, style: "display: block" %>
<%= form.number_field :inventory_count %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
controller에도 허용된 파라미터에 :inventory_count
를 추가해야 합니다.
def product_params
params.expect(product: [ :name, :description, :featured_image, :inventory_count ])
end
재고 수량이 절대 음수가 되지 않도록 validation하는 것도 유용할 것입니다. 그러므로 model에 해당 validation도 추가해보겠습니다.
class Product < ApplicationRecord
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
end
이러한 변경으로 이제 우리 스토어의 product들의 재고 수량을 업데이트할 수 있습니다.
16.2 제품에 구독자 추가하기
제품의 재입고를 사용자에게 알리기 위해서는 이러한 구독자들을 추적해야 합니다.
이메일 주소를 저장하고 해당 제품과 연결하기 위해 Subscriber라는 모델을 생성해보겠습니다.
$ bin/rails generate model Subscriber product:belongs_to email
이제 새로운 migration을 실행하세요:
$ bin/rails db:migrate
product:belongs_to
를 포함함으로써, Rails에게 subscribers와 products가 일대다 관계를 가지고 있다고 알려주었습니다. 즉, Subscriber는 하나의 Product 인스턴스에 "belong to" 관계에 있음을 의미합니다.
하지만 Product는 많은 subscribers를 가질 수 있으므로, Product 모델에 has_many :subscribers, dependent: :destroy
를 추가하여 두 모델 간의 관계의 나머지 부분을 추가합니다. 이는 Rails에게 두 데이터베이스 테이블 간의 조인 쿼리를 어떻게 수행할지 알려줍니다.
class Product < ApplicationRecord
# subscriber들을 가지며, 상품이 삭제되면 연관된 subscriber도 함께 삭제됩니다
has_many :subscribers, dependent: :destroy
# 대표 이미지 하나를 첨부할 수 있습니다
has_one_attached :featured_image
# rich text 형식의 설명을 가집니다
has_rich_text :description
# 이름은 필수값입니다
validates :name, presence: true
# 재고 수량은 0 이상이어야 합니다
validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
end
이제 이러한 subscriber들을 생성하기 위한 controller가 필요합니다. app/controllers/subscribers_controller.rb
에 다음 코드로 생성해봅시다:
class SubscribersController < ApplicationController
allow_unauthenticated_access
before_action :set_product
def create
@product.subscribers.where(subscriber_params).first_or_create
redirect_to @product, notice: "구독이 완료되었습니다."
end
private
def set_product
@product = Product.find(params[:product_id])
end
def subscriber_params
params.expect(subscriber: [ :email ])
end
end
redirect는 Rails flash에 notice를 설정합니다. flash는 다음 페이지에 표시할 메시지를 저장하는 데 사용됩니다.
flash 메시지를 표시하기 위해, app/views/layouts/application.html.erb
파일의 body 안에 notice를 추가해봅시다:
<html>
<!-- ... -->
<body>
<div class="notice"><%= notice %></div>
<!-- ... -->
</body>
</html>
To subscribe users to a specific product, we'll use a nested route so we know
which product the subscriber belongs to. In config/routes.rb
change
resources :products
to the following:
resources :products do
resources :subscribers, only: [ :create ]
end
product에 대한 모든 standard route들을 생성하고 subscribers에 대해서는 create action만을 중첩시킵니다.
상품 상세 페이지에서 재고가 있는지 확인하고 재고 수량을 표시할 수 있습니다. 그렇지 않다면, 품절 메시지와 함께 재입고 알림을 받을 수 있는 구독 양식을 표시할 수 있습니다.
app/views/products/_inventory.html.erb
에 새로운 partial을 생성하고 다음을 추가하세요:
<% if product.inventory_count? %>
<p><%= product.inventory_count %> 개 재고 있음</p>
<% else %>
<p>재고 없음</p>
<p>재입고시 이메일로 알려드립니다.</p>
<%= form_with model: [product, Subscriber.new] do |form| %>
<%= form.email_field :email, placeholder: "you@example.com", required: true %>
<%= form.submit "제출" %>
<% end %>
<% end %>
그런 다음 app/views/products/show.html.erb
를 업데이트하여 cache
블록 다음에 이 partial을 렌더링하세요.
<%= render "inventory", product: @product %>
16.3 In Stock Email 알림
Action Mailer는 이메일을 보낼 수 있게 해주는 Rails의 기능입니다. 이를 사용하여 상품이 재입고되었을 때 구독자들에게 알림을 보내겠습니다.
다음 명령어로 mailer를 생성할 수 있습니다:
$ bin/rails g mailer Product in_stock
이것은 app/mailers/product_mailer.rb
에 in_stock
메서드를 가진 클래스를 생성합니다.
구독자의 이메일 주소로 메일을 보내기 위해 이 메서드를 업데이트하세요.
class ProductMailer < ApplicationMailer
# 제목은 config/locales/en.yml의 I18n 파일에서
# 다음 lookup으로 설정할 수 있습니다:
#
# en.product_mailer.in_stock.subject
#
def in_stock
@product = params[:product]
mail to: params[:subscriber].email
end
end
mailer 제너레이터는 views 폴더에 HTML 용과 Text 용 두 개의 이메일 템플릿을 생성합니다. 템플릿을 수정하여 메시지와 제품으로의 링크를 포함할 수 있습니다.
app/views/product_mailer/in_stock.html.erb
를 다음과 같이 수정하세요:
<h1>좋은 소식입니다!</h1>
<p><%= link_to @product.name, product_url(@product) %>가 재입고되었습니다.</p>
그리고 app/views/product_mailer/in_stock.text.erb
파일을 다음과 같이 작성합니다:
좋은 소식입니다!
<%= @product.name %>이(가) 다시 입고되었습니다.
<%= product_url(@product) %>
이메일 클라이언트가 링크 클릭 시 브라우저에서 열어야 할 전체 URL을 알아야 하기 때문에 메일러에서는 product_path
대신 product_url
을 사용합니다.
Rails 콘솔을 열고 보낼 product와 subscriber를 로드하여 이메일을 테스트할 수 있습니다:
store(dev)> product = Product.first
store(dev)> subscriber = product.subscribers.find_or_create_by(email: "subscriber@example.org")
store(dev)> ProductMailer.with(product: product, subscriber: subscriber).in_stock.deliver_later
로그에 이메일이 출력되는 것을 볼 수 있습니다.
ProductMailer#in_stock: 처리된 발신 메일 63.0ms
전달된 메일 66a3a9afd5d4a_108b04a4c41443@local.mail (33.1ms)
날짜: Fri, 26 Jul 2024 08:50:39 -0500
보낸사람: from@example.com
받는사람: subscriber@example.com
Message-ID: <66a3a9afd5d4a_108b04a4c41443@local.mail>
제목: 재고 있음
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="--==_mimepart_66a3a9afd235e_108b04a4c4136f";
charset=UTF-8
Content-Transfer-Encoding: 7bit
----==_mimepart_66a3a9afd235e_108b04a4c4136f
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
좋은 소식입니다!
티셔츠가 재입고되었습니다.
http://localhost:3000/products/1
----==_mimepart_66a3a9afd235e_108b04a4c4136f
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
<!-- BEGIN app/views/layouts/mailer.html.erb --><!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
/* Email 스타일은 인라인으로 작성되어야 합니다 */
</style>
</head>
<body>
<!-- BEGIN app/views/product_mailer/in_stock.html.erb --><h1>좋은 소식입니다!</h1>
<p><a href="http://localhost:3000/products/1">티셔츠</a>가 재입고되었습니다.</p>
<!-- END app/views/product_mailer/in_stock.html.erb -->
</body>
</html>
<!-- END app/views/layouts/mailer.html.erb -->
----==_mimepart_66a3a9afd235e_108b04a4c4136f--
ActionMailer::MailDeliveryJob 수행됨 (Job ID: 5e2bd5f2-f54f-4088-ace3-3f6eb15aaf46) Async(default)에서 111.34ms
Product 모델에서 콜백을 사용하여 inventory 개수가 0에서 양수로 변경될 때마다 이메일을 보낼 수 있습니다.
class Product < ApplicationRecord
has_one_attached :featured_image
has_rich_text :description
has_many :subscribers, dependent: :destroy
validates :name, presence: true
validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
after_update_commit :notify_subscribers, if: :back_in_stock?
def back_in_stock?
# inventory_count가 0에서 0보다 큰 수로 변경되었는지 확인
inventory_count_previously_was.zero? && inventory_count > 0
end
def notify_subscribers
# 각 구독자에게 재입고 알림 메일 발송
subscribers.each do |subscriber|
ProductMailer.with(product: self, subscriber: subscriber).in_stock.deliver_later
end
end
end
after_update_commit
은 데이터베이스에 변경 사항이 저장된 후 호출되는 Active Record callback입니다. if: :back_in_stock?
는 back_in_stock?
메서드가 true를 반환할 때만 callback이 실행되도록 지시합니다.
Active Record는 속성의 변경 사항을 추적하므로 back_in_stock?
는 inventory_count_previously_was
를 사용하여 inventory_count
의 이전 값을 확인합니다. 그런 다음 현재 재고 수량과 비교하여 제품이 재입고되었는지 판단할 수 있습니다.
notify_subscribers
는 Active Record 연관관계를 사용하여 이 특정 제품의 모든 구독자를 subscribers
테이블에서 조회한 다음, 각 구독자에게 in_stock
이메일을 전송하기 위해 대기열에 추가합니다.
16.4 Concern 추출하기
Product 모델은 이제 notification을 처리하기 위한 상당한 양의 코드를 가지고 있습니다. 코드를 더 잘 구성하기 위해, 이를 ActiveSupport::Concern
으로 추출할 수 있습니다. Concern은 사용하기 쉽도록 문법적 장치가 포함된 Ruby 모듈입니다.
먼저 Notifications 모듈을 만들어보겠습니다.
app/models/product/notifications.rb
에 다음과 같은 파일을 생성하세요:
module Product::Notifications
extend ActiveSupport::Concern
included do
has_many :subscribers, dependent: :destroy
after_update_commit :notify_subscribers, if: :back_in_stock?
end
def back_in_stock?
inventory_count_previously_was == 0 && inventory_count > 0
end
def notify_subscribers
subscribers.each do |subscriber|
ProductMailer.with(product: self, subscriber: subscriber).in_stock.deliver_later
end
end
end
모듈을 클래스에 포함시키면 included
블록 내부의 모든 코드가 해당 클래스의 일부인 것처럼 실행됩니다. 동시에, 모듈에 정의된 메서드들은 해당 클래스의 객체(인스턴스)에서 호출할 수 있는 일반 메서드가 됩니다.
이제 notification을 트리거하는 코드가 Notification 모듈로 추출되었으므로, Product 모델은 Notifications 모듈을 포함하도록 단순화될 수 있습니다.
class Product < ApplicationRecord
include Notifications
has_many :subscribers, dependent: :destroy
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
end
Concerns는 Rails 애플리케이션의 기능을 구성하는 좋은 방법입니다. Product에 더 많은 기능을 추가할수록 클래스가 지저분해질 것입니다. 대신 Concerns를 사용하여 각 기능을 Product::Notifications
와 같은 독립적인 모듈로 추출할 수 있으며, 이 모듈에는 구독자를 처리하고 알림을 보내는 방법에 대한 모든 기능이 포함됩니다.
코드를 concerns로 추출하면 기능을 재사용하기 쉽게 만드는 데도 도움이 됩니다. 예를 들어, 구독자 알림이 필요한 새로운 모델을 도입할 수 있습니다. 이 모듈은 동일한 기능을 제공하기 위해 여러 모델에서 사용될 수 있습니다.
16.5 구독 취소 링크
구독자가 어떤 시점에서 구독을 취소하고 싶을 수 있으므로, 다음으로 이것을 구현해보겠습니다.
먼저, 이메일에 포함시킬 URL에서 사용할 구독 취소 route가 필요합니다.
resource :unsubscribe, only: [ :show ]
구독 해지를 위한 단일 리소스를 정의하며 show 액션만 허용합니다.
Active Record에는 다양한 목적으로 데이터베이스 레코드를 찾기 위한 고유한 토큰을 생성할 수 있는 generates_token_for
라는 기능이 있습니다. 이메일의 구독 취소 URL에서 사용할 고유한 구독 취소 토큰을 생성하는 데 이것을 활용할 수 있습니다.
class Subscriber < ApplicationRecord
belongs_to :product
generates_token_for :unsubscribe
end
컨트롤러는 먼저 URL의 token으로부터 Subscriber 레코드를 찾을 것입니다. 구독자가 발견되면, 해당 레코드를 삭제하고 홈페이지로 리다이렉트합니다. app/controllers/unsubscribes_controller.rb
를 생성하고 다음 코드를 추가하세요:
class UnsubscribesController < ApplicationController
allow_unauthenticated_access
before_action :set_subscriber
def show
@subscriber&.destroy
redirect_to root_path, notice: "성공적으로 구독을 해지했습니다."
end
private
def set_subscriber
@subscriber = Subscriber.find_by_token_for(:unsubscribe, params[:token])
end
end
마지막으로, email template에 unsubscribe 링크를 추가해봅시다.
app/views/product_mailer/in_stock.html.erb
에서 link_to
를 추가하세요:
<h1>좋은 소식입니다!</h1>
<p><%= link_to @product.name, product_url(@product) %>가 재입고되었습니다.</p>
<%= link_to "구독 해지", unsubscribe_url(token: params[:subscriber].generate_token_for(:unsubscribe)) %>
app/views/product_mailer/in_stock.text.erb
에서 URL을 일반 텍스트로 추가하세요:
좋은 소식입니다!
<%= @product.name %>이(가) 재입고 되었습니다.
<%= product_url(@product) %>
구독 취소: <%= unsubscribe_url(token: params[:subscriber].generate_token_for(:unsubscribe)) %>
구독 취소 링크를 클릭하면 subscriber 레코드가 데이터베이스에서 삭제됩니다. 컨트롤러는 또한 오류를 발생시키지 않고 유효하지 않거나 만료된 토큰을 안전하게 처리합니다.
Rails console을 사용하여 다른 이메일을 보내고 로그에서 구독 취소 링크를 테스트해보세요.
17 Adding CSS & JavaScript
CSS & JavaScript는 웹 애플리케이션을 구축하는데 핵심이므로, Rails에서 이들을 사용하는 방법을 알아보겠습니다.
17.1 Propshaft
Rails의 asset pipeline을 Propshaft라고 합니다. CSS, JavaScript, 이미지 및 기타 asset들을 브라우저에 제공합니다. 프로덕션 환경에서 Propshaft는 페이지를 더 빠르게 로드할 수 있도록 캐싱을 위해 각 asset의 버전을 추적합니다. 이것이 어떻게 작동하는지 자세히 알아보려면 Asset Pipeline 가이드를 확인하세요.
app/assets/stylesheets/application.css
를 수정하고 글꼴을 sans-serif로 변경해보겠습니다.
body {
font-family: Arial, Helvetica, sans-serif;
padding: 1rem;
}
nav {
justify-content: flex-end;
display: flex;
font-size: 0.875em;
gap: 0.5rem;
max-width: 1024px;
margin: 0 auto;
padding: 1rem;
}
nav a {
display: inline-block;
}
main {
max-width: 1024px;
margin: 0 auto;
}
.notice {
color: green;
}
section.product {
display: flex;
gap: 1rem;
flex-direction: row;
}
section.product img {
border-radius: 8px;
flex-basis: 50%;
max-width: 50%;
}
그런 다음 새로운 스타일을 사용하기 위해 app/views/products/show.html.erb
를 업데이트하겠습니다.
<p><%= link_to "뒤로", products_path %></p>
<section class="product">
<%= image_tag @product.featured_image if @product.featured_image.attached? %>
<section class="product-info">
<% cache @product do %>
<h1><%= @product.name %></h1>
<%= @product.description %>
<% end %>
<%= render "inventory", product: @product %>
<% if authenticated? %>
<%= link_to "수정", edit_product_path(@product) %>
<%= button_to "삭제", @product, method: :delete, data: { turbo_confirm: "정말로 삭제하시겠습니까?" } %>
<% end %>
</section>
</section>
페이지를 새로고침하면 CSS가 적용된 것을 확인할 수 있습니다.
17.2 Import Maps
Rails는 기본적으로 JavaScript를 위해 import maps를 사용합니다. 이를 통해 빌드 단계 없이 현대적인 JavaScript 모듈을 작성할 수 있습니다.
JavaScript pins는 config/importmap.rb
에서 찾을 수 있습니다. 이 파일은 브라우저에서 importmap 태그를 생성하는 데 사용되는 소스 파일과 JavaScript 패키지 이름을 매핑합니다.
# ./bin/importmap 실행으로 npm 패키지 고정하기
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
각각의 pin은 JavaScript 패키지 이름(예: "@hotwired/turbo-rails"
)을
특정 파일이나 URL(예: "turbo.min.js"
)에 매핑합니다. pin_all_from
은
디렉토리(예: app/javascript/controllers
) 내의 모든 파일을 네임스페이스(예:
"controllers"
)에 매핑합니다.
Import map은 모던 JavaScript 기능을 지원하면서도 설정을 깔끔하고 최소한으로 유지합니다.
import map에 이미 있는 이 JavaScript 파일들은 무엇일까요? 이것들은 Rails가 기본적으로 사용하는 Hotwire라는 프론트엔드 프레임워크입니다.
17.3 Hotwire
Hotwire는 서버 사이드에서 생성된 HTML을 최대한 활용하도록 설계된 JavaScript 프레임워크입니다. 다음과 같은 3가지 핵심 컴포넌트로 구성됩니다:
- Turbo는 커스텀 JavaScript를 작성하지 않고도 네비게이션, 폼 제출, 페이지 컴포넌트 및 업데이트를 처리합니다.
- Stimulus는 페이지에 기능을 추가하기 위해 커스텀 JavaScript가 필요한 경우를 위한 프레임워크를 제공합니다.
- Native를 사용하면 웹 앱을 임베딩하고 네이티브 모바일 기능으로 점진적으로 향상시켜 하이브리드 모바일 앱을 만들 수 있습니다.
우리는 아직 JavaScript를 작성하지 않았지만, 프론트엔드에서 Hotwire를 사용해왔습니다. 예를 들어, 제품을 추가하고 편집하기 위해 만든 폼은 Turbo로 동작했습니다.
자세한 내용은 Asset Pipeline과 Working with JavaScript in Rails 가이드에서 확인하세요.
18 Testing
Rails는 강력한 테스트 스위트와 함께 제공됩니다. 제품이 재입고되었을 때 올바른 수의 이메일이 전송되는지 확인하기 위한 테스트를 작성해보겠습니다.
18.1 Fixtures
Rails로 모델을 생성할 때, test/fixtures
디렉토리에 해당하는 fixture 파일이 자동으로 생성됩니다.
Fixtures는 테스트를 실행하기 전에 테스트 데이터베이스를 채우는 사전 정의된 데이터 세트입니다. 기억하기 쉬운 이름으로 레코드를 정의할 수 있어 테스트에서 쉽게 접근할 수 있습니다.
이 파일은 기본적으로 비어있습니다 - 테스트를 위해 fixtures로 채워야 합니다.
test/fixtures/products.yml
위치의 product fixtures 파일을 다음과 같이 업데이트해 보겠습니다:
tshirt:
name: 티셔츠
inventory_count: 15
subscribers 테스트를 위해서는 test/fixtures/subscribers.yml
에 이 두 fixture를 추가합시다:
david:
product: tshirt
email: david@example.org
chris:
product: tshirt
email: chris@example.org
여기서 Product
fixture를 이름으로 참조할 수 있다는 것을 알 수 있습니다. Rails는 데이터베이스에서 이를 자동으로 연결해주므로 테스트에서 record ID와 association을 직접 관리할 필요가 없습니다.
이러한 fixture들은 테스트 스위트를 실행할 때 자동으로 데이터베이스에 삽입됩니다.
18.2 Email 테스트하기
test/models/product_test.rb
에서 테스트를 추가해봅시다:
require "test_helper"
class ProductTest < ActiveSupport::TestCase
include ActionMailer::TestHelper
test "재고가 다시 들어왔을 때 이메일 알림을 보냄" do
product = products(:tshirt)
# 상품을 재고 없음으로 설정
product.update(inventory_count: 0)
assert_emails 2 do
product.update(inventory_count: 99)
end
end
end
이 테스트가 하는 일을 분석해보겠습니다.
먼저, 테스트 중에 전송된 이메일을 모니터링할 수 있도록 Action Mailer test helper를 포함시킵니다.
tshirt
fixture는 products()
fixture helper를 사용하여 로드되며 해당 레코드에 대한 Active Record 객체를 반환합니다. 각 fixture는 데이터베이스 ID가 실행할 때마다 다를 수 있기 때문에 이름으로 fixture를 쉽게 참조할 수 있도록 테스트 스위트에서 helper를 생성합니다.
그런 다음 재고를 0으로 업데이트하여 tshirt가 품절되었는지 확인합니다.
다음으로, assert_emails
를 사용하여 블록 내부의 코드에서 2개의 이메일이 생성되었는지 확인합니다. 이메일을 트리거하기 위해, 블록 안에서 제품의 재고 수량을 업데이트합니다. 이는 Product 모델의 notify_subscribers
콜백을 트리거하여 이메일을 전송합니다. 실행이 완료되면, assert_emails
는 이메일 수를 세고 예상 개수와 일치하는지 확인합니다.
테스트 스위트는 bin/rails test
로 실행하거나 파일명을 전달하여 개별 테스트 파일을 실행할 수 있습니다.
$ bin/rails test test/models/product_test.rb
단일 프로세스에서 1개의 테스트 실행 중 (병렬화 임계값은 50)
실행 옵션: --seed 3556
# 실행 중:
.
0.343842초 만에 완료, 2.9083 runs/s, 5.8166 assertions/s.
1 runs, 2 assertions, 0 failures, 0 errors, 0 skips
테스트가 통과했습니다!
Rails는 또한 test/mailers/product_mailer_test.rb
에 ProductMailer
의 예제 테스트를 생성했습니다. 이 테스트도 통과하도록 업데이트해봅시다.
require "test_helper"
class ProductMailerTest < ActionMailer::TestCase
test "in_stock" do
mail = ProductMailer.with(product: products(:tshirt), subscriber: subscribers(:david)).in_stock
assert_equal "재고 있음", mail.subject
assert_equal [ "david@example.org" ], mail.to
assert_equal [ "from@example.com" ], mail.from
assert_match "좋은 소식입니다!", mail.body.encoded
end
end
이제 전체 테스트 스위트를 실행하고 모든 테스트가 통과하는지 확인해봅시다.
$ bin/rails test
Running 2 tests in a single process (parallelization threshold is 50)
Run options: --seed 16302
# Running:
..
Finished in 0.665856s, 3.0037 runs/s, 10.5128 assertions/s.
2 runs, 7 assertions, 0 failures, 0 errors, 0 skips
전체 기능을 테스트하는 테스트 스위트를 계속 구축하기 위한 출발점으로 이를 사용할 수 있습니다.
Rails 애플리케이션 테스트에 대해 자세히 알아보세요.
19 Rubocop을 사용한 일관된 코드 포맷팅
코드를 작성할 때 때로는 일관되지 않은 포맷팅을 사용할 수 있습니다. Rails에는 코드를 일관되게 포맷팅하는 데 도움이 되는 Rubocop이라는 linter가 포함되어 있습니다.
다음 명령을 실행하여 코드의 일관성을 확인할 수 있습니다:
$ bin/rubocop
rails 코드에서 위반 사항이 있다면 출력하고 어떤 내용인지 알려줄 것입니다.
53개 파일 검사 중
.....................................................
53개 파일 검사됨, 위반사항 없음
Rubocop은 --autocorrect
플래그(또는 축약형 -a
)를 사용하여 위반사항을 자동으로 수정할 수 있습니다.
$ bin/rubocop -a
20 Security
Rails는 애플리케이션의 보안 문제를 검사하기 위한 Brakeman gem을 포함하고 있습니다 - 세션 하이재킹, 세션 고정, 리디렉션과 같은 공격으로 이어질 수 있는 취약점들을 검사합니다.
bin/brakeman
을 실행하면 애플리케이션을 분석하고 보고서를 출력합니다.
$ bin/brakeman
스캐너 로딩중...
...
== 개요 ==
Controllers: 6
Models: 6
Templates: 15
Errors: 0
보안 경고: 0
== 경고 유형 ==
경고사항이 없습니다
GitHub Actions를 사용한 Continuous Integration
Rails 애플리케이션 보안에 대해 자세히 알아보세요
21 GitHub Actions를 사용한 Continuous Integration
Rails 앱은 rubocop, brakeman 및 테스트 스위트를 실행하는 미리 작성된 GitHub Actions 설정이 포함된 .github
폴더를 생성합니다.
GitHub Actions가 활성화된 GitHub 저장소에 코드를 push하면, 자동으로 이러한 단계들이 실행되고 각각의 성공 또는 실패를 보고합니다. 이를 통해 코드 변경사항의 결함과 문제를 모니터링하고 작업의 일관된 품질을 보장할 수 있습니다.
22 Production 환경에 배포하기
이제 재미있는 부분입니다: 앱을 배포해봅시다.
Rails는 Kamal이라는 배포 도구를 제공하며, 이를 사용하여 애플리케이션을 서버에 직접 배포할 수 있습니다. Kamal은 Docker 컨테이너를 사용하여 애플리케이션을 실행하고 무중단 배포를 수행합니다.
기본적으로 Rails는 Kamal이 Docker 이미지를 빌드하는 데 사용할 production-ready Dockerfile을 제공하며, 이는 모든 의존성과 설정이 포함된 애플리케이션의 컨테이너화된 버전을 만듭니다. 이 Dockerfile은 Thruster를 사용하여 production 환경에서 효율적으로 assets를 압축하고 제공합니다.
Kamal로 배포하기 위해서는 다음이 필요합니다:
- 1GB 이상의 RAM을 가진 Ubuntu LTS를 실행하는 서버. 서버는 정기적인 보안 및 버그 수정을 받을 수 있도록 Long-Term Support(LTS) 버전의 Ubuntu 운영 체제를 실행해야 합니다. Hetzner, DigitalOcean 및 기타 호스팅 서비스에서 시작하기 위한 서버를 제공합니다.
- Docker Hub 계정과 액세스 토큰. Docker Hub는 서버에서 다운로드하고 실행할 수 있도록 애플리케이션 이미지를 저장합니다.
Docker Hub에서 애플리케이션 이미지를 위한 Repository를 생성하세요. 저장소 이름으로 "store"를 사용하세요.
config/deploy.yml
을 열고 192.168.0.1
을 서버의 IP 주소로, your-user
를 Docker Hub 사용자 이름으로 변경하세요.
# Name of your application. Used to uniquely configure containers.
service: store
# Name of the container image.
image: your-user/store
# Deploy to these servers.
servers:
web:
- 192.168.0.1
# Credentials for your image host.
registry:
# Specify the registry server, if you're not using Docker Hub
# server: registry.digitalocean.com / ghcr.io / ...
username: your-user
proxy:
섹션에서 애플리케이션의 SSL을 활성화할 도메인을 추가할 수 있습니다. DNS 레코드가 서버를 가리키도록 확인하면 Kamal이 LetsEncrypt를 사용하여 해당 도메인에 대한 SSL 인증서를 발급합니다.
proxy:
ssl: true
host: app.example.com
Docker 웹사이트에서 액세스 토큰을 생성하세요. Kamal이 애플리케이션의 Docker 이미지를 push할 수 있도록 Read & Write 권한이 필요합니다.
그런 다음 Kamal이 토큰을 찾을 수 있도록 터미널에서 액세스 토큰을 export 하세요.
export KAMAL_REGISTRY_PASSWORD=your-access-token
서버를 설정하고 애플리케이션을 처음 배포하기 위해 다음 명령어를 실행하세요.
$ bin/kamal setup
축하합니다! 여러분의 새로운 Rails 애플리케이션이 실제로 운영 환경에서 실행되고 있습니다!
새로운 Rails 앱이 작동하는 것을 보려면, 브라우저를 열고 서버의 IP 주소를 입력하세요. 스토어가 실행되고 있는 것을 볼 수 있을 것입니다.
이후에 앱을 수정하고 production 환경에 배포하고 싶을 때는 다음과 같이 실행할 수 있습니다:
$ bin/kamal deploy
22.1 프로덕션에 User 추가하기
프로덕션에서 상품을 생성하고 편집하기 위해서는, 프로덕션 데이터베이스에 User 레코드가 필요합니다.
Kamal을 사용하여 프로덕션 Rails 콘솔을 열 수 있습니다.
$ bin/kamal console
store(prod)> User.create!(email_address: "you@example.org", password: "s3cr3t", password_confirmation: "s3cr3t")
이제 이 이메일과 비밀번호로 production 환경에 로그인하여 product를 관리할 수 있습니다.
22.2 Solid Queue를 사용한 Background Jobs
Background jobs는 별도의 프로세스에서 비동기적으로 태스크를 실행할 수 있게 해주어, 사용자 경험을 방해하지 않도록 합니다. 10,000명의 수신자에게 주식 이메일을 보내는 것을 상상해보세요. 시간이 꽤 걸릴 수 있으므로, Rails 앱의 응답성을 유지하기 위해 해당 태스크를 background job으로 옮길 수 있습니다.
개발 환경에서 Rails는 ActiveJob로 background jobs를 처리하기 위해 :async
queue adapter를 사용합니다. Async는 대기 중인 jobs를 메모리에 저장하지만 재시작 시 대기 중인 jobs는 사라집니다. 이는 개발에는 좋지만 프로덕션에는 적합하지 않습니다.
Background jobs를 더 견고하게 만들기 위해 Rails는 프로덕션 환경에서 solid_queue
를 사용합니다. Solid Queue는 jobs를 데이터베이스에 저장하고 별도의 프로세스에서 실행합니다.
Solid Queue는 config/deploy.yml
에 SOLID_QUEUE_IN_PUMA: true
환경 변수를 사용하여 프로덕션 Kamal 배포에서 활성화됩니다. 이는 웹 서버인 Puma에게 Solid Queue 프로세스를 자동으로 시작하고 중지하도록 지시합니다.
Action Mailer의 deliver_later
로 이메일을 보낼 때, 이 이메일들은 HTTP 요청을 지연시키지 않도록 백그라운드에서 보내기 위해 Active Job으로 전송됩니다. 프로덕션의 Solid Queue를 통해 이메일은 백그라운드에서 전송되고, 전송 실패 시 자동으로 재시도되며, 재시작 중에도 jobs는 데이터베이스에 안전하게 보관됩니다.
23 다음 단계
첫 번째 Rails 애플리케이션을 구축하고 배포한 것을 축하드립니다!
계속해서 기능을 추가하고 업데이트를 배포하면서 학습을 이어나가시기를 추천드립니다. 다음과 같은 아이디어들이 있습니다:
- CSS로 디자인 개선하기
- 제품 리뷰 추가하기
- 앱을 다른 언어로 번역 완성하기
- 결제를 위한 체크아웃 플로우 추가하기
- 사용자가 제품을 저장할 수 있는 위시리스트 추가하기
- 제품 이미지를 위한 캐러셀 추가하기
또한 다른 Ruby on Rails 가이드들을 읽으면서 더 많이 배우시기를 추천드립니다:
즐거운 개발되세요!