Getting Started with Rails
위 튜토리얼을 따라 학습하며 작성한 글입니다.
개요
제품에 재고가 채워지면 email 로 알림을 받을 수 있도록 만들것이다.
제품당 여러명에게 email 을 보내야하므로 products 테이블에 email 을 추가하면 안되고 새로운 table 을 만들어서 email 을 등록할 것이다.
user 테이블에 email 을 추가 후 관계를 맺어도 되지만, 여기서는 구독자 table 을 만들어서 관계를 만들것이다.
model 생성
1
2
3
4
5
6
7
$ bin/rails generate model Subscriber product:belongs_to email
invoke active_record
create db/migrate/20251103133732_create_subscribers.rb
create app/models/subscriber.rb
invoke test_unit
create test/models/subscriber_test.rb
create test/fixtures/subscribers.yml
susbcribers 에 products_id 를 외래키로 넣기위해 product:belongs_to 를 작성해준다.
그리고 email column 이 있어야하므로 email 도 넣었다.
이렇게 생성된 테이블은 아래와 같다.
|id|product_id|email|created_at|updated_at| |—|—|—|—|—|
create_subscribers.rb 파일을 열어보자.
1
2
3
4
5
6
7
8
9
10
class CreateSubscribers < ActiveRecord::Migration[8.0]
def change
create_table :subscribers do |t|
t.belongs_to :product, null: false, foreign_key: true
t.string :email
t.timestamps
end
end
end
자동으로 코드를 작성해준 모습을 볼 수 있다.
반대로 생각하면 t.belongs_to :product, null: false, foreign_key: true 부분이 products_id 를 외래키로 추가하는 코드인 것을 알 수 있다.
$ bin/rails db:migrate를 입력하며 반영하자.
여기까지만 하면 아쉽게도 의존관계가 완성되지 않는다.
subscribers 가 product_id 를 외래키로 사용하지만 products 는 모르는 상태(?) 라고 생각하면 쉽다.
그래서 products 에서도 subscribers 에 대한 의존관계를 설정해야한다.
1
2
3
4
5
6
7
8
class Product < ApplicationRecord
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
Product model 에 has_many :subscribers 를 추가하여 1:다 의존관계를 설정한다.
product 하나당 여러개의 email 이 존재할 수 있기 때문이다.
만약 1:1 관계로 설정하려면 has_one 으로 작성하면 된다.
dependent 에 :destroy 를 주면 데이터 삭제시 의존하는 데이터도 같이 삭제된다.
dependent 에 :nullify 를 주면 데이터 삭제시 의존하는 컬럼에 null 값이 들어간다.
액션 작성
구독 버튼을 클릭하면 table 에 email 을 저장하고 client 에게 구독되었다는 메세지를 전달해보자.
구독 버튼은 뒤에서 구현하고 controller 부터 구현하자.
form 의 구독버튼을 클릭하면 record 를 추가할 것이므로 create 액션이 적합하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SubscribersController < ApplicationController
allow_unauthenticated_access
before_action :set_product
def create
@product.subscribers.where(subscriber_params).first_or_create
redirect_to @product, notice: "You are now subscribed."
end
private
def set_product
@product = Product.find(params[:product_id])
end
def subscriber_params
params.expect(subscriber: [:email])
end
end
create 에 작성된 코드를 차근차근 알아보자
1
@product.subscribers.where(subscriber_params).first_or_create
앞서 Product model 에 subscribers 테이블과 연관관계를 맺었다.
그래서 @product.subscribers 를 사용해 subscribers 테이블의 records 를 가져올 수 있다.
다음 쿼리를 수행하게 된다.
1
2
3
select "subscribers".*
from "subscribers"
where "subscribers"."product_id" = @product.id
거기에 where 조건문을 사용하여 다음 쿼리문이 되었다.
1
2
3
4
select "subscribers".*
from "subscribers"
where "subscribers"."product_id" = @product.id
and "subscribers"."email" = 사용자가 입력한 값
.first_or_create 는 조회된 첫번째 record 를 리턴하거나 새로 만들라는 의미이다.
조건을 만족하는 record 가 없으므로 조건을 만족하는 record 를 새로 만들게된다.
1
redirect_to @product, notice: "You are now subscribed."
redrect_to @product 는 설명을 생략한다.
notice 는 데이터를 임시로 보관하는 flash 이다.
notice 라는 flash 에 “you are now subscribed” 문자열을 보관한 것이다.
flash 는 다음 액션 전까지 사용가능한 데이터 임시 보관장소이다.
1
2
3
4
5
6
7
8
9
<%# app/views/layouts/application.html.erb %>
<html>
<%# 생략 %>
<body>
<div class="notice"><%= flash[:notice] %></div>
<div class="alert"><%= flash[:alert] %></div>
<%# 생략 %>
</body>
</html>
flash 에 저장된 값을 꺼내려면 위 코드처럼 falsh[:flash명] 으로 작성하면 된다.
이미지로 보면 이해가 쉽다.
라우팅
subscribers#create 는 만들었지만 해당 액션으로 가는 라우팅을 설정하지 않았다. 설정해주자.
1
resources :subscribers, only: [ :create ]
이러면 path 는 POST /subscribers 가 될 것이다.
하지만 우리가 원하는건 특정 product 에 대해 subscribers 를 설정하는 것이다.
즉 POST /products/:id/subscribers 가 되야한다.
1
2
3
resources :products do
resources :subscribers, only: [ :create ]
end
nested(중첩) route 를 사용했다.
이제 POST /products/:id/subscribers 가 subscribers#create 를 호출하게 된다.
나는 여기서 의문이 있었다. 중첩되는것까진 이해가 되는데 products/subscribers 가 아니라 products/:id/subscribers 인 이유가 무엇일까?
그 해답은 nested route 가 다음내용을 전제하고 있기 때문이다.
nested route 는 리소스가 다른 리소스를 가질 수 있을 때 사용한다.
예를 들어 products 의 product 가 subscribers 를 가질 수 있을 때 사용하면된다.
즉, 각각의 product 가 subscribers 를 가지고 있을 때 nested route 를 사용하는 것이므로 반드시 :id 가 필요한 것이다.
form 작성
1
2
3
4
5
6
7
8
9
10
11
12
<%# app/views/products/_inventory.html.erb %>
<% if product.inventory_count.positive? %>
<p><%= product.inventory_count %> in stock</p>
<% else %>
<p>Out of stock</p>
<p>Email me when available.</p>
<%= form_with model: [product, Subscriber.new] do |form| %>
<%= form.email_field :email, placeholder: "you@example.com", required: true %>
<%= form.submit "Submit" %>
<% end %>
<% end %>
_inventory.html.erb 파일을 생성하고 구독 이메일을 입력하는 form 을 만든다.
1
2
3
4
5
<%# app/view/products/show.html.erb %>
생략
<%= render "inventory", product: @product %>
생략
<% end %>

