Ver3.0 ๐ Ruby on Rails์ GraphQL๋ก ๋ง๋๋ ์ฐจ์ธ๋ API: ๊ฐ๋ฐ์ ๋ฏธ๋๋ฅผ ์ฌ๋ ์๋ฒฝ ๊ฐ์ด๋

๐ Ruby on Rails์ GraphQL๋ก ๋ง๋๋ ์ฐจ์ธ๋ API: ๊ฐ๋ฐ์ ๋ฏธ๋๋ฅผ ์ฌ๋ ์๋ฒฝ ๊ฐ์ด๋
REST API๋ ์ด์ ์๋ ์ด์ผ๊ธฐ? ํ๋์ ์ธ API ๊ฐ๋ฐ์ ์๋ก์ด ํจ๋ฌ๋ค์์ ๋ง๋๋ณด์ธ์!
๐ฏ ์ ์ง๊ธ GraphQL์ธ๊ฐ?
์๋ ํ์ธ์, ๊ฐ๋ฐ์ ์ฌ๋ฌ๋ถ! ๐ ์ค๋์ ์ ๋ง ํฅ๋ฏธ์ง์งํ ์ฃผ์ ๋ฅผ ๊ฐ์ง๊ณ ์์ด์. ๋ฐ๋ก Ruby on Rails์ GraphQL์ ํ์์ ์ธ ์กฐํฉ์ ๋ํ ์ด์ผ๊ธฐ์ ๋๋ค.
์ฌ๋ฌ๋ถ, ํน์ ์ด๋ฐ ๊ฒฝํ ์์ผ์ ๊ฐ์? REST API๋ฅผ ๋ง๋ค๋ค๊ฐ "์, ์ด ๋ฐ์ดํฐ๋ง ํ์ํ๋ฐ ์ ์ด๋ ๊ฒ ๋ง์ ์ ๋ณด๋ฅผ ๋ฐ์์ผ ํ์ง?" ๐ค ํ๊ณ ๊ณ ๋ฏผํ๋ ์๊ฐ ๋ง์ด์์. ๋๋ ํ๋์ ํ๋ฉด์ ๊ตฌ์ฑํ๊ธฐ ์ํด ์ฌ๋ฌ ๊ฐ์ API ์๋ํฌ์ธํธ๋ฅผ ํธ์ถํด์ผ ํ๋ ๊ทธ ๋ต๋ตํจ...
๋ฐ๋ก ์ด๋ฐ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ๊ธฐ ์ํด ๋ฑ์ฅํ ๊ฒ์ด GraphQL์ ๋๋ค! 2015๋ ํ์ด์ค๋ถ(ํ Meta)์ด ์คํ์์ค๋ก ๊ณต๊ฐํ ์ดํ, Netflix, Shopify, GitHub, Airbnb ๋ฑ ๊ธ๋ก๋ฒ ํ ํฌ ๊ธฐ์ ๋ค์ด ์๋คํฌ์ด ๋์ ํ๊ณ ์๋ ์ฐจ์ธ๋ API ์ฟผ๋ฆฌ ์ธ์ด์ฃ .
๐ REST API vs GraphQL: ๋ฌด์์ด ๋ค๋ฅผ๊น?
๋จผ์ ๊ธฐ์กด์ REST API์ GraphQL์ด ์ด๋ป๊ฒ ๋ค๋ฅธ์ง ๋ช ํํ๊ฒ ์ดํดํด๋ณผ๊น์? ์ค์ ์์๋ฅผ ํตํด ์ดํด๋ณด๋ฉด ์ฐจ์ด๊ฐ ํ ๋๊ปด์ง ๊ฑฐ์์! ๐จ
๐ REST API์ ์ ํต์ ์ธ ๋ฐฉ์
์ฌ์ฉ์ ํ๋กํ ํ์ด์ง๋ฅผ ๋ง๋ ๋ค๊ณ ๊ฐ์ ํด๋ณผ๊ฒ์. ์ฌ์ฉ์ ์ ๋ณด, ์ต๊ทผ ๊ฒ์๊ธ 3๊ฐ, ํ๋ก์ ์๋ฅผ ๋ณด์ฌ์ค์ผ ํ๋ค๋ฉด:
GET /api/users/123
GET /api/users/123/posts?limit=3
GET /api/users/123/followers/count
๋ณด์ด์๋์? 3๋ฒ์ HTTP ์์ฒญ์ด ํ์ํฉ๋๋ค. ๊ฒ๋ค๊ฐ ์ฒซ ๋ฒ์งธ ์์ฒญ์์๋ ์ฌ์ฉ์์ ๋ชจ๋ ์ ๋ณด(์ด๋ฉ์ผ, ์ฃผ์, ์ ํ๋ฒํธ ๋ฑ)๋ฅผ ๋ฐ์ง๋ง, ์ค์ ๋ก๋ ์ด๋ฆ๊ณผ ํ๋กํ ์ฌ์ง๋ง ํ์ํ ์๋ ์์ด์. ์ด๋ฅผ Over-fetching(๊ณผ๋ค ์กฐํ)์ด๋ผ๊ณ ๋ถ๋ฆ ๋๋ค.
โก GraphQL์ ํ์ ์ ์ธ ์ ๊ทผ
๊ฐ์ ์์ ์ GraphQL๋ก ์ฒ๋ฆฌํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น์?
query {
user(id: 123) {
name
profileImage
posts(limit: 3) {
title
createdAt
}
followersCount
}
}
์์ฐ! ๐ ๋จ ํ ๋ฒ์ ์์ฒญ์ผ๋ก ํ์ํ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์ ํํ๊ฒ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค. ํ์ํ ํ๋๋ง ๋ช ์ํ๊ธฐ ๋๋ฌธ์ ๋ถํ์ํ ๋ฐ์ดํฐ ์ ์ก๋ ์๊ณ ์!
| ๋น๊ต ํญ๋ชฉ | REST API | GraphQL |
|---|---|---|
| ์๋ํฌ์ธํธ | ๋ฆฌ์์ค๋ณ ์ฌ๋ฌ ๊ฐ | ๋จ์ผ ์๋ํฌ์ธํธ (/graphql) |
| ๋ฐ์ดํฐ ์กฐํ | ์๋ฒ๊ฐ ์ ์ํ ๊ตฌ์กฐ | ํด๋ผ์ด์ธํธ๊ฐ ํ์ํ ๊ฒ๋ง ์์ฒญ |
| Over-fetching | ์์ฃผ ๋ฐ์ | ๋ฐ์ํ์ง ์์ |
| Under-fetching | ์ถ๊ฐ ์์ฒญ ํ์ | ํ ๋ฒ์ ํด๊ฒฐ |
| ๋ฒ์ ๊ด๋ฆฌ | /v1, /v2 ๋ฑ ํ์ | ์คํค๋ง ์งํ๋ก ํด๊ฒฐ |
| ๋ฌธ์ํ | ๋ณ๋ ์์ฑ ํ์ | ์๋ ์์ฑ (Introspection) |
๐ Ruby on Rails์์ GraphQL ์์ํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก Rails ํ๋ก์ ํธ์ GraphQL์ ๋์ ํ๋ ๋ฐฉ๋ฒ์ ์์๋ณผ๊น์? ์๊ฐ๋ณด๋ค ํจ์ฌ ๊ฐ๋จํด์! ๐
๐ ๏ธ 1๋จ๊ณ: Gem ์ค์น
Rails์์ GraphQL์ ์ฌ์ฉํ๋ ค๋ฉด graphql gem์ด ํ์ํฉ๋๋ค. Gemfile์ ์ถ๊ฐํด์ฃผ์ธ์:
gem 'graphql', '~> 2.0'
gem 'graphiql-rails', group: :development
๊ทธ๋ฆฌ๊ณ ํฐ๋ฏธ๋์์ ์ค์น๋ฅผ ์คํํฉ๋๋ค:
bundle install
rails generate graphql:install
graphiql-rails๋ ๊ฐ๋ฐ ํ๊ฒฝ์์ GraphQL ์ฟผ๋ฆฌ๋ฅผ ํ
์คํธํ ์ ์๋ ๋ฉ์ง ์น ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํด์. ๋ธ๋ผ์ฐ์ ์์ /graphiql ๊ฒฝ๋ก๋ก ์ ์ํ๋ฉด ์ฌ์ฉํ ์ ์์ต๋๋ค!
๐ 2๋จ๊ณ: ์คํค๋ง ์ ์ํ๊ธฐ
GraphQL์ ํต์ฌ์ ์คํค๋ง(Schema)์ ๋๋ค. ์คํค๋ง๋ API์์ ์ ๊ณตํ๋ ๋ฐ์ดํฐ์ ํ์ ๊ณผ ๊ด๊ณ๋ฅผ ์ ์ํ๋ ์ฒญ์ฌ์ง์ด์์.
์๋ฅผ ๋ค์ด, ๋ธ๋ก๊ทธ ์ ํ๋ฆฌ์ผ์ด์ ์ User ํ์ ์ ์ ์ํด๋ณผ๊น์?
# app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
description "๋ธ๋ก๊ทธ ์ฌ์ฉ์"
field :id, ID, null: false
field :name, String, null: false
field :email, String, null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :posts, [Types::PostType], null: true
field :followers_count, Integer, null: false
def followers_count
object.followers.count
end
end
end
๋ณด์๋ค์ํผ ๊ฐ ํ๋์ ํ์
๊ณผ null ํ์ฉ ์ฌ๋ถ๋ฅผ ๋ช
ํํ๊ฒ ์ ์ํ ์ ์์ด์. followers_count์ฒ๋ผ ์ปค์คํ
resolver ๋ฉ์๋๋ ์ฝ๊ฒ ์ถ๊ฐํ ์ ์์ฃ ! ๐ฏ
๐ 3๋จ๊ณ: Query ์์ฑํ๊ธฐ
์ด์ ํด๋ผ์ด์ธํธ๊ฐ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ ์ ์๋๋ก Query๋ฅผ ์ ์ํด๋ด ์๋ค:
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
# ๋จ์ผ ์ฌ์ฉ์ ์กฐํ
field :user, UserType, null: true do
argument :id, ID, required: true
end
def user(id:)
User.find_by(id: id)
end
# ๋ชจ๋ ์ฌ์ฉ์ ์กฐํ
field :users, [UserType], null: false do
argument :limit, Integer, required: false, default_value: 10
end
def users(limit:)
User.limit(limit)
end
# ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์
field :current_user, UserType, null: true
def current_user
context[:current_user]
end
end
end
argument๋ฅผ ํตํด ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ์ ์ ์๊ณ , default_value๋ก ๊ธฐ๋ณธ๊ฐ๋ ์ค์ ํ ์ ์์ด์. Rails์ ActiveRecord์ ์๋ฒฝํ๊ฒ ํตํฉ๋๋ ๊ฒ๋ ํฐ ์ฅ์ ์ด์ฃ ! ๐ช
โ๏ธ 4๋จ๊ณ: Mutation ๊ตฌํํ๊ธฐ
๋ฐ์ดํฐ๋ฅผ ์์ฑ, ์์ , ์ญ์ ํ๋ ์์ ์ Mutation์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค. ์ฌ์ฉ์ ์์ฑ mutation์ ๋ง๋ค์ด๋ณผ๊น์?
# app/graphql/mutations/create_user.rb
module Mutations
class CreateUser < BaseMutation
description "์๋ก์ด ์ฌ์ฉ์ ์์ฑ"
# ์
๋ ฅ ํ๋
argument :name, String, required: true
argument :email, String, required: true
argument :password, String, required: true
# ๋ฐํ ํ๋
field :user, Types::UserType, null: true
field :errors, [String], null: false
def resolve(name:, email:, password:)
user = User.new(
name: name,
email: email,
password: password
)
if user.save
{
user: user,
errors: []
}
else
{
user: nil,
errors: user.errors.full_messages
}
end
end
end
end
Mutation์ ์ฑ๊ณต/์คํจ ์ํ์ ์๋ฌ ๋ฉ์์ง๋ฅผ ํจ๊ป ๋ฐํํ ์ ์์ด์ ํด๋ผ์ด์ธํธ์์ ์๋ฌ ํธ๋ค๋ง์ด ํจ์ฌ ์์ํด์ง๋๋ค! ๐
๐ ์ค์ ํ์ฉ: ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค
๊ธฐ๋ณธ๊ธฐ๋ฅผ ์ตํ๋ค๋ฉด, ์ด์ GraphQL์ ์ง์ง ํ์๋ฅผ ๋ฐํํ ์ฐจ๋ก์ ๋๋ค! ์ค๋ฌด์์ ์์ฃผ ์ฌ์ฉ๋๋ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ์ดํด๋ณผ๊ฒ์. ๐ฅ
๐ฏ N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ํด๊ฒฐํ๊ธฐ
GraphQL์ ์ฌ์ฉํ๋ค ๋ณด๋ฉด ๊ฐ์ฅ ํํ๊ฒ ๋ง์ฃผ์น๋ ๋ฌธ์ ๊ฐ ๋ฐ๋ก N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ์ ๋๋ค. ์๋ฅผ ๋ค์ด, 10๋ช ์ ์ฌ์ฉ์์ ๊ฐ์์ ๊ฒ์๊ธ์ ์กฐํํ๋ฉด:
query {
users {
name
posts {
title
}
}
}
์ด ์ฟผ๋ฆฌ๋ 1๋ฒ์ ์ฌ์ฉ์ ์กฐํ + 10๋ฒ์ ๊ฒ์๊ธ ์กฐํ = ์ด 11๋ฒ์ DB ์ฟผ๋ฆฌ๋ฅผ ๋ฐ์์ํฌ ์ ์์ด์. ๐ฑ
ํด๊ฒฐ์ฑ ์ graphql-batch gem์ ์ฌ์ฉํ๋ ๊ฒ์ ๋๋ค:
# Gemfile
gem 'graphql-batch'
# app/graphql/loaders/record_loader.rb
class RecordLoader < GraphQL::Batch::Loader
def initialize(model)
@model = model
end
def perform(ids)
@model.where(id: ids).each { |record| fulfill(record.id, record) }
ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
end
end
# Type์์ ์ฌ์ฉ
field :posts, [Types::PostType], null: true
def posts
RecordLoader.for(Post).load_many(object.post_ids)
end
์ด๋ ๊ฒ ํ๋ฉด ์ฟผ๋ฆฌ๊ฐ ์๋์ผ๋ก ๋ฐฐ์น ์ฒ๋ฆฌ๋์ด ๋จ 2๋ฒ์ DB ์ฟผ๋ฆฌ๋ก ์ค์ด๋ญ๋๋ค! ๐
๐ ์ธ์ฆ๊ณผ ๊ถํ ๊ด๋ฆฌ
์ค์ ์๋น์ค์์๋ ์ธ์ฆ๊ณผ ๊ถํ ๊ด๋ฆฌ๊ฐ ํ์๊ฒ ์ฃ ? GraphQL์์๋ ์ด๋ฅผ ๋งค์ฐ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ด์:
# app/graphql/mutations/base_mutation.rb
module Mutations
class BaseMutation < GraphQL::Schema::Mutation
def ready?(**args)
if context[:current_user].nil?
raise GraphQL::ExecutionError, "๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค"
end
true
end
end
end
# ํน์ ๊ถํ ์ฒดํฌ
class DeletePost < BaseMutation
argument :id, ID, required: true
field :success, Boolean, null: false
def resolve(id:)
post = Post.find(id)
unless post.user_id == context[:current_user].id
raise GraphQL::ExecutionError, "๊ถํ์ด ์์ต๋๋ค"
end
{ success: post.destroy }
end
end
๐ ํ์ด์ง๋ค์ด์ ๊ตฌํํ๊ธฐ
๋๋์ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃฐ ๋๋ ํ์ด์ง๋ค์ด์ ์ด ํ์์ ๋๋ค. GraphQL์๋ Relay ์คํ์ผ์ ์ปค์ ๊ธฐ๋ฐ ํ์ด์ง๋ค์ด์ ์ด๋ผ๋ ํ์ค์ด ์์ด์:
# app/graphql/types/query_type.rb
field :posts, Types::PostType.connection_type, null: false do
argument :first, Integer, required: false
argument :after, String, required: false
end
def posts(first: 10, after: nil)
Post.all
end
ํด๋ผ์ด์ธํธ์์๋ ์ด๋ ๊ฒ ์ฌ์ฉํฉ๋๋ค:
query {
posts(first: 10, after: "cursor123") {
edges {
node {
id
title
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
์ด ๋ฐฉ์์ ๋ฌดํ ์คํฌ๋กค ๊ตฌํ์ ์๋ฒฝํ๊ณ , ๋ฐ์ดํฐ๊ฐ ์ถ๊ฐ/์ญ์ ๋์ด๋ ์์ ์ ์ผ๋ก ์๋ํฉ๋๋ค! ๐
๐ ์ค์๊ฐ ์ ๋ฐ์ดํธ: Subscriptions
GraphQL์ ์จ๊ฒจ์ง ๋ณด์, Subscription์ ์๊ฐํฉ๋๋ค! ์ค์๊ฐ ์ฑํ ์ด๋ ์๋ฆผ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ์ ๋ง ์ ์ฉํด์:
# app/graphql/types/subscription_type.rb
class Types::SubscriptionType < GraphQL::Schema::Object
field :message_added, Types::MessageType, null: false do
argument :room_id, ID, required: true
end
def message_added(room_id:)
# ๊ตฌ๋
์ค์
end
end
# ๋ฉ์์ง ์์ฑ ์ ํธ๋ฆฌ๊ฑฐ
class CreateMessage < BaseMutation
def resolve(room_id:, content:)
message = Message.create!(room_id: room_id, content: content)
# ๊ตฌ๋
์๋ค์๊ฒ ์๋ฆผ
YourSchema.subscriptions.trigger(
'messageAdded',
{ room_id: room_id },
message
)
{ message: message }
end
end
WebSocket์ ํตํด ์ค์๊ฐ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ํธ์ํ ์ ์์ด์, ๋ณ๋์ ํด๋ง์ด๋ ๋ณต์กํ ๋ก์ง ์์ด๋ ์ค์๊ฐ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์์ต๋๋ค! ๐
โก ์ฑ๋ฅ ์ต์ ํ ์ ๋ต
GraphQL์ ์ ์ฐ์ฑ์ ์๋ ์ ๊ฒ์ด ๋ ์ ์์ด์. ์๋ชป ์ฌ์ฉํ๋ฉด ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๊ฑฐ๋ ์. ์ค์ ์์ ๊ฒ์ฆ๋ ์ต์ ํ ๊ธฐ๋ฒ๋ค์ ๊ณต์ ํ ๊ฒ์! ๐ช
๐ฏ ์ฟผ๋ฆฌ ๋ณต์ก๋ ์ ํ
์ ์์ ์ธ ์ฌ์ฉ์๊ฐ ๋๋ฌด ๋ณต์กํ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋ด์ ์๋ฒ๋ฅผ ๋ง๋น์ํฌ ์ ์์ด์. ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ์ฟผ๋ฆฌ ๋ณต์ก๋๋ฅผ ์ ํํฉ๋๋ค:
# app/graphql/your_schema.rb
class YourSchema < GraphQL::Schema
max_complexity 200
max_depth 10
# ํ์์์ ์ค์
default_max_page_size 100
end
# ํ๋๋ณ ๋ณต์ก๋ ์ค์
field :posts, [PostType], null: false do
complexity 5
end
field :expensive_calculation, String, null: false do
complexity 50
end
๐พ ์บ์ฑ ์ ๋ต
GraphQL์์ ์บ์ฑ์ ์กฐ๊ธ ๊น๋ค๋ก์ธ ์ ์์ด์. REST์ฒ๋ผ URL ๊ธฐ๋ฐ ์บ์ฑ์ด ์ ๋๋๊น์. ํ์ง๋ง ๋ฐฉ๋ฒ์ ์์ต๋๋ค!
# 1. ์ฟผ๋ฆฌ ๋ ๋ฒจ ์บ์ฑ
def users
Rails.cache.fetch("users_list", expires_in: 5.minutes) do
User.all.to_a
end
end
# 2. ํ๋ ๋ ๋ฒจ ์บ์ฑ
field :expensive_data, String, null: false
def expensive_data
Rails.cache.fetch("user_#{object.id}_expensive_data", expires_in: 1.hour) do
# ๋ณต์กํ ๊ณ์ฐ...
calculate_expensive_data
end
end
# 3. DataLoader๋ฅผ ํ์ฉํ ์์ฒญ ๋ ๋ฒจ ์บ์ฑ
class UserLoader < GraphQL::Batch::Loader
def perform(ids)
User.where(id: ids).each { |user| fulfill(user.id, user) }
end
end
๐ ๋ชจ๋ํฐ๋ง๊ณผ ๋ถ์
์ด๋ค ์ฟผ๋ฆฌ๊ฐ ๋๋ฆฐ์ง, ์ด๋ค ํ๋๊ฐ ์์ฃผ ์ฌ์ฉ๋๋์ง ์์์ผ ์ต์ ํํ ์ ์๊ฒ ์ฃ ? Apollo Studio๋ GraphQL Inspector ๊ฐ์ ๋๊ตฌ๋ฅผ ํ์ฉํ์ธ์:
# app/graphql/your_schema.rb
class YourSchema < GraphQL::Schema
# ์ฟผ๋ฆฌ ์คํ ์๊ฐ ์ธก์
trace_with GraphQL::Tracing::NewRelicTracing
# ๋๋ ์ปค์คํ
ํธ๋ ์ด์ฑ
trace_with CustomTracer
end
class CustomTracer < GraphQL::Tracing::PlatformTracing
def platform_trace(platform_key, key, data)
start_time = Time.now
result = yield
duration = Time.now - start_time
Rails.logger.info "GraphQL #{key}: #{duration}s"
result
end
end
๐ก๏ธ ๋ณด์ ๋ฒ ์คํธ ํ๋ํฐ์ค
API ๋ณด์์ ์๋ฌด๋ฆฌ ๊ฐ์กฐํด๋ ์ง๋์น์ง ์์ฃ ! GraphQL ํน์ ์ ๋ณด์ ์ด์๋ค๊ณผ ๋์ ๋ฐฉ๋ฒ์ ์์๋ด ์๋ค. ๐
๐ซ Introspection ์ฟผ๋ฆฌ ์ ํ
๊ฐ๋ฐ ์ค์๋ ํธ๋ฆฌํ์ง๋ง, ํ๋ก๋์ ์์๋ ์คํค๋ง ์ ๋ณด ๋ ธ์ถ์ด ๋ณด์ ์ํ์ด ๋ ์ ์์ด์:
class YourSchema < GraphQL::Schema
# ํ๋ก๋์
์์ Introspection ๋นํ์ฑํ
disable_introspection_entry_points if Rails.env.production?
# ๋๋ ์กฐ๊ฑด๋ถ ํ์ฉ
def self.introspection_enabled?
context = Thread.current[:graphql_context]
context[:current_user]&.admin?
end
end
๐ ํ๋ ๋ ๋ฒจ ๊ถํ ๊ด๋ฆฌ
๋ฏผ๊ฐํ ์ ๋ณด๋ ํ๋ ๋ ๋ฒจ์์ ์ ๊ทผ์ ์ ์ดํด์ผ ํฉ๋๋ค:
class UserType < Types::BaseObject
field :email, String, null: false
def email
# ๋ณธ์ธ ๋๋ ๊ด๋ฆฌ์๋ง ์ด๋ฉ์ผ ์กฐํ ๊ฐ๋ฅ
if context[:current_user]&.id == object.id || context[:current_user]&.admin?
object.email
else
raise GraphQL::ExecutionError, "์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค"
end
end
# ๋๋ ๋ ๊น๋ํ๊ฒ
field :email, String, null: false do
authorize :read_email
end
end
โ ๏ธ Rate Limiting
API ๋จ์ฉ์ ๋ฐฉ์งํ๊ธฐ ์ํ ์๋ ์ ํ๋ ํ์์ ๋๋ค:
# Rack::Attack ์ฌ์ฉ
class Application < Rails::Application
config.middleware.use Rack::Attack
end
# config/initializers/rack_attack.rb
Rack::Attack.throttle('graphql/ip', limit: 100, period: 1.minute) do |req|
req.ip if req.path == '/graphql'
end
# ์ฌ์ฉ์๋ณ ์ ํ
Rack::Attack.throttle('graphql/user', limit: 500, period: 1.hour) do |req|
if req.path == '/graphql'
# JWT ํ ํฐ์์ ์ฌ์ฉ์ ID ์ถ์ถ
extract_user_id(req)
end
end
๐จ ํ๋ก ํธ์๋์์ ์๋ฒฝํ ์กฐํ
GraphQL์ ์ง๊ฐ๋ ํ๋ก ํธ์๋์ ํจ๊ปํ ๋ ๋น์ ๋ฐํฉ๋๋ค! React, Vue, Angular ๋ฑ ์ด๋ค ํ๋ ์์ํฌ์๋ ํ์์ ๊ถํฉ์ ์๋ํ์ฃ . ๐
โ๏ธ React + Apollo Client
๊ฐ์ฅ ์ธ๊ธฐ ์๋ ์กฐํฉ์ธ React์ Apollo Client๋ฅผ ์ดํด๋ณผ๊น์?
// Apollo Client ์ค์
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:3000/graphql',
cache: new InMemoryCache(),
headers: {
authorization: `Bearer ${localStorage.getItem('token')}`
}
});
// React ์ปดํฌ๋ํธ์์ ์ฌ์ฉ
import { useQuery, gql } from '@apollo/client';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
posts {
title
}
}
}
`;
function UserList() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <p>๋ก๋ฉ ์ค...</p>;
if (error) return <p>์๋ฌ ๋ฐ์: {error.message}</p>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
<p>๊ฒ์๊ธ ์: {user.posts.length}</p>
</div>
))}
</div>
);
}
Apollo Client๋ ์๋ ์บ์ฑ, ๋๊ด์ UI ์ ๋ฐ์ดํธ, ์๋ฌ ํธ๋ค๋ง ๋ฑ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค! ๐
๐ Mutation ์คํํ๊ธฐ
import { useMutation, gql } from '@apollo/client';
const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!, $password: String!) {
createUser(input: {
name: $name
email: $email
password: $password
}) {
user {
id
name
email
}
errors
}
}
`;
function SignupForm() {
const [createUser, { loading, error }] = useMutation(CREATE_USER);
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const { data } = await createUser({
variables: {
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password')
}
});
if (data.createUser.errors.length === 0) {
alert('ํ์๊ฐ์
์ฑ๊ณต!');
} else {
alert(data.createUser.errors.join(', '));
}
} catch (err) {
console.error('์๋ฌ:', err);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="์ด๋ฆ" required />
<input name="email" type="email" placeholder="์ด๋ฉ์ผ" required />
<input name="password" type="password" placeholder="๋น๋ฐ๋ฒํธ" required />
<button type="submit" disabled={loading}>
{loading ? '์ฒ๋ฆฌ ์ค...' : '๊ฐ์
ํ๊ธฐ'}
</button>
</form>
);
}
๐ฑ ์ค์๊ฐ ์ ๋ฐ์ดํธ ๊ตฌ๋
import { useSubscription, gql } from '@apollo/client';
const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageAdded($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
content
user {
name
}
createdAt
}
}
`;
function ChatRoom({ roomId }) {
const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION, {
variables: { roomId }
});
if (loading) return <p>์ฐ๊ฒฐ ์ค...</p>;
return (
<div>
<p>์ ๋ฉ์์ง: {data.messageAdded.content}</p>
<p>๋ณด๋ธ ์ฌ๋: {data.messageAdded.user.name}</p>
</div>
);
}
์ด๋ ๊ฒ ๊ฐ๋จํ๊ฒ ์ค์๊ฐ ์ฑํ ์ ๊ตฌํํ ์ ์๋ค๋, ์ ๋ง ๋๋์ง ์๋์? ๐
๐งช ํ ์คํ ์ ๋ต
์ข์ ์ฝ๋๋ ํ ์คํธ๋ก ์์ฑ๋ฉ๋๋ค! GraphQL API๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํ ์คํธํ๋ ๋ฐฉ๋ฒ์ ์์๋ณผ๊ฒ์. ๐ฌ
โ RSpec์ผ๋ก Query ํ ์คํธํ๊ธฐ
# spec/graphql/queries/users_query_spec.rb
require 'rails_helper'
RSpec.describe 'Users Query', type: :request do
describe 'users' do
let!(:users) { create_list(:user, 3) }
let(:query) do
<<~GQL
query {
users {
id
name
email
}
}
GQL
end
it '๋ชจ๋ ์ฌ์ฉ์๋ฅผ ๋ฐํํ๋ค' do
post '/graphql', params: { query: query }
json = JSON.parse(response.body)
data = json['data']['users']
expect(data.length).to eq(3)
expect(data.first['name']).to eq(users.first.name)
end
end
describe 'user(id:)' do
let(:user) { create(:user) }
let(:query) do
<<~GQL
query($id: ID!) {
user(id: $id) {
id
name
email
}
}
GQL
end
it 'ํน์ ์ฌ์ฉ์๋ฅผ ๋ฐํํ๋ค' do
post '/graphql', params: {
query: query,
variables: { id: user.id }
}
json = JSON.parse(response.body)
data = json['data']['user']
expect(data['id']).to eq(user.id.to_s)
expect(data['name']).to eq(user.name)
end
end
end
โ Mutation ํ ์คํธํ๊ธฐ
# spec/graphql/mutations/create_user_spec.rb
require 'rails_helper'
RSpec.describe Mutations::CreateUser, type: :request do
let(:mutation) do
<<~GQL
mutation($name: String!, $email: String!, $password: String!) {
createUser(input: {
name: $name
email: $email
password: $password
}) {
user {
id
name
email
}
errors
}
}
GQL
end
context '์ ํจํ ์
๋ ฅ์ผ ๋' do
let(:variables) do
{
name: 'John Doe',
email: 'john@example.com',
password: 'password123'
}
end
it '์ฌ์ฉ์๋ฅผ ์์ฑํ๋ค' do
expect {
post '/graphql', params: { query: mutation, variables: variables }
}.to change(User, :count).by(1)
json = JSON.parse(response.body)
data = json['data']['createUser']
expect(data['user']['name']).to eq('John Doe')
expect(data['errors']).to be_empty
end
end
context '์ ํจํ์ง ์์ ์
๋ ฅ์ผ ๋' do
let(:variables) do
{
name: '',
email: 'invalid-email',
password: '123'
}
end
it '์๋ฌ๋ฅผ ๋ฐํํ๋ค' do
post '/graphql', params: { query: mutation, variables: variables }
json = JSON.parse(response.body)
data = json['data']['createUser']
expect(data['user']).to be_nil
expect(data['errors']).not_to be_empty
end
end
end
๐ ์ค์ ํ๋ก์ ํธ ์ฌ๋ก ์ฐ๊ตฌ
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ด์ ์ค์ ํ๋ก์ ํธ์์ ์ด๋ป๊ฒ ํ์ฉ๋๋์ง ์ดํด๋ณผ๊น์? ๐ฏ
๐ฑ ์์ ๋ฏธ๋์ด ํ๋ซํผ
Instagram์ด๋ Twitter ๊ฐ์ ์์ ๋ฏธ๋์ด๋ GraphQL์ ์๋ฒฝํ ์ฌ์ฉ ์ฌ๋ก์ ๋๋ค:
1. ํผ๋ ๋ก๋ฉ ์ต์ ํ
- REST: ๊ฒ์๊ธ ๋ชฉ๋ก โ ๊ฐ ๊ฒ์๊ธ์ ์์ฑ์ ์ ๋ณด โ ๊ฐ ๊ฒ์๊ธ์ ๋๊ธ โ ๊ฐ ๋๊ธ์ ์์ฑ์ (4๋จ๊ณ ์์ฒญ)
- GraphQL: ํ ๋ฒ์ ์์ฒญ์ผ๋ก ๋ชจ๋ ๋ฐ์ดํฐ ์กฐํ
2. ๋ชจ๋ฐ์ผ ๋ฐ์ดํฐ ์ ์ฝ
- ๋ชจ๋ฐ์ผ ์ฑ์์๋ ์ธ๋ค์ผ๋ง, ์น์์๋ ๊ณ ํด์๋ ์ด๋ฏธ์ง๋ฅผ ์์ฒญ
- ํ์ํ ํ๋๋ง ์ ํ์ ์ผ๋ก ๊ฐ์ ธ์ ๋ฐ์ดํฐ ์ฌ์ฉ๋ 50% ๊ฐ์
3. ์ค์๊ฐ ์๋ฆผ
- Subscription์ ํ์ฉํด ์ ๋๊ธ, ์ข์์, ํ๋ก์ฐ ์๋ฆผ์ ์ค์๊ฐ์ผ๋ก ํธ์
- ํด๋ง ์์ด๋ ์ฆ๊ฐ์ ์ธ ์ ๋ฐ์ดํธ ๊ฐ๋ฅ
๐ ์ด์ปค๋จธ์ค ํ๋ซํผ
Shopify๋ GraphQL์ ์ ๋ฉด์ ์ผ๋ก ๋์ ํ ๋ํ์ ์ธ ์ด์ปค๋จธ์ค ํ๋ซํผ์ ๋๋ค:
# ์ํ ์์ธ ํ์ด์ง ์ฟผ๋ฆฌ ์์
query ProductPage($handle: String!) {
productByHandle(handle: $handle) {
id
title
description
priceRange {
minVariantPrice {
amount
currencyCode
}
}
images(first: 5) {
edges {
node {
url
altText
}
}
}
variants(first: 10) {
edges {
node {
id
title
price
availableForSale
selectedOptions {
name
value
}
}
}
}
reviews(first: 5) {
edges {
node {
rating
content
author {
name
}
}
}
}
}
}
์ด ํ๋์ ์ฟผ๋ฆฌ๋ก ์ํ ์ ๋ณด, ์ด๋ฏธ์ง, ์ต์ , ๋ฆฌ๋ทฐ๊น์ง ๋ชจ๋ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค! REST์๋ค๋ฉด ์ต์ 4-5๋ฒ์ API ํธ์ถ์ด ํ์ํ์ ๊ฑฐ์์. ๐ฎ
๐ ์จ๋ผ์ธ ๊ต์ก ํ๋ซํผ
๋ณต์กํ ๋ฐ์ดํฐ ๊ด๊ณ๋ฅผ ๊ฐ์ง ๊ต์ก ํ๋ซํผ์์๋ GraphQL์ด ๋น์ ๋ฐํฉ๋๋ค:
โข ํ์ต ๋์๋ณด๋
- ์๊ฐ ์ค์ธ ๊ฐ์ ๋ชฉ๋ก
- ๊ฐ ๊ฐ์์ ์งํ๋ฅ
- ๋ค์์ ๋ณผ ๋ ์จ
- ์๋ฃํ ๊ณผ์
- ํ๋ํ ๋ฐฐ์ง
โ ๋ชจ๋ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ์กฐํ!
โข ๊ฐ์ ์์ธ ํ์ด์ง
- ๊ฐ์ ์ ๋ณด + ์ปค๋ฆฌํ๋ผ + ๊ฐ์ฌ ์ ๋ณด + ์๊ฐํ + ๊ด๋ จ ๊ฐ์
โ ์ฌ์ฉ์ ๊ถํ์ ๋ฐ๋ผ ๋ค๋ฅธ ํ๋ ๋ฐํ (๋ฏธ๋ฆฌ๋ณด๊ธฐ vs ์ ์ฒด ์ ๊ทผ)
โข ์ค์๊ฐ Q&A
- Subscription์ผ๋ก ์ค์๊ฐ ์ง๋ฌธ/๋ต๋ณ ์ ๋ฐ์ดํธ
- ๊ฐ์ ์ค ์ค์๊ฐ ์ฑํ ๊ธฐ๋ฅ
๐ง ํํ ์ค์์ ํด๊ฒฐ์ฑ
GraphQL์ ์ฒ์ ๋์ ํ ๋ ๋ง์ ๊ฐ๋ฐ์๋ค์ด ๊ฒช๋ ์ค์๋ค์ ์ ๋ฆฌํด๋ดค์ด์. ๋ฏธ๋ฆฌ ์๊ณ ์์ผ๋ฉด ์ํ์ฐฉ์ค๋ฅผ ์ค์ผ ์ ์๊ฒ ์ฃ ? ๐
โ ์ค์ 1: ๋ชจ๋ ๊ฒ์ ํ๋์ ์ฟผ๋ฆฌ์ ๋ฃ๊ธฐ
query EverythingQuery {
currentUser {
id
name
email
posts {
id
title
comments {
id
content
author {
name
posts {
title
}
}
}
}
followers {
name
followers {
name
}
}
}
}
๋ฌธ์ ์ : ๋๋ฌด ๊น์ ์ค์ฒฉ์ ์ฑ๋ฅ ๋ฌธ์ ๋ฅผ ์ผ์ผํค๊ณ , ์บ์ฑ๋ ์ด๋ ต๊ฒ ๋ง๋ญ๋๋ค.
ํด๊ฒฐ์ฑ : ์ฟผ๋ฆฌ๋ฅผ ๋ชฉ์ ๋ณ๋ก ๋ถ๋ฆฌํ๊ณ , ํ์ํ ๊น์ด๋ง ์กฐํํ์ธ์:
# ์ฌ์ฉ์ ํ๋กํ์ฉ
query UserProfile {
currentUser {
id
name
email
postsCount
followersCount
}
}
# ๊ฒ์๊ธ ๋ชฉ๋ก์ฉ
query UserPosts($userId: ID!) {
user(id: $userId) {
posts(first: 10) {
edges {
node {
id
title
commentsCount
}
}
}
}
}
โ ์ค์ 2: N+1 ๋ฌธ์ ๋ฌด์ํ๊ธฐ
์์ ์ธ๊ธํ์ง๋ง, ์ด ๋ฌธ์ ๋ ์ ๋ง ํํ๊ณ ์ฌ๊ฐํด์ ๋ค์ ํ๋ฒ ๊ฐ์กฐํ ๊ฒ์!
# ๋์ ์ - N+1 ๋ฐ์
class UserType < Types::BaseObject
field :posts, [PostType], null: true
def posts
object.posts # ๊ฐ ์ฌ์ฉ์๋ง๋ค ๋ณ๋ ์ฟผ๋ฆฌ ์คํ!
end
end
# ์ข์ ์ - Eager Loading
class QueryType < Types::BaseObject
field :users, [UserType], null: false
def users
User.includes(:posts) # ๋ฏธ๋ฆฌ ๋ก๋!
end
end
โ ์ค์ 3: ์๋ฌ ์ฒ๋ฆฌ ์ํ
GraphQL์ HTTP 200์ ๋ฐํํ๋ฉด์๋ ์๋ฌ๋ฅผ ํฌํจํ ์ ์์ด์. ํด๋ผ์ด์ธํธ์์ ๋ฐ๋์ ํ์ธํด์ผ ํฉ๋๋ค:
// ๋์ ์
const { data } = await client.query({ query: GET_USERS });
console.log(data.users); // data๊ฐ null์ผ ์ ์์!
// ์ข์ ์
const { data, errors } = await client.query({ query: GET_USERS });
if (errors) {
console.error('GraphQL ์๋ฌ:', errors);
// ์๋ฌ ์ฒ๋ฆฌ ๋ก์ง
return;
}
if (!data || !data.users) {
console.error('๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค');
return;
}
console.log(data.users);
โ ์ค์ 4: ํ์ ์ ์ ๋ถ๋ช ํ
# ๋์ ์
field :created_at, String, null: true # ๋ฌธ์์ด? ๋ ์ง?
# ์ข์ ์
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :status, Types::PostStatusEnum, null: false # Enum ์ฌ์ฉ
field :tags, [String], null: false # ๋ฐฐ์ด์์ ๋ช
ํํ
ํ์ ์ ๋ช ํํ๊ฒ ์ ์ํ๋ฉด ์๋ ์์ฑ, ํ์ ์ฒดํฌ, ๋ฌธ์ํ๊ฐ ๋ชจ๋ ๊ฐ์ ๋ฉ๋๋ค! ๐ฏ
๐ฎ GraphQL์ ๋ฏธ๋์ ํธ๋ ๋
GraphQL์ ๊ณ์ ์งํํ๊ณ ์์ด์! ์์ผ๋ก ์ฃผ๋ชฉํด์ผ ํ ํธ๋ ๋๋ค์ ์ดํด๋ณผ๊น์? ๐
๐ Federation (์ฐํฉ)
๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ์์ ์ฌ๋ฌ GraphQL ์๋น์ค๋ฅผ ํ๋๋ก ํตํฉํ๋ ๊ธฐ์ ์ ๋๋ค:
โข ๊ฐ ํ์ด ๋ ๋ฆฝ์ ์ผ๋ก ์คํค๋ง ๊ด๋ฆฌ
โข ๋จ์ผ GraphQL ์๋ํฌ์ธํธ๋ก ๋ชจ๋ ์๋น์ค ์ ๊ทผ
โข ํ์ ์ ์ฌ๋ฌ ์๋น์ค์ ๊ฑธ์ณ ํ์ฅ ๊ฐ๋ฅ
โข ์ ์ง์ ๋ง์ด๊ทธ๋ ์ด์ ์ง์
์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ์๋น์ค์ ์ฃผ๋ฌธ ์๋น์ค๊ฐ ๋ถ๋ฆฌ๋์ด ์์ด๋ ํด๋ผ์ด์ธํธ๋ ํ๋์ ์ฟผ๋ฆฌ๋ก ์ฌ์ฉ์ ์ ๋ณด์ ์ฃผ๋ฌธ ๋ด์ญ์ ํจ๊ป ์กฐํํ ์ ์์ต๋๋ค!
โก Defer์ Stream
GraphQL์ ์๋ก์ด ๊ธฐ๋ฅ์ผ๋ก, ๋์ฉ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ง์ ์ผ๋ก ์ ์กํ ์ ์์ต๋๋ค:
query {
user(id: "123") {
name
email
... @defer {
posts {
title
content
}
}
}
}
์ด๋ ๊ฒ ํ๋ฉด ๊ธฐ๋ณธ ์ ๋ณด๋ ์ฆ์ ๋ฐ๊ณ , ๊ฒ์๊ธ์ ์ค๋น๋๋ ๋๋ก ๋ฐ์ ์ ์์ด์. ์ฌ์ฉ์ ๊ฒฝํ์ด ํจ์ฌ ์ข์์ง๊ฒ ์ฃ ? ๐
๐ Persisted Queries
๋ณด์๊ณผ ์ฑ๋ฅ์ ๋์์ ๊ฐ์ ํ๋ ๊ธฐ๋ฒ์ ๋๋ค:
1. ํด๋ผ์ด์ธํธ๊ฐ ์ฟผ๋ฆฌ ํด์๋ง ์ ์ก
2. ์๋ฒ๋ ๋ฏธ๋ฆฌ ๋ฑ๋ก๋ ์ฟผ๋ฆฌ ์คํ
3. ๋คํธ์ํฌ ํธ๋ํฝ ๊ฐ์ + ์ ์์ ์ฟผ๋ฆฌ ์ฐจ๋จ
ํนํ ๋ชจ๋ฐ์ผ ์ฑ์์ ํจ๊ณผ์ ์ด์์. ์ฟผ๋ฆฌ ๋ฌธ์์ด์ ๋งค๋ฒ ์ ์กํ์ง ์์๋ ๋๋๊น์!
๐ค AI์ GraphQL
์ต๊ทผ์๋ AI๊ฐ GraphQL ์ฟผ๋ฆฌ๋ฅผ ์๋์ผ๋ก ์์ฑํ๊ฑฐ๋ ์ต์ ํํ๋ ๋๊ตฌ๋ค๋ ๋ฑ์ฅํ๊ณ ์์ด์:
โข AI ๊ธฐ๋ฐ ์ฟผ๋ฆฌ ์ต์ ํ ๋๊ตฌ
โข ์์ฐ์ด๋ก GraphQL ์ฟผ๋ฆฌ ์์ฑ
โข ์๋ ์คํค๋ง ์ค๊ณ ์ถ์ฒ
โข ์ฑ๋ฅ ๋ณ๋ชฉ ์ง์ ์๋ ๊ฐ์ง
๊ฐ๋ฐ์์ ์์ฐ์ฑ์ด ๋์ฑ ๋์์ง ๊ฒ์ผ๋ก ๊ธฐ๋๋ฉ๋๋ค!
๐ผ ์ค๋ฌด ๋์ ๊ฐ์ด๋
์, ์ด์ ์ค์ ๋ก ํ์ฌ๋ ํ๋ก์ ํธ์ GraphQL์ ๋์ ํ๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น์? ๋จ๊ณ๋ณ๋ก ์์๋ด ์๋ค! ๐
1๏ธโฃ 1๋จ๊ณ: ํ๊ฐ์ ๊ณํ
โก ํ์ฌ API์ ๋ฌธ์ ์ ํ์ (Over-fetching, Under-fetching, ๋ฒ์ ๊ด๋ฆฌ ๋ฑ)
โก ํ์ ๊ธฐ์ ์คํ๊ณผ ํธํ์ฑ ํ์ธ
โก ํ์ต ๊ณก์ ๊ณ ๋ ค (ํ์๋ค์ ๊ต์ก ์๊ฐ)
โก ๊ธฐ์กด REST API์์ ๊ณต์กด ์ ๋ต ์๋ฆฝ
โก ์ฑ๋ฅ ๋ชฉํ ์ค์ (์๋ต ์๊ฐ, ๋ฐ์ดํฐ ์ ์ก๋ ๋ฑ)
โก ๋ณด์ ์๊ตฌ์ฌํญ ๊ฒํ
2๏ธโฃ 2๋จ๊ณ: ํ์ผ๋ฟ ํ๋ก์ ํธ
์ฒ์๋ถํฐ ์ ์ฒด๋ฅผ ๋ง์ด๊ทธ๋ ์ด์ ํ์ง ๋ง์ธ์! ์์ ๊ธฐ๋ฅ๋ถํฐ ์์ํ๋ ๊ฒ์ด ์์ ํฉ๋๋ค:
# config/routes.rb
Rails.application.routes.draw do
# ๊ธฐ์กด REST API๋ ์ ์ง
namespace :api do
namespace :v1 do
resources :users
resources :posts
end
end
# GraphQL์ ๋ณ๋ ์๋ํฌ์ธํธ๋ก
post "/graphql", to: "graphql#execute"
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end
end
์ถ์ฒ ํ์ผ๋ฟ ๊ธฐ๋ฅ:
- โข ์๋ก์ด ๊ธฐ๋ฅ (๊ธฐ์กด ์ฝ๋ ์ํฅ ์์)
- โข ๋ณต์กํ ๋ฐ์ดํฐ ์กฐํ๊ฐ ํ์ํ ํ๋ฉด
- โข ๋ชจ๋ฐ์ผ ์ฑ์ ํน์ ํ๋ฉด
- โข ๋ด๋ถ ๊ด๋ฆฌ์ ๋๊ตฌ
3๏ธโฃ 3๋จ๊ณ: ์ ์ง์ ํ์ฅ
ํ์ผ๋ฟ์ด ์ฑ๊ณตํ๋ค๋ฉด, ๋จ๊ณ์ ์ผ๋ก ํ์ฅํ์ธ์:
| ๋จ๊ณ | ์์ | ์์ ๊ธฐ๊ฐ |
|---|---|---|
| Phase 1 | ํต์ฌ ํ์ ์ ์ (User, Post ๋ฑ) | 1-2์ฃผ |
| Phase 2 | ์ฝ๊ธฐ ์์ ๋ง์ด๊ทธ๋ ์ด์ | 2-4์ฃผ |
| Phase 3 | ์ฐ๊ธฐ ์์ (Mutations) ์ถ๊ฐ | 2-3์ฃผ |
| Phase 4 | ์ค์๊ฐ ๊ธฐ๋ฅ (Subscriptions) | 1-2์ฃผ |
| Phase 5 | ์ฑ๋ฅ ์ต์ ํ ๋ฐ ๋ชจ๋ํฐ๋ง | ์ง์์ |
4๏ธโฃ 4๋จ๊ณ: ํ ๊ต์ก
๊ธฐ์ ๋์ ์ ์ฑ๊ณต์ ํ์ ์ดํด๋์ ๋ฌ๋ ค์์ด์:
Week 1: ๊ธฐ์ด
โข GraphQL ๊ฐ๋ ๊ณผ ์ฒ ํ
โข REST์์ ์ฐจ์ด์
โข ๊ฐ๋จํ ์ฟผ๋ฆฌ ์์ฑ ์ค์ต
Week 2: Rails ํตํฉ
โข graphql-ruby gem ์ฌ์ฉ๋ฒ
โข ํ์ ์ ์์ Resolver ์์ฑ
โข N+1 ๋ฌธ์ ํด๊ฒฐ
Week 3: ๊ณ ๊ธ ๊ธฐ๋ฅ
โข Mutation๊ณผ ์๋ฌ ์ฒ๋ฆฌ
โข ์ธ์ฆ/๊ถํ ๊ด๋ฆฌ
โข ์ฑ๋ฅ ์ต์ ํ
Week 4: ํ๋ก ํธ์๋ ์ฐ๋
โข Apollo Client ์ฌ์ฉ๋ฒ
โข ์บ์ฑ ์ ๋ต
โข ์ค์๊ฐ ์ ๋ฐ์ดํธ
์ฌ๋ฅ๋ท(https://www.jaenung.net) ๊ฐ์ ํ๋ซํผ์์ GraphQL ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์ ํ ๊ต์ก์ ์งํํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์์! ์ค๋ฌด ๊ฒฝํ์ด ํ๋ถํ ๊ฐ๋ฐ์์๊ฒ ์ง์ ๋ฐฐ์ฐ๋ฉด ์ํ์ฐฉ์ค๋ฅผ ํฌ๊ฒ ์ค์ผ ์ ์๊ฑฐ๋ ์. ๐
๐ฏ ์ฑ๊ณต ์ฌ๋ก์ ROI
GraphQL ๋์ ์ผ๋ก ์ค์ ๋ก ์ด๋ค ์ฑ๊ณผ๋ฅผ ๊ฑฐ๋ ์ ์์๊น์? ์ค์ ๋ฐ์ดํฐ๋ฅผ ๋ฐํ์ผ๋ก ์ดํด๋ด ์๋ค! ๐
๐ ์ ๋์ ์ฑ๊ณผ
โข API ํธ์ถ ํ์: ํ๊ท 5ํ โ 1ํ (80% ๊ฐ์)
โข ๋ฐ์ดํฐ ์ ์ก๋: 40% ๊ฐ์
โข ๋ชจ๋ฐ์ผ ์ฑ ๋ก๋ฉ ์๊ฐ: 30% ๊ฐ์
โข ๊ฐ๋ฐ ์๋: ์ ๊ธฐ๋ฅ ์ถ์ ์๊ฐ 50% ๋จ์ถ
๐ฐ ๋น์ฉ ์ ๊ฐ ํจ๊ณผ:
โข ์๋ฒ ๋น์ฉ: ์ $5,000 โ $3,500 (30% ์ ๊ฐ)
โข ๊ฐ๋ฐ ์๊ฐ: ๊ธฐ๋ฅ๋น ํ๊ท 2์ฃผ โ 1์ฃผ
โข API ๋ฌธ์ํ ์๊ฐ: ๊ฑฐ์ 0 (์๋ ์์ฑ)
๐ ์ ์ฑ์ ์ฑ๊ณผ
- โ "ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํํ๊ฒ ์์ฒญํ ์ ์์ด์ ์ข์์"
- โ "ํ๋ก ํธ์๋์ ๋ฐฑ์๋ ๊ฐ ์ปค๋ฎค๋์ผ์ด์ ์ด ๋ช ํํด์ก์ด์"
- โ "ํ์ ์์ ์ฑ ๋๋ถ์ ๋ฒ๊ทธ๊ฐ ์ค์์ด์"
- โ "GraphiQL๋ก API ํ ์คํธ๊ฐ ์ ๋ง ํธํด์"
- โ "๋ฒ์ ๊ด๋ฆฌ ๊ฑฑ์ ์ด ์์ด์ก์ด์"
โ ๏ธ ์ฃผ์ํ ์
๋ฌผ๋ก ์ฅ์ ๋ง ์๋ ๊ฒ์ ์๋์์. ์์งํ๊ฒ ๋จ์ ๋ ์๋ ค๋๋ฆด๊ฒ์:
1. ํ์ต ๊ณก์
โข ํ์๋ค์ด ์๋ก์ด ๊ฐ๋ ์ ์ตํ๋ ๋ฐ ์๊ฐ ํ์
โข ํนํ ์คํค๋ง ์ค๊ณ๋ ๊ฒฝํ์ด ์ค์
2. ์บ์ฑ ๋ณต์ก๋
โข REST์ HTTP ์บ์ฑ์ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์
โข ๋ณ๋์ ์บ์ฑ ์ ๋ต ํ์
3. ํ์ผ ์ ๋ก๋
โข GraphQL ํ์ค์ ํ์ผ ์ ๋ก๋ ์คํ์ด ์์
โข ๋ณ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ฐฉ๋ฒ ํ์
4. ๋ชจ๋ํฐ๋ง
โข REST๋ณด๋ค ๋ณต์กํ ์ฟผ๋ฆฌ ์ถ์ ํ์
โข ์ ์ฉ ๋ชจ๋ํฐ๋ง ๋๊ตฌ ๊ณ ๋ ค
๐ ๏ธ ์ ์ฉํ ๋๊ตฌ์ ๋ฆฌ์์ค
GraphQL ๊ฐ๋ฐ์ ๋ ํจ์จ์ ์ผ๋ก ๋ง๋ค์ด์ค ๋๊ตฌ๋ค์ ์๊ฐํฉ๋๋ค! ๐
๐ง ๊ฐ๋ฐ ๋๊ตฌ
| ๋๊ตฌ | ์ฉ๋ | ์ถ์ฒ๋ |
|---|---|---|
| GraphiQL | ๋ธ๋ผ์ฐ์ ๊ธฐ๋ฐ IDE, ์ฟผ๋ฆฌ ํ ์คํธ | โญโญโญโญโญ |
| Apollo Studio | ์คํค๋ง ๊ด๋ฆฌ, ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง | โญโญโญโญโญ |
| Postman | API ํ ์คํธ, ์ปฌ๋ ์ ๊ด๋ฆฌ | โญโญโญโญ |
| GraphQL Voyager | ์คํค๋ง ์๊ฐํ | โญโญโญโญ |
| graphql-ruby | Rails์ฉ GraphQL ๊ตฌํ | โญโญโญโญโญ |
| graphql-batch | N+1 ๋ฌธ์ ํด๊ฒฐ | โญโญโญโญโญ |
๐ ํ์ต ๋ฆฌ์์ค
1. ๊ณต์ ๋ฌธ์
โข GraphQL.org - ๊ณต์ ์คํ๊ณผ ํํ ๋ฆฌ์ผ
โข graphql-ruby.org - Rails ์ ์ฉ ๊ฐ์ด๋
2. ์จ๋ผ์ธ ๊ฐ์
โข Udemy: "GraphQL with React and Rails"
โข Pluralsight: "Building GraphQL APIs with Ruby"
โข ์ฌ๋ฅ๋ท์์ GraphQL ์ ๋ฌธ๊ฐ์ 1:1 ๋ฉํ ๋ง๋ ์ถ์ฒ!
3. ์ปค๋ฎค๋ํฐ
โข GraphQL Korea (ํ์ด์ค๋ถ ๊ทธ๋ฃน)
โข Stack Overflow #graphql ํ๊ทธ
โข GitHub Discussions
4. ์ค์ต ํ๋ก์ ํธ
โข How to GraphQL (howtographql.com)
โข GraphQL ํํ ๋ฆฌ์ผ (๊ณต์ ์ฌ์ดํธ)
โข ์คํ์์ค ํ๋ก์ ํธ ๊ธฐ์ฌ
๐ ๋๋ฒ๊น ๋๊ตฌ
# ๊ฐ๋ฐ ํ๊ฒฝ์์ ์ฟผ๋ฆฌ ๋ก๊น
class YourSchema < GraphQL::Schema
if Rails.env.development?
trace_with GraphQL::Tracing::ActiveSupportNotificationsTracing
end
end
# config/initializers/graphql_logging.rb
ActiveSupport::Notifications.subscribe("execute_query.graphql") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
Rails.logger.info "GraphQL Query (#{event.duration.round(2)}ms): #{event.payload[:query]}"
end
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์๋ค์. ๐ Ruby on Rails์ GraphQL์ ์กฐํฉ์ ๋ํด ๊น์ด ์๊ฒ ์ดํด๋ดค์ต๋๋ค.
ํต์ฌ์ ๋ค์ ํ๋ฒ ์ ๋ฆฌํด๋ณผ๊น์?
1. GraphQL์ ์ฅ์
โ ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํํ๊ฒ ์์ฒญ
โ ๋จ์ผ ์๋ํฌ์ธํธ๋ก ๋ชจ๋ ๋ฐ์ดํฐ ์ ๊ทผ
โ ๊ฐ๋ ฅํ ํ์ ์์คํ
โ ์๋ ๋ฌธ์ํ
โ ์ค์๊ฐ ์ ๋ฐ์ดํธ ์ง์
2. Rails์์ ์๋ฒฝํ ์กฐํ
โ graphql-ruby gem์ผ๋ก ์ฌ์ด ํตํฉ
โ ActiveRecord์ ์์ฐ์ค๋ฌ์ด ์ฐ๋
โ Rails์ ๊ฐ๋ ฅํ ์ํ๊ณ ํ์ฉ
โ ๊ธฐ์กด REST API์ ๊ณต์กด ๊ฐ๋ฅ
3. ์ค๋ฌด ์ ์ฉ ํ
โ ์์ ๊ธฐ๋ฅ๋ถํฐ ์์
โ N+1 ๋ฌธ์ ํญ์ ์ฃผ์
โ ์ ์ ํ ์บ์ฑ ์ ๋ต ์๋ฆฝ
โ ๋ณด์๊ณผ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง ํ์
โ ํ ๊ต์ก์ ์ถฉ๋ถํ ์๊ฐ ํฌ์
GraphQL์ ๋จ์ํ ๊ธฐ์ ํธ๋ ๋๊ฐ ์๋๋ผ, API ๊ฐ๋ฐ์ ํจ๋ฌ๋ค์์ ๋ฐ๊พธ๋ ํ์ ์ ๋๋ค. ํนํ ๋ชจ๋ฐ์ผ ์ฐ์ ์๋, ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ, ์ค์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ผ๋ฐํ๋๋ฉด์ ๊ทธ ๊ฐ์น๋ ๋์ฑ ๋น์ ๋ฐํ๊ณ ์์ด์.
๋ฌผ๋ก ๋ชจ๋ ํ๋ก์ ํธ์ GraphQL์ด ์ ๋ต์ ์๋๋๋ค. ๊ฐ๋จํ CRUD ์ ํ๋ฆฌ์ผ์ด์ ์ด๋ผ๋ฉด REST API๋ก๋ ์ถฉ๋ถํ ์ ์์ด์. ํ์ง๋ง ๋ณต์กํ ๋ฐ์ดํฐ ๊ด๊ณ๋ฅผ ๋ค๋ฃจ๊ฑฐ๋, ๋ค์ํ ํด๋ผ์ด์ธํธ(์น, ๋ชจ๋ฐ์ผ, IoT ๋ฑ)๋ฅผ ์ง์ํด์ผ ํ๋ค๋ฉด, GraphQL์ ์ ๋ง ๊ฐ๋ ฅํ ์ ํ์ง๊ฐ ๋ ๊ฑฐ์์! ๐ช
์ด ๊ธ์ด ์ฌ๋ฌ๋ถ์ GraphQL ์ฌ์ ์ ์ข์ ์ถ๋ฐ์ ์ด ๋์๊ธฐ๋ฅผ ๋ฐ๋๋๋ค. ๊ถ๊ธํ ์ ์ด ์๋ค๋ฉด ์ฌ๋ฅ๋ท(https://www.jaenung.net)์์ GraphQL ์ ๋ฌธ๊ฐ๋ค๊ณผ ์ํตํด๋ณด์ธ์. ์ค๋ฌด ๊ฒฝํ์ด ํ๋ถํ ๊ฐ๋ฐ์๋ค์ ์กฐ์ธ์ ์ ๋ง ๊ฐ์ง ์์ฐ์ด ๋ ๊ฑฐ์์!
์, ์ด์ ์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ์ GraphQL์ ๋์ ํด๋ณผ ์ค๋น๊ฐ ๋์ จ๋์? ๐
Happy Coding! ์ฆ๊ฑฐ์ด ๊ฐ๋ฐ ๋์ธ์! ๐โจ
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.