Ruby On Rails Basic A-Z

예상 리딩 타임: 30min ~ 1hour
아래의 다큐멘테이션은, 간단한 유저모델, 그리고 간단한 글을 남길 수 있도록 API 설계와 함께 구성 되어 있습니다.

Data Type

type : 몇가지 타입이 더 있지만, 일단 이정도만 알아도 크게 지장은 없습니다. 점차 알아가면 되죠!
major types: Integer, Float, Array, String, Hash, Boolean
integer
a = 5; b = 6 #number: integer, float > +,-,*,%,/
Ruby
float
a = 1.23451;
Ruby
Array
a = Array.new; #or a = [] a << 5 a << "6" a << [9] puts "#{a}" -> [5,"6", [9]]
Ruby
String
str = "Hi! This is good!"
Ruby
Hash
value2 = [5] hash = { key1: "value1", key2: value2, } key1 <-- symbol #old style hash = { :key1 => "value1", :key2 => value2 } #1. puts hash[:key1] #-> "value1" #2. value2 = 3 hash[:key2] ? -> [5] # -> 레일즈에서는 이미 지나갔습니다..🏃‍♀️
Ruby
Boolean - true/false
a = true if a puts "true 입니다" end b = false unless b puts "false 입니다" end
Ruby

Terms

Terms: method, module, class
method
일반적인 함수를, 루비에선 메소드라고 합니다.
module
methods, constants, class variables들의 조합.
object가 모듈로부터 형성 될 수는 없음.
class
클라스! 서로 상속도 받을 수 있는 것.
module에서 짠 코드를 class에 추가할 수도 있습니다 (include하여 - 자세한건 맨 뒤에 있습니다)
어려운 개념이 아니고 아래에서 자세히 설명합니다.
class : 말로 설명한다면.. 하나의 기초구조! 라고 할 수 있습니다. 이 기초구조에는 여러가지 method를 내포하게 할 수 있고, 또 여러가지 state를 넣을 수도 있습니다.
class SomeClass def initialize @some_data = 'initial value' end def insert_this_value(val) @some_data = val end def get_data return @some_data end end # a = SomeClass.new # a.get_data # -> initial value # a.insert_this_value('hello') # a.get_data # -> hello
Ruby
기본 적으로 def initialize 은 없어도 되지만, 만들어 주게 되는 경우, .new를 하는 순간 실행하여 줍니다.
Object orientated 형식의 코드 입니다.
나중에 더 깊게 다루도록 하겠습니다.
method - 자바스크립트에서 함수랑 같습니다. RoR에서는 method라는 말을 사용합니다.
ex) add, subtract
#starts with def #return #and end def add(a, b) return a + b end def subtract(a,b) return a - b end def how_to_use added = add(1,2) puts "result: #{added}" subtracted = subtract(2,1) puts "result: #{subtracted}" end
Ruby
module
module FirstModule module SubModule class SomeClass def initialize @some_var = "init" end def update_var(str) @some_var = str end def get_var return @some_var end end end end ##--- some = FirstModule::SubModule::SomeClass.new some.get_var #init some.update_var("new value") some.get_var #new value
Ruby
모듈의 사용용례는 문서의 맨뒤에 기재했습니다.
+ 폴더 정리에 용이 합니다.
auto load path
module Tutorial2 class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 6.1 config.autoload_paths << "#{Rails.root}/lib" # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") end end
Ruby

Gemfile

리액트에선 package.json과 같은 역할을 합니다. 이곳에 필요한 라이브러리를 불러오고, bundle 커맨드를 리눅스에서 입력하여 설치 합니다. bundle을 하기 위해선 bundler가 설치 되어 있어야 합니다. 설치 되어 있지 않은 경우 지시사항을 따라서 설치 하면 됩니다.
초반에는 버저닝이라는게 매우 까다롭게 다가옵니다. 처음에 루비와 레일즈에 초점을 두시고, 익숙해지실 즈음, 버저닝의 세계에 빠져보시길 권장합니다.
#ex) inside Gemfile gem 'faraday', '~> 0.11.0' gem 'devise'
Ruby
User model

Grape API

grape API - opinionated api & API ingeneral
how to install?
gem 'grape' # add this to Gemfile $ bundle #or bundle install
Ruby
route.rb
Rails.application.routes.draw do devise_for :users mount Grapes::API => "/" end
JavaScript
Foldering(*****)
app/api/grapes/api.rb
module Grapes class API < Grape::API #<-- after gem installed format :json prefix :api version "v1", :path #여기 숫자를 바꿔서 버져닝을 하고 route도 바꿉니다. mount Grapes::V1::MemoAPI mount Grapes::V1::UserSettingAPI mount Grapes::V1::UserAPI end end
Ruby
app/api/grapes/v1/
memo_api.rb
user_api.rb
user_setting_api.rb
m
create first memo_api.rb
module Grapes module V1 class MemoAPI < Grapes::API #grapes/api.rb에 있는 Grapes::API클래스를 가져옵니다. #여기에 작업을 합시다! end end end
Ruby

