Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby로 배우는 GraphQL 완벽 입문 가이드

개발자들이 GraphQL의 놀라운 장점을 극찬하는 이야기를 들어보셨을 겁니다. 이 시리즈는 기술을 직접 사용해 보면서 배우는 방식을 지향하는데요, 이번 글에서는 GraphQL을 활용한 예제 애플리케이션을 단계별로 살펴보겠습니다.

GraphQL이란 무엇인가?

GraphQL은 API를 구축하기 위한 쿼리 언어이자 런타임입니다. 개발 스택에서 REST API와 비슷한 위치를 차지하지만, 훨씬 더 유연하다는 점이 특징입니다. REST와 달리 GraphQL은 응답 형식과 내용을 클라이언트 측에서 직접 지정할 수 있습니다. SQL의 SELECT 문으로 조회 결과를 지정할 수 있는 것처럼, GraphQL도 반환될 JSON 데이터 구조를 클라이언트가 결정할 수 있습니다.

SQL에 비유하자면, GraphQL에는 WHERE 절이 없습니다. 대신 응답 데이터를 제공할 애플리케이션 객체의 필드를 식별하는 방식으로 동작합니다.

이름 그대로 GraphQL은 애플리케이션을 하나의 데이터 그래프로 모델링합니다. 자신의 애플리케이션을 그래프로 생각해 본 적이 없더라도 걱정하지 마세요. 사실 대부분의 시스템에서 이미 사용되는 모델입니다. JSON으로 표현 가능한 데이터는 방향성 있는 그래프(directed graph)이기 때문입니다. 애플리케이션이 API를 통해 그래프 모델을 노출한다고 관점을 잡으면, GraphQL을 훨씬 쉽게 이해할 수 있습니다.

애플리케이션에서 GraphQL 활용하기

GraphQL을 추상적으로 설명했으니, 이제 실제로 GraphQL을 사용하는 애플리케이션을 만들어 보겠습니다. 가장 먼저 할 일은 데이터 모델, 즉 그래프를 정의하는 것입니다. 저는 최근 일렉트릭 업라이트 베이스 연주와 음악 전반을 공부하는 새로운 취미를 갖게 되었는데요, 데모 앱 주제를 고민하다 보니 자연스럽게 음악 관련 예제가 떠올랐습니다.

예제의 객체 타입은 Artist(아티스트)Song(노래) 두 가지입니다. 한 명의 아티스트는 여러 곡을 가질 수 있고, 각 곡은 특정 아티스트에 속합니다. 각 객체 타입에는 name 같은 속성이 포함됩니다.

API 정의하기

GraphQL은 SDL(Schema Definition Language)을 사용합니다. GraphQL 명세에서는 이를 '타입 시스템 정의 언어(type system definition language)'라고 부르기도 합니다. 이론적으로 GraphQL 타입은 어떤 언어로든 정의할 수 있지만, 가장 널리 쓰이는 공용 언어는 SDL입니다. 그럼 SDL로 API를 정의해 보겠습니다.

type Artist {
  name: String!
  songs: [Song]
  origin: [String]
}

type Song {
  name: String!
  artist: Artist
  duration: Int
  release: String
}

ArtistString 타입의 name 필드를 가집니다. 느낌표(!)는 해당 필드가 non-null, 즉 반드시 값이 존재해야 함을 의미합니다. songs는 Song 객체의 배열이며, origin은 문자열 배열입니다. Song도 구조가 비슷한데, 한 가지 특이한 필드가 있습니다.

release 필드는 원래 날짜나 시간 타입이 적합하지만, GraphQL의 기본 타입에는 이런 타입이 정의되어 있지 않습니다. 서로 다른 GraphQL 구현체 간의 완전한 호환성을 위해 우선 String을 사용했습니다. 다만 우리가 사용할 GraphQL 구현체에는 Time 타입이 추가되어 있으므로, release 필드를 Time 타입으로 바꿔 API를 더 정확하게 문서화하겠습니다. 실제 반환값은 문자열이지만, 타입을 Time으로 선언하면 API의 의도가 명확해집니다.

  release: Time

마지막 단계는 객체를 하나 이상 가져오는 방법을 정의하는 것입니다. 이를 '루트(root)', 쿼리의 경우 '쿼리 루트(query root)'라고 부릅니다. 우리의 루트에는 아티스트 이름을 인자로 받는 artist라는 단일 필드(메서드)만 존재합니다.

type Query {
  artist(name: String!): Artist
}

애플리케이션 작성하기

이제 이 스키마를 애플리케이션에서 어떻게 활용하는지 살펴보겠습니다. Ruby용 GraphQL 서버 구현체는 여러 가지가 있습니다. 일부 방식은 앞서 작성한 SDL을 Ruby 코드로 변환해야 하지만, 제가 직접 만든 HTTP 서버인 Agoo는 SDL 정의를 그대로 사용하고 Ruby 코드도 순수한 기본 Ruby로 작성할 수 있습니다. 그래서 여기서는 Agoo를 사용하겠습니다.

주목할 점은 Ruby 클래스 이름이 GraphQL 타입 이름과 일치한다는 것입니다. 클래스명과 타입명을 맞춰두면 불필요한 복잡성 없이 깔끔하게 매핑됩니다.

