์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ Ruby on Rails์™€ GraphQL๋กœ ๋งŒ๋“œ๋Š” ์ฐจ์„ธ๋Œ€ API: ๊ฐœ๋ฐœ์˜ ๋ฏธ๋ž˜๋ฅผ ์—ฌ๋Š” ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿš€ Ruby on Rails์™€ GraphQL๋กœ ๋งŒ๋“œ๋Š” ์ฐจ์„ธ๋Œ€ API: ๊ฐœ๋ฐœ์˜ ๋ฏธ๋ž˜๋ฅผ ์—ฌ๋Š” ์™„๋ฒฝ ๊ฐ€์ด๋“œ

REST API๋Š” ์ด์ œ ์˜›๋‚  ์ด์•ผ๊ธฐ? ํ˜„๋Œ€์ ์ธ API ๊ฐœ๋ฐœ์˜ ์ƒˆ๋กœ์šด ํŒจ๋Ÿฌ๋‹ค์ž„์„ ๋งŒ๋‚˜๋ณด์„ธ์š”!

Rails Graph QL RESTful API Traditional Approach GraphQL API Modern Solution โšก

๐ŸŽฏ ์™œ ์ง€๊ธˆ GraphQL์ธ๊ฐ€?

์•ˆ๋…•ํ•˜์„ธ์š”, ๊ฐœ๋ฐœ์ž ์—ฌ๋Ÿฌ๋ถ„! ๐Ÿ˜Š ์˜ค๋Š˜์€ ์ •๋ง ํฅ๋ฏธ์ง„์ง„ํ•œ ์ฃผ์ œ๋ฅผ ๊ฐ€์ง€๊ณ  ์™”์–ด์š”. ๋ฐ”๋กœ Ruby on Rails์™€ GraphQL์˜ ํ™˜์ƒ์ ์ธ ์กฐํ•ฉ์— ๋Œ€ํ•œ ์ด์•ผ๊ธฐ์ž…๋‹ˆ๋‹ค.

์—ฌ๋Ÿฌ๋ถ„, ํ˜น์‹œ ์ด๋Ÿฐ ๊ฒฝํ—˜ ์žˆ์œผ์‹ ๊ฐ€์š”? REST API๋ฅผ ๋งŒ๋“ค๋‹ค๊ฐ€ "์•„, ์ด ๋ฐ์ดํ„ฐ๋งŒ ํ•„์š”ํ•œ๋ฐ ์™œ ์ด๋ ‡๊ฒŒ ๋งŽ์€ ์ •๋ณด๋ฅผ ๋ฐ›์•„์•ผ ํ•˜์ง€?" ๐Ÿค” ํ•˜๊ณ  ๊ณ ๋ฏผํ–ˆ๋˜ ์ˆœ๊ฐ„ ๋ง์ด์—์š”. ๋˜๋Š” ํ•˜๋‚˜์˜ ํ™”๋ฉด์„ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์—ฌ๋Ÿฌ ๊ฐœ์˜ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ–ˆ๋˜ ๊ทธ ๋‹ต๋‹ตํ•จ...

๋ฐ”๋กœ ์ด๋Ÿฐ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋“ฑ์žฅํ•œ ๊ฒƒ์ด GraphQL์ž…๋‹ˆ๋‹ค! 2015๋…„ ํŽ˜์ด์Šค๋ถ(ํ˜„ Meta)์ด ์˜คํ”ˆ์†Œ์Šค๋กœ ๊ณต๊ฐœํ•œ ์ดํ›„, Netflix, Shopify, GitHub, Airbnb ๋“ฑ ๊ธ€๋กœ๋ฒŒ ํ…Œํฌ ๊ธฐ์—…๋“ค์ด ์•ž๋‹คํˆฌ์–ด ๋„์ž…ํ•˜๊ณ  ์žˆ๋Š” ์ฐจ์„ธ๋Œ€ API ์ฟผ๋ฆฌ ์–ธ์–ด์ฃ .

๐Ÿ’ก ์žฌ๋ฏธ์žˆ๋Š” ์‚ฌ์‹ค: GraphQL์€ ์‚ฌ์‹ค ํŽ˜์ด์Šค๋ถ์ด ๋ชจ๋ฐ”์ผ ์•ฑ ๊ฐœ๋ฐœ ์ค‘ ๊ฒช์—ˆ๋˜ ๋„คํŠธ์›Œํฌ ํšจ์œจ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด 2012๋…„๋ถ€ํ„ฐ ๋‚ด๋ถ€์ ์œผ๋กœ ๊ฐœ๋ฐœํ•˜๊ธฐ ์‹œ์ž‘ํ–ˆ์–ด์š”. ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์ด ์–ผ๋งˆ๋‚˜ ์ค‘์š”ํ•œ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์‚ฌ๋ก€์ฃ !