Http Protocol

http protocol
first get method
get do puts "get my daily memo" end get :list do puts "get list of my memo" end
Ruby
한 번 chrome에 localhost:3000/api/v1/memo 접근하기
post - mostly when creating
#when creating
Ruby
chrome addressbar를 통해서 접근하기
Put - mostly when updating
desc 'Update a status.' params do requires :id, type: String, desc: 'Status ID.' requires :status, type: String, desc: 'Your status.' end put ':id' do authenticate! current_user.statuses.find(params[:id]).update({ user: current_user, text: params[:status] }) end
Ruby
delete - when deleting
desc 'Delete a status.' params do requires :id, type: String, desc: 'Status ID.' end delete ':id' do authenticate! current_user.statuses.find(params[:id]).destroy end
Ruby

Active Record - with DB

terms: Table, database, migration, ActiveRecord
데이터 베이스에는 테이블이 한개 이상 존재 하게 되는데, 이번 샘플 basic tutorial에서는 User, Memo, Like 테이블 총 세가지가 있습니다.
레일즈에서는 데이터 베이스를 수정을 하면서 계속 진화 시켜 가는데, 이를 migration이라고 명명합니다.
레일즈에선 ActiveRecord라는 요상한 그리고 핵심인 개념이 있는데, 이건 모델이라고도 합니다. 이 모델은 여러가지 메소드, 그리고 비즈니스 로직을 담당하고, 실제 데이터 베이스를 업데이트 하는 것 사이를 담당합니다.
ORM(object relational mapping)은 서비스의 복잡한 비즈 로직을 데이터 베이스 테이블에 연결할 수 있게 해주는 기술입니다. 어렵게 설명했지만, 다른 말로 쉽게 설명하면, 기존에는 비즈니스 로직을 작성하고, sql로 일일이 database업데이트에 대해서 쿼리문을 작성해야 했다면, ORM을 통해서 간단한 Ruby On Rails 문법으로 접근 및 수정할 수 있게 됩니다. (한마디로 쉽다!)
테이블에 존재하는 레코드를 엔트리라고 하는데(entry), 유저 테이블의 여러개의 엔트리를 보면 대략 이렇습니다.
{id:1, email: sungpah@tutorial.com, nick_name: "good man"}
{id:2, email: sungpah2@tutorial.com, nick_name: "good man2"}
여기서 column은 id, email, nick_name입니다. 우리가 설계를 할때 먼저 고려해야 할 것이 바로 이 칼럼들이죠. 어떤 칼럼을 어떻게 만들것인가!
엑티브 레코드의 몇가지 메소드로는
User.first → 맨 위에 있는 엔트리가 리턴이 되고 User.last → 테이블에 맨 아래에 있는 엔트리 리턴 #만들 때 User.create!({ email: "new_email_entry@gmail.com", nick_name: "new nick name" }) #수정 할 때 user = User.find_by(email:'new_email_entry@gmail.com') user.email = 'updated_new_email@gmail.com' user.save #삭제할 때 - 자세한 건 더 나중에 깊게! user.delete #or user.destroy
Ruby
def create_record(content, user_id, question_id) NewMemo.create!({ user_id: user_id, content: content, question_id: question_id }) end def test_where(user_id, question_id) NewMemo.where(user_id: user_id, question_id: question_id) end def test_find_by(user_id) NewMemo.find_by(user_id: user_id) #return the first element. end def test_find(id) NewMemo.find(id) end def test_modify(id, content) new_memo = NewMemo.find_by(id: id) if new_memo new_memo.content = content new_memo.save else puts 'so sad' end end def modify(id, content) new_memo = NewMemo.find_by(id: id) unless new_memo return end new_memo.content = content new_memo.save end def delete(id) new_memo = NewMemo.find_by(id:id) unless new_memo return end new_memo.delete end def get_length NewMemo.count #NewMemo.all.length end def update_all_test #..# end
Ruby
빠르게 메모를 생각해보면 아래 정도의 테이블이 되면 됩니다.
Memo table
columns
content
question_id
user_id

DEVISE (USER)

참고로 다른 테이블고 달리 User의 경우, 레일즈에서 제공해주는 좋은 pre-made table이 있는데, devise gem 을 설치 하면 됩니다.
gem 'devise' #add this to Gemfile
Ruby
database migrate - 데이터 베이스 스키마를 업데이트를 해주는 것입니다. database migrate, migration 다 동의어로 생각하면 됩니다.
#table을 만들기 class CreateProducts < ActiveRecord::Migration def change create_table :products do |t| t.string :name t.text :description t.timestamps end end end #리버스.. class ChangeProductsPrice < ActiveRecord::Migration def change reversible do |dir| change_table :products do |t| dir.up { t.change :price, :string } dir.down { t.change :price, :integer } end end end end #애드 칼럼 & 애드 인덱스 class AddPartNumberToProducts < ActiveRecord::Migration[6.0] def change add_column :products, :part_number, :string add_index :products, :part_number remove_column :products, :part_number, :string change_column :products, :part_number, :text rename_column :users, :email, :email_address end end
Ruby