class Artist
  attr_reader :name
  attr_reader :songs
  attr_reader :origin
 
  def initialize(name, origin)
    @name = name
    @songs = []
    @origin = origin
  end
 
  # Song 객체가 자신을 아티스트에 추가할 때만 사용됩니다.
  def add_song(song)
    @songs << song
  end
end
 
class Song
  attr_reader :name     # string
  attr_reader :artist   # reference
  attr_reader :duration # integer
  attr_reader :release  # time
 
  def initialize(name, artist, duration, release)
    @name = name
    @artist = artist
    @duration = duration
    @release = release
    artist.add_song(self)
  end
end

Ruby 클래스의 메서드가 GraphQL 필드와 대응됩니다. 메서드는 인자가 없거나 args={}만 받도록 되어 있습니다. GraphQL API가 기대하는 형식이 바로 이것이며, 이 예제 역시 그 규칙을 따릅니다. initialize 메서드는 예제용 데이터를 세팅하는 용도로 사용되는데, 곧 확인하게 될 것입니다.

다음으로 쿼리 루트 클래스도 정의해야 합니다. SDL의 Query 루트 타입과 일치하는 artist 메서드에 주목하세요. 또한 artists를 위한 attr_reader도 추가했습니다. SDL 문서의 Query 타입에 해당 필드만 추가하면 이 값도 API로 노출됩니다.

class Query
  attr_reader :artists
 
  def initialize(artists)
    @artists = artists
  end
 
  def artist(args={})
    @artists[args['name']]
  end
end

GraphQL 루트(쿼리 루트와 혼동하지 마세요)는 쿼리 루트 바로 위에 위치합니다. GraphQL 명세에 따르면 이 루트는 선택적으로 세 개의 필드를 가질 수 있으며, 여기서 Ruby 클래스는 query 필드만 구현했습니다. 초기화 코드에서는 제가 즐겨 듣는 뉴질랜드 인디 밴드의 데이터를 로드합니다.

class Schema
  attr_reader :query
  attr_reader :mutation
  attr_reader :subscription
 
  def initialize()
    # 테스트용 데이터 설정.
    artist = Artist.new('Fazerdaze', ['Morningside', 'Auckland', 'New Zealand'])
    Song.new('Jennifer', artist, 240, Time.utc(2017, 5, 5))
    Song.new('Lucky Girl', artist, 170, Time.utc(2017, 5, 5))
    Song.new('Friends', artist, 194, Time.utc(2017, 5, 5))
    Song.new('Reel', artist, 193, Time.utc(2015, 11, 2))
    @artists = {artist.name => artist}
 
    @query = Query.new(@artists)
  end
end

마지막 설정 단계는 구현체마다 조금씩 다릅니다. 여기서는 서버가 /graphql HTTP 요청 경로를 처리하도록 핸들러를 등록한 뒤 서버를 시작합니다.

Agoo::Server.init(6464, 'root', thread_count: 1, graphql: '/graphql')
Agoo::Server.start()

그런 다음 GraphQL 구현체를 앞서 정의한 SDL($songs_sdl)로 설정하고, 서버가 요청을 처리하는 동안 애플리케이션은 대기 상태로 유지합니다.

Agoo::GraphQL.schema(Schema.new) {
  Agoo::GraphQL.load($songs_sdl)
}
sleep

이 예제의 전체 코드는 GitHub에서 확인할 수 있습니다.

API 사용해 보기

API를 테스트하려면 웹 브라우저, Postman 또는 curl을 사용할 수 있습니다.

시도해 볼 GraphQL 쿼리는 다음과 같습니다.

{
  artist(name:"Fazerdaze") {
    name
    songs{
      name
      duration
    }
  }
}

이 쿼리는 Fazerdaze라는 이름의 아티스트를 요청하고, JSON 문서로 namesongs를 반환받습니다. 각 Song 객체에 대해서는 곡의 nameduration이 JSON 객체로 반환됩니다. 출력 결과는 다음과 같아야 합니다.

{
  "data": {
    "artist": {
      "name": "Fazerdaze",
      "songs": [
        {
          "name": "Jennifer",
          "duration": 240
        },
        {
          "name": "Lucky Girl",
          "duration": 170
        },
        {
          "name": "Friends",
          "duration": 194
        },
        {
          "name": "Reel",
          "duration": 193
        }
      ]
    }
  }
}

쿼리에서 선택적인 공백을 제거한 후, curl로 HTTP GET 요청을 보내면 동일한 결과를 받을 수 있습니다.

curl -w "\n" 'localhost:6464/graphql?query=\{artist(name:"Fazerdaze")\{name,songs\{name,duration\}\}\}&indent=2'

쿼리에서 durationrelease로 바꿔서 실행해 보세요. Ruby의 Time 객체가 JSON 문자열로 변환되는 것을 확인할 수 있습니다.

마무리하며

GraphQL로 재미있게 실험해 볼 수 있었기를 바랍니다. 이 글을 따라오시면서 유용한 내용을 배워갔길 희망합니다. 좋은 독자여서 감사합니다. Ruby에 대해 더 이야기 나누고 싶으시다면, 굿즈 판매 중인 바에서 만나요!