๐Ÿ” 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
๐Ÿ’ก Pro Tip: 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์˜ Mutation์€ REST์˜ POST, PUT, DELETE์™€ ์œ ์‚ฌํ•˜์ง€๋งŒ, ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜ํ•  ์ˆ˜ ์žˆ๊ณ , ์—ฌ๋Ÿฌ ์ž‘์—…์„ ํ•˜๋‚˜์˜ ์š”์ฒญ์œผ๋กœ ๋ฌถ์„ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์—์„œ ๋” ์œ ์—ฐํ•ฉ๋‹ˆ๋‹ค.

๐Ÿš€ ์‹ค์ „ ํ™œ์šฉ: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค

๊ธฐ๋ณธ๊ธฐ๋ฅผ ์ตํ˜”๋‹ค๋ฉด, ์ด์ œ 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์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋“  ์Šคํ‚ค๋งˆ ์ •๋ณด๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค(Introspection). ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ด๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ฑฐ๋‚˜ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์—๊ฒŒ๋งŒ ํ—ˆ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค!

๐Ÿ“„ ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ตฌํ˜„ํ•˜๊ธฐ

๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ๋Š” ํŽ˜์ด์ง€๋„ค์ด์…˜์ด ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. 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
๐Ÿ’ก ์บ์‹ฑ ํŒ: GraphQL ์ฟผ๋ฆฌ ์ž์ฒด๋ฅผ ํ•ด์‹œํ™”ํ•ด์„œ ์บ์‹œ ํ‚ค๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์–ด์š”. ๊ฐ™์€ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐ˜๋ณต๋˜๋ฉด ์บ์‹œ๋œ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”๋กœ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์ฃ !

๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋ถ„์„

์–ด๋–ค ์ฟผ๋ฆฌ๊ฐ€ ๋А๋ฆฐ์ง€, ์–ด๋–ค ํ•„๋“œ๊ฐ€ ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š”์ง€ ์•Œ์•„์•ผ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ๊ฒ ์ฃ ? 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์€ POST ์š”์ฒญ์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์ผ๋ฐ˜์ ์ธ HTTP ์บ์‹ฑ์ด ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋Š” DDoS ๊ณต๊ฒฉ์— ๋” ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ ๋ฐ˜๋“œ์‹œ Rate Limiting์„ ๊ตฌํ˜„ํ•˜์„ธ์š”!

๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ์™€์˜ ์™„๋ฒฝํ•œ ์กฐํ™”

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
๐Ÿ’ก ํ…Œ์ŠคํŒ… ํŒ: GraphQL ์ฟผ๋ฆฌ๋ฅผ ๋ณ„๋„์˜ .graphql ํŒŒ์ผ๋กœ ๊ด€๋ฆฌํ•˜๋ฉด ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ์—์„œ ๋™์ผํ•œ ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๊ธฐ ์ข‹์•„์š”!

๐ŸŒŸ ์‹ค์ œ ํ”„๋กœ์ ํŠธ ์‚ฌ๋ก€ ์—ฐ๊ตฌ

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ฐฐ์› ์œผ๋‹ˆ, ์ด์ œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋˜๋Š”์ง€ ์‚ดํŽด๋ณผ๊นŒ์š”? ๐ŸŽฏ

๐Ÿ“ฑ ์†Œ์…œ ๋ฏธ๋””์–ด ํ”Œ๋žซํผ

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 ์„œ๋น„์Šค๋ฅผ ํ•˜๋‚˜๋กœ ํ†ตํ•ฉํ•˜๋Š” ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค:

Apollo Federation์˜ ์žฅ์ :

โ€ข ๊ฐ ํŒ€์ด ๋…๋ฆฝ์ ์œผ๋กœ ์Šคํ‚ค๋งˆ ๊ด€๋ฆฌ
โ€ข ๋‹จ์ผ 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 ๋„์ž…์œผ๋กœ ์‹ค์ œ๋กœ ์–ด๋–ค ์„ฑ๊ณผ๋ฅผ ๊ฑฐ๋‘˜ ์ˆ˜ ์žˆ์„๊นŒ์š”? ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์‚ดํŽด๋ด…์‹œ๋‹ค! ๐Ÿ“Š

๐Ÿ“ˆ ์ •๋Ÿ‰์  ์„ฑ๊ณผ

๐ŸŽ‰ Airbnb์˜ ์‚ฌ๋ก€:

โ€ข 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! ์ฆ๊ฑฐ์šด ๊ฐœ๋ฐœ ๋˜์„ธ์š”! ๐Ÿ˜Šโœจ

Thank You! GraphQL๊ณผ ํ•จ๊ป˜ํ•˜๋Š” ๋ฉ‹์ง„ ๊ฐœ๋ฐœ ์—ฌ์ •์„ ์‘์›ํ•ฉ๋‹ˆ๋‹ค ๐Ÿš€ ๐Ÿ’Ž โšก
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0