Routes

라우트는 http 문법과 url에 있는 값들을 레일즈가 이해할 수 있도록 매핑을 해주는 역할을 합니다. routes.rb파일을 열어보시면 다음과 같이 보입니다.
우리는 앞으로
ourdomain/v1/api/user
ourdomain/v1/api/memo
이렇게 두가지의 기본 패스로 접근하는 api를 만들겁니다. 가령
ourdomain/v1/api/memo/create 이렇게 말이죠.
route에서 engine을 사용하기 위해선 mount를 해야 합니다. mount하면 올려두다 정도로 해석하면 됩니다. 즉, 엔진을 우리의 레일즈에 올려두는 거라고 보면 됩니다.
mount Grapes::API => '/'
Ruby
앞에서 Grapes::API 요런거는 모듈::클래스 라고 소개 했습니다. 우리는 Grapes라는 모듈을 여기다가 선언해 봅시다.
그림을 보면, app > api > grapes 폴더에
v1 폴더
api.rb
v1.rb
이렇게 세가지가 있습니다.
그림의 오른쪽에 있는 부분이 바로 api.rb 파일입니다.
잘 보시면 prefix :api, 그리고 version v1, :path 의 의미는
"/api/v1" 를 의미 합니다.
우리가 앞에서 / 패스에 Grapes::API 를 마운트 했고, 이 파일에서는 또 여러개의 API (Grapes::V1::MemoAPI 외 3)을 마운트 했습니다.
여기서 memo_api를 샘플로 열어봤습니다. 이 api는 resource :memo do 로 시작을 합니다. 여기서 부터는
/memo
Ruby
로 path가 정의 됩니다.
resource :memo do get do end get :some_other_get do end post :some_post do end end
Ruby
라면, 이 패스는
/memo #and /memo/some_other_get #and /memo/some_post # - post
Ruby
로 접근이 됩니다.
이런 서브로 mount된 파일은 가장 상위단에서 바라 보게 된다면
/api/v1/memo /api/v1/memo/some_other_get /api/v1/memo/some_post
Ruby
과 매핑 됩니다.

User API

User create API
signup
signin
modify
validate (이것을 이해 하려면 client의 splash의 역할을 알아야 합니다)
이정도의 api정도면 기본은 갖춥니다. 물론, 패스워드 찾기, 리셋하기 등의 기능들은 아직 만들진 않았지만, 이는 추후에, Mailer의 구조가 잡히면 쉽게 추가 해보도록 합시다.
signup
간단하게 email, password, nick_name 세가지를 필수로 받는 api를 작성해봅시다.
params do requires :email, type: String requires :password, type: String end post "signup" do user = User.find_by(email:params[:email]) if user return { success: false, message: "User Email Not Usable" } end user = User.create!({ email: params[:email], password: params[:password] }) jwt_token = User.create_jwt_token(user.id) return { success: true, user: user, jwt_token: jwt_token } end
Ruby
갑자기 jwt_token이라는 것이 등장했습니다. jwt_token은 무엇이고 왜 사용해야 하나요? (꼭 jwt_token을 사용하라는 말은 아닙니다! 다른 토큰이 사용되도 됩니다. 물론 가장 일반적으로 많이 쓰이니, 대세를 따라가는 것으로...)
유저가 회원가입을 하고 나면, 웹이나 앱에서는 앞으로 최소한 몇분에서 몇시간은 유저가 다시 로그인을 할 필요가 없이 로그인 상태를 유지하게 해야 합니다. 여러가지 방법중에서 jwt_token의 경우, server에서 토큰을 연산하여 클라이언트에 보내주면, 클라이언트는 그 값을 세션이나 쿠키, 스토리지 등에 저장을 하고, 다음 request(from client)부터는 해당 token값을 항상 header에 실어서 요청을 합니다. 만약에 누구나 알 수 있는 유저의 번호 값 (ex. 1번) 을 가지고 call 을 요청하면 임의의 누군가에 의해 계정을 해킹 당할 수 있는 것이지요.
예를 들어 아래와 같은 api는 절대로 작성해서는 안됩니다.
params do requires :id, type: Integer end post "delete_user" do user = User.find(params[:id]) user.delete end
Ruby
계속 jwt_token에 대해서 이야기하고 있지만 정작 jwt_token이 무슨 값을 가지고 있는지를 설명을 안했네요.
우리는 유저의 정보 몇가지를 암호화 할거고 그게 우리의 jwt_token이 됩니다.
payload = { user_id: user.id, exp: (Time.now + 5.hours).to_i } #JWT -> gem 설치 하세용!( gem 'jwt' to Gemfile -> bundle) token = JWT.encode payload, SOME_SECRET_KEY, 'HS256' #puts "#{token}" #-> eyzxl1230xciw1nsdlkfskaxc....jclsadkclaqs 이런 형식의 스트링이 나옵니다. # 이 값을 클라이언트에 보내서, 클라이언트는 값을 가지고 있다가 다음 콜을 할 때 사용합니다. decoded_token = JWT.decode token, SOME_SECRET_KEY, true, { algorithm: 'HS256' } #puts "#{decoded_token}" #{ # user_id: user.id, # exp: (Time.now + 5.hours).to_i #} #이렇게 나오게 됩니다.
Ruby
크게 HS256방식과 RS256방식이 있는데, HS256은 하나의 시크릿키 값(보통 스트링)을 가지고 인크립션을 하고, 또 그 키 값을 가지고 드크립션을 해줍니다. 이키를 절대로 잊어버리면 안됩니다. (절대 누구와도 쉐어를 해서는 안됩니다) 레일즈 서버와 클라이언트를 둘다 컨트롤 할 수 있는 상황이라면 HS256을 간단히 사용할 수 있습니다. 혹은 시크릿키를 안전하게 보관할 수 있는 상황이라면 또한 문제 없이 사용할 수 있습니다.
만약 그런 경우가 아니라면 (키값이 해킹 당할 수 있는 상황이라면) RS256방식을 쓰면 좋습니다. RS256의 경우 public/private key 두 개를 사용하는 방식인데, token발행시 private키를 사용하여 발행하고, 클라이언트에서 public키를 가지고 validate을 해서 씁니다. 우리는 이 방식을 사용하진 않아도 됩니다. api 회사들이 이런 구조를 사용한다고 봐도 좋습니다.
sign in - email, password 필요
params do requires :email, type: String requires :password, type: String end post "signin" do user = User.find_by(params[:email]) user.password # > 이렇게 하면 패스워드가 사라집니다. ! 개발자가 함부로 못하죠! 읽을 수가 없어요. is_valid = user.valid_password?(params[:password]) # true false unless is_valid return { success: false, message: "not valid", } end return { success: true, message: "valid" } end
Ruby
유저에 관련한 api는 대략 이렇게 정리하고, 이제 기록을 하는 memo api를 살펴보겠습니다.

Memo Model

content (text)
user_id (integer : 어떤 유저가 남겼는지 알기 위해서 유저의 정보중 최소한의 것이 필요하죠. in general integer값인 user_id를 씁니다.)
이 유저아이디값으로 우리는 유저 테이블에 접근할 수 있습니다.
User.find_by(id: user_id) #!!
Ruby
created_at, updated_at 은 자동으로 형성 됩니다. (ORM nice)
question_id
질문이라는 모델이 있습니다. 하루에 한 질문씩 있다고 가정을 하고, 그 질문에 대해서 우리는 메모를 남기는 것이지요.
이렇게 question_id라고 하는 순간, question이라는 모델을 만들어 줘야 합니다.
그리고 migration을 해서 테이블을 형성 하면 됩니다. 앞으로는 모델을 만든다는 말은 마이그레이션까지 다 한다고 생각합시다.
Memo API
create : content, user_id, question_id가 필요합니다.
modify: content, user_id, question_id가 필요합니다.
delete: user_id, question_id
Like, dislike: user_id, memo_id
List: user_id
Today: user_id
보면 모든 api콜에서 필요한 것은 유저 정보 입니다. user_id가 필요한데, 앞에서 user_id를 api콜의 페이로드로는 쓰지 않아야 한다고 했지요. 그러면? 네, jwt_token을 디크립션해서 거기서 뽑아서 쓰면 됩니다.
이중에서, create을 한 번 만들어 보도록 합시다.
params do requires :content, type: String requires :question_id, type: Integer requires :jwt_token, type: String #-- 이렇게 해야 할까요? ㅎ-ㅎ end post :create do result = User.validate_jwt_token(params[:jwt_token]) unless result[:user_id] return { success: false, message: "not a valid user" } end user_id = result[:user_id] Memo.create!({ user_id: user_id, content: params[:content], question_id: params[:question_id] }) return { success: true } end
Ruby
몇가지 초점을 둘게 있습니다.
유저의 인크립션 된 정보 jwt_token을 저렇게 바디에 실어서 보낼 것인가? (바디 = http request의 바디)
content, question_id가 이상한것을 입력 하는 건 검수는 안하나?
하나의 질문에 대해 여러개의 메모가 남겨지면 괜찮나?
이 질문에 대해 정확히 알고 나면, 나머지 api를 설계하는 데에도 큰 도움이 됩니다. 이 시점에서 잠깐 이 부분을 다뤄보도록 하죠.
1.
jwt_token에 대하여..
일단 bearer토큰은 저렇게 보내지 않는게 원칙입니다. ISOC 위원회가 있는데, 여기서 발표한 원칙은 jwt_token를 사용할 때에는, 클라이언트가 절대로 이 값을 사용하지 않는다라는 원칙 하에, request의 header값에 실어서 보낸다라는 기준을 만들었어요. 이 위원회가 뭐하는 것까지는 제가 다 설명할 수는 없고, 전세계에서 룰처럼 사용하는 원칙이라고 생각을 하시면 됩니다.
한마디로, request의 헤더에 넣어서 보내고, 그 request값을 서버사이드에서 읽어서 처리 합니다. 코드로 한 번 볼까요? 처리하는 것만 레일즈에서 담당하므로, 그 코드를 간략하게 작성해보았습니다.
jwt_token = request.headers["Authorization"].split(" ").last
Ruby
처음에 코드를 접할 때에, "request라는 변수는 대체 또 뭐야" 라는 생각을 한 적이 있었어요. 이 request라는 변수는 레일즈에서 제공하는 class입니다. grape api 전역에서 디폴트로 쓸 수 있습니다. (일반 모델에서는 아니지만요!) 시간이 되시면 request.inspect로 찍어서 한 번 쭉 찾아보시길 권장합니다. 뭐든 찾아보면 좋아요! https://api.rubyonrails.org/v6.1.3/classes/ActionDispatch/Request.html
따라서, 우리의 api는 아래 처럼 바뀌어야 합니다.
params do requires :content, type: String requires :question_id, type: Integer #jwt_token을 페이로드에 담지 않습니다. end post :create do unless request.headers["Authorization"] raise "!!! - 여기는 뭐 개인 취향 입니다. 이 나쁜 유저에게 어떻게 응징할까요??!?" end jwt_token = request.headers["Authorization"].split(" ").last #아래 코드는 동일합니다. result = User.validate_jwt_token(params[:jwt_token]) unless result[:user_id] return { success: false, message: "not a valid user" } end #...이하 동일.. end
Ruby
2. 이상한 것 검수 (content, question_id)
이상하다의 정의가 필요합니다. 어떻게 할까요? 일단 content는 한글자 정도는 쳐줬으면 좋겠어요 ..ㅎㅎ 실수로 보낼 수도 있으니까요. (물론 그 실수는 클라이언트에서 처리를 해줘야 하는게 저의 원칙입니다. 이유는 클라이언트에서 처리를 하면 굳이 리퀘스트를 해서 서버에서 처리를 안해줘도 되니까요. 리퀘스트 하나당 전부 돈입니다. 돈돈돈. 이런거로 flex하지 말자구요! (그리고 습관처럼 해야 efficient한 코드가 됩니다)
if params[:content].split(" ").length <= 1 return { success: false, message: "쪼매 더 적으소" } end
Ruby
question_id값이 이상한 값이 들어올 수 있습니다. 해킹이 들어올 가능성은 적지만 (jwt_token에서 걸러지니까요) 그래도, 가끔 개발하다가 클라이언트에서 실수를 하는 경우도 있답니다. 그땐 레코드가 이상한게 만들어지면 안되겠죠! question_id로 실제 퀘스쳔이 존재하는지 알아봐야 합니다.
question = Question.find_by(id: params[:question_id]) unless question return { success: false, message: "누구냐 넌" } end
Ruby
이정도면 어느정도 깔끔하게 예외처리가 잘 된것 같네요. 물론 처음에 코딩을 접할 때는 이것까지 해야 할 필요성을 못느낍니다. 왜냐하면, 나 혼자서 로컬 머신에서 하기 때문이죠. 토이프로젝트를 하시는 분이라면 스킵하셔도 될 수도 있을 수도 있지만..이라고 이야기 하고 싶지 않습니다! 이정도는 해주셔야 해요.
3. 하나의 질문에 대해 여러개의 메모가 남겨지면 괜찮나? (안괜찮으니까 이걸 보고 있죠 )
더블서밋을 할 수도 있고, 클라이언트에서 처리가 잘못 될 수도 있고.. 이유는 많으나, 초보자일 수록, 프로덕션에서 많은 실수를 하는 부분입니다. 뉴비 일때부터 차근차근!
저는 약 두가지 방법을 사용하는데, 두번째 방법은, 아직 알려드릴 타이밍은 아닌 듯 하여 (여기서 또 깊게 한 번 더 들어가야 해요 ㅎㅎ) 첫번째 방법을 기재하려 합니다. (두 방법 다 써야 합니다 참고로). 두번째 방법은 데이터 베이스 자체에 유니크 인덱스를 달아서 디비에 데이터가 입력이 될때에 prevention하는 방법 입니다.
그건 바로 active record model의 도움을 받는 것입니다. 이말은 우리의 서버에서 처리를 하겠다는 말이에요. (서버 짜고 있는데 써버에서 처리를 하겠다는 말이 우숩죠. 그냥 그렇다는 ㅋㅋ)
just open your memo.rb (올바르게 마이그레이션 했으면 이 모델이 만들어질겁니다)
class Memo < ActiveRecord::Base #비어있죠. end #이게 아래로 변합니다. class Memo < ActiveRecord::Base validates :user_id, uniqueness: { scope: :question_id, message: "user_id와 question_id에 대해서 메모는 하나여야 하지요" } end
Ruby
수정된 메모 클레스를 보시면 (메모 클래스 = 메모 모델), memo 엔트리에 대해 user_id와 question_id가 딱 하나의 조합만 validate하겠다라고 선언하고 있습니다.
응용해서, 더 많은 조합이 하나만 되게 해주세요! 라는 것은 이렇게 할 수 있습니다.
valides :user_id, uniqeness: {scope: [:question_id, :other_column_1, :other_column_2]}
Ruby
매우 쉽죠?
해당 방법을 사용하여, 어플리케이션 단에서 데이터가 잘 못되는 것을 막을 수 있습니다.
이렇게 총 세가지 문제에 대해서 명확하게 답을 해보았네요.
Memo List API
each and map
이정도 했으니, 이제 또 다른 토픽으로 넘어가 보도록 할까요? 이제 iterate에 대해서 언급을 할 때가 드디어 왔습니다. 이제 만들어볼 api는 바로 memo의 리스트인데, 하나의 질문에 대해서 작성되어 있는 모든 메모를 다 가지고 오고 싶은 상황 입니다. 나의 메모 뿐만 아니라, 다른 모든 사람의 메모를 보고 싶은거죠 (물론 공개를 했다는 전제로!)
알아야 할 것.
1.
여러개의 엔트리를 불러 오는 방법 (via active record)
2.
불러온 레코드를 잘 정리하는 방법
입니다.
#where 로 문을 열고, 찾아야 할 칼럼 이름으로 서치를 합니다. #이를 만족한 모든 레코드를 데이터베이스에서 모두 가져옵니다. memo_entries = Memo.where(question_id: params[:question_id])
Ruby
이렇게 불러오면, 우리의 memo_entries는 0개 이상의 값이 들어가있는 array비슷한 오브젝트가 됩니다. 이 클레스를 정확히 보려면
puts memo_entries.class # -> => Memo::ActiveRecord_Relation
Ruby
앞서 active record는 데이터 베이스와 우리의 어플리케이션의 인터페이스 역할을 한다고 했죠! 이 클래스에는 이것저것 성질이 있는데, 자세한 것은 나중에 공식문서를 보고 보시면 됩니다 (ㅎㅎ)
제가 주로 쓰는 성질은 array 같다는 것이에요. 예를 들어 이런 식으로 볼 수 있습니다.
memo_entries.each do |memo_entry| puts memo_entry.id end
Ruby
앞으로 여러분이 가장 "많이" 쓸 구문인 .each do |entry| 입니다. | | 안에 들어가는 값은 매개 변수 이고, 어떠한 글자여도 상관이 없습니다. 임시 변수같은 거에요.
some_kind_of_array.each do 는 some_kind_of_array 를 맨 위의 값부터 맨 아래 값까지 한 바퀴 돌리겠다는 이야기 입니다. (한바퀴 돌린다는 말이 너무 직설적인가 싶지만, 맨 위부터 하나씩 차례대로 다 한 번 씩 방문하겠다! 라는 말 입니다.) 위의 경우는 조건에 맞는 memo_entries들의 memo_entries[0] memo_entres[1] .... memo_entres[memo_entries.length - 1] 를 방문한다는 이야기 입니다..
첫번째 알아야 할 것은 이정도 인것 같네요.
두번째는 바로 이를 정리하는 것 입니다. 우리는 memo를 보여줄때 우리는 그 메모를 작성한 사람의 nick_name까지는 보여줄 예정입니다. 하지만 memo entry하나에는 user_id만 있는데, 어떻게 nick_name을 가져올 수 있을까요? 여러가지 방법이 있지만, 간단한 방법을 소개 하겠습니다.
memo_entries = Memo.where(question_id: params[:question_id]) user_ids = memo_entries.map(&:user_id) users = User.where(id: user_ids) new_array = Array.new memo_entries.each do |memo_entry| user = users.find{|user| user.id == memo_entry.user_id} memo_entry.as_json! memo_entry[:user] = user new_array << memo_entry end
Ruby
1.
먼저 memo_entries에서 유저아이디값만 추출을 해서 user_ids라고 합니다.
이때 map 메소드를 사용했습니다. map이 처음엔 익숙치 않을 텐데, 자세히 아래에서 설명하겠습니다.
2.
User테이블에서 user_ids를 가지고 users를 가져옵니다
3.
new_array 빈통을 하나 만듭니다.
4.
new_entries를 가지고 iterate을 합니다.
a.
이때 하나의 memo_entry당 user_id에 맞는 user를 users로 부터 찾습니다.
b.
find 구문을 알아야 합니다.
c.
memo_entry에 집어 넣기 위해서 json형태로 바꿉니다.
memo_entry.user = user 라고 하고 싶지만.. 그건 안되더라구요 ㅎ!
d.
memo_entry[:user] = user
e.
마지막으로 new_array에 마지막 완성된 값을 넣습니다.
어찌 저찌 해서 하긴 했는데, 뭔가 좀 복잡하죠? 하나씩 좀 더 깔끔하게 작업해볼까요?
준비물은
1.
map에 대한 이해 정도면 되겠군요.
Map method는 무언가를 다른 무언가로 매핑해준다라는 말입니다. 그럼 매핑은 뭔가요? ㅋㅋ 맵 + ing인가요? ㅎㅎ
맵이 하는 것은 왼쪽에있는 모든 것에 동일한 규칙을 적용해서 오른쪽으로 새롭게 만든다라고 생각을 하시면 됩니다. 예를 들어, [1,2,3,4] 라는 어레이가 있을 때, 우리가 적용하고 싶은 규칙은 "2를 곱해" 일 경우, 결과값은 [2,4,6,8] 이 되는 것 입니다. 쉽죠?
여기서 룰은 하나의 메소드라고 할 수도 있습니다. 위에서 "2를 곱해" 라는 것은 하나의 method겠죠 (루비온레일즈에선)
앞서
user_ids = memo_entries.map(&:user_id)
Ruby
이걸 본다면, memo_entries에 모든 엔트리에 하나씩 &:user_id 라는 것을 적용해서 출력하라 이말입니다. 물론 &:user_id는 처음 보는 거고, 그건 공부를 해야 알수있는 거긴 하지만, 어찌 됐든 &:user_id는 하나의 메소드라고 생각하시면 됩니다. 참고로 &:user_id 라고 하면 왼쪽에 있는 것 중에서 user_id를 하나씩 뽑아 와라라는 말 입니다. 따라서 최종 결과인 user_ids는 대략 이런 모습이 되겠죠.
memo_entries = [ { id: 5, user_id: 6, content: "저녁으로 라면을 먹었습니다" }, { id: 6, user_id: 7, content: "소화하려고 한강한바퀴 돌고 왔어요" } ] #라고 할 때, &:user_id를 하면 각각의 엔트리에서 user_id만 추출하고 결과 값은 [6,7] #이 됩니다. 매우 쉽죠?
Ruby
다른 연습으로는 user_id만 두배 곱해서 memo_entries를 복원해봐! 입니다. 그럼 아래 처럼 해볼 수 있습니다.
result = memo_entries.map {|memo_entry| memo_entry = memo_entry.as_json memo_entry["user_id"] = memo_entry["user_id"] * 2 memo_entry #마지막에 꼭 수정된 memo_entry를 써줘야 합니다. 안그러면 그 직전 값이 나오는데 그건 두배가된 user_id값입니다. }
Ruby
왜 여기서 as_json을 했냐면 (아직 json이 뭔지 말하지도 않았지만!), memo_entry는 ActiveRecord class이기 때문에, 안에 있는 칼럼 값을 바꿀수가 없습니다. 그래서 json스타일의 hash로 바꿔주고 거기에 있는 user_id 심볼에 접근해서 곱하기 2를 하여 대입해주는 거죠.
마지막에 한 번더 memo_entry라고 쓴거는 중요한데, 최종적으로 수정된 memo_entry 변수를 매핑 할때 그걸 쓰겠다는 말 입니다. 최종 결과는
result = [ { id: 5, user_id: 12, content: "저녁으로 라면을 먹었습니다" }, { id: 6, user_id: 14, content: "소화하려고 한강한바퀴 돌고 왔어요" } ]
Ruby
가 됩니다.
Syntax를 다시한 번 보면
something.map {|매개변수| logic #last line is the return of {} converted_object }
Ruby
처럼 생각하면 됩니다.
자 이제 최종적으로 깔끔하게 한 번 작성해볼까요?
memo_entries = Memo.where(question_id: params[:question_id]) user_ids = memo_entries.map(&:user_id) users = User.where(id: user_ids) new_array = memo_entries.map {|memo_entry| user = users.find{|user| user.id == memo_entry.user_id} memo_entry = memo_entry.as_json memo_entry["user"] = user memo_entry }
Ruby
입니다. 간단하죠?
물론 데이터베이스에 접근하는 방법에서 더 간단히 하는 방법들이 있지만, 그 부분은 일부로 빼도록 하겠습니다. (has_many) 저마다 생각들은 다르시겠지만, 초반에는 특정 엑티브 레코드 API를 안쓰는게 더 효율적일때가 있기에 추가 설명은 나중에 기회가 되면 하겠습니다.
참고로 많은 실수유형을 하나 보여드리면
memo_entries = Memo.where(question_id: params[:question_id]) new_array = memo_entries.map {|memo_entry| user = User.find_by(id: memo_entry.user_id) memo_entry = memo_entry.as_json memo_entry["user"] = user memo_entry }
Ruby
얼핏보면 훨씬 간결해보이나, 치명적인 단점이 있습니다. 바로
user = User.find_by(id: memo_entry.user_id)
Ruby
입니다. 이유는, 저 커맨드 한줄에 문제가 있기 보단, map함수가 iterate할 때마다 database에 신호를 보낸 다는 것 입니다. 루비 속도보다, 외부 디비를 접속하는 시간이 일반적으로 길기 때문에, memo_entries의 길이가 길수록 비례해서 매우 느려집니다. 따라서
코드가 길어진다 해도, 디비 접근 횟수를 줄이는 것이 먼저 선행되어야 합니다.
레일즈에서 기본적으로 알아야할 내용은 다 다뤘습니다. 물론 더 상세히 더 정교히 만들기 위해선 아직 할일이 많이 남았지만, 이정도만 알아도, 사업 하나를 시작하는데는 문제가 없습니다.(라고 하기엔 아직 프로덕션이 남았지만!)
include & require
추가로 저번 강의때에 나왔던 include에 대해서, 다음 예제를 통해서 빠르게 보겠습니다.
module Logging def log(level, message) File.open("log.txt", "a") do |f| f.write "#{level}: #{message}" end end end class Service include Logging #include를 하나만 할 수있는 것은 아닙니다. def do_something begin # do something rescue StandardError => e log :error, e.message end end end
Ruby
Logging이라는 모듈을 인클루드 하면 클래스 내에서 사용가능합니다. 쉽게 설명하면 include를 통해서 클래스에 모듈 코드를 가져올 수 있습니다. 코드레벨 내에서의 익스텐션이라고 보면 됩니다.
또한 require라는 syntax도 있습니다. 루비에서 require는 다른 파일을 run하는 기능으로 씁니다. load와 비슷합니다.
Oddly enough, Ruby's require is analogous to C's include, while Ruby's include is almost nothing like C's include.
require in ruby ~= include in C
루비에서는 import 라는 말은 쓰지 않네요! ㅎㅎ
저라면 아마도 커스텀으로 module을 하나 만들어서 logging 하는 메소드 내에, 슬랙 메시지로 보내기 같은 것을 해볼 수 있을 것 같네요. 그리고 Logging Module을 앞으로 만들 모든 클래스에 항상 include를 하면 될것이구요!
module Logging def log(level, message) Slack.send_message("#{level}: #{message}") end end class Service include Logging def do_something begin # do something rescue StandardError => e log :error, e.message end end end
Ruby
반복되는 User관련 정보를 이제 깔끔하게 current_user로 만들어 보자
helper를 사용해봅시다.

route_param 예시

module Grapes module V1 class NewMemoAPI < Grapes::API helpers AuthHelpers #auth helpers resource :new_memo do #create params do requires :question_id, type: Integer requires :content, type: String #text end post :create do authenticate! NewMemo.create!({ user_id: current_user.id, question_id: params[:question_id], content: params[:content] }) return { success: true, message: "created" } end get :list do new_memos = NewMemo.all user_ids = new_memos.map(&:user_id) users = User.where(id: user_ids) return new_memos.map{|new_memo| user = users.find{|user| user.id == new_memo.user_id} nick_name = "n/a" if user nick_name = user.nick_name end { new_memo: new_memo, nick_name: nick_name, gogo: 'home', } } return array end route_param :id, type: Integer do #/api/v1/new_memo/#{id}/modify params do requires :content, type: String #content end put :modify do authenticate! memo = NewMemo.find_by(id: params[:id]) if memo unless current_user.id == memo.user_id error!('401 unauthorized', 401) end memo.content = content memo.save else error!('404 not found', 404) end end #/api/v1/new_memo/#{id}/delete delete :delete do memo = NewMemo.find_by(id: params[:id]) if memo unless current_user.id == memo.user_id raise "who are you?" end memo.delete else end end #/api/v1/new_memo/#{id}/like post :like do end #/api/v1/new_memo/#{id}/unlike post :unlike do end end end end end end
Ruby
faraday 예시
jwt_token = User.create_jwt_token(1) payload = { content: 'yaho6' } url = "http://localhost:3000/api/v1/new_memo/1/modify" conn = Faraday.new(:url => url) do |faraday| faraday.request :url_encoded # form-encode POST params faraday.response :logger # log requests to STDOUT faraday.adapter Faraday.default_adapter # make requests with Net::HTTP end #conn.post , conn.get response = conn.put do |req| req.headers['Content-Type'] = 'application/json' req.headers['Authorization'] = "Bearer " + jwt_token req.headers['Accept'] = "application/json" req.body = payload.to_json end parsed_json = ActiveSupport::JSON.decode(response.body)
Ruby
추가적인 토픽으로는
"동시접속"을 할 때 유의점
푸시 메시지는 어떻게 보내지?
서버에서 사용자에게 메일은 어떻게 보내지?
몇몇 액션들은 느린데, 백그라운드 job같은거는 없나?
특정 타이밍에 특정한 메소드를 실행하고 싶은데 방법은 뭘까?
텍스트 파일이나 엑셀 파일 같은걸 읽어서 처리 하고 싶은데 방법은?
구글, 페북 소셜 로그인 같은거는 어떻게 하지?
입니다. 기회가 될때 또 한 번 적어보겠습니다