• 본 시리즈에서는 How to GraphQL의 Tutorial 문서들을 차례대로 번역합니다.
  • 이 글은 GraphQL Fundamentals - Core Concepts을 번역한 글입니다.
  • 오역 또는 의역이 있을 수 있습니다. 양해 부탁드리며, 수정이 필요한 부분은 댓글로 요청해주세요.

주요 개념

이번 장에서는 GraphQL을 이루는 기본적인 언어 구조에 대하여 배우게 됩니다. Type, Query, Mutation 등의 문법 사항들도 처음으로 접하게 됩니다. 또한 배우는 내용들을 직접 사용해볼 수 있도록 graphql-up에 기반한 샌드박스 환경도 준비했으니, 자유롭게 사용하시기 바랍니다.

역자 주: 본 게시글에서는 실습 환경이 제공되지 않으니 양해 부탁드립니다.

The Schea Definition Language (SDL)

GraphQL은 API의 스키마를 정의하기 위한 고유의 타입 시스템을 갖추고 있습니다. 스키마를 작성하는 문법을 스키마 정의 언어(SDL)이라고 부릅니다.

아래의 코드는 Person 이라는 간단한 타입을 정의하는 방법을 보여주는 예시입니다.

type Person {
  name: String!
  age: Int!
}

이 타입은 String 타입인 nameInt 타입인 age라는 2개의 필드를 가집니다. 타입 뒤에 붙는 !는 해당 필드가 필수값임을 뜻합니다.

타입 간의 관계를 표현하는 것도 가능합니다. 아래의 예시를 보면, 블로그 어플리케이션의 예시에서는 PersonPost와 연관 관계를 가질 수 있습니다.

type Post {
  title: String!
  author: Person!
}

반대로, 관계를 형성하는 반대편은 아래와 같이 Person 타입 상에 위치해야 합니다.

type Person {
  name: String!
  age: Int!
  posts: [Post!]!
}

위의 예시에서는 PersonPost 간에 일대다 관계가 형성되었습니다. Person에서 posts 필드는 실제로 게시글들로 구성된 배열입니다.

쿼리문을 사용하여 데이터 불러오기

REST API를 사용할 떄에는 특정 엔드포인트를 통하여 데이터를 불러오게 됩니다. 각 엔드포인트는 반환할 정보에 대하여 명확하게 정의한 구조를 가집니다. 즉, 클라이언트의 데이터 요구 사항은 클라이언트가 접근하는 URL 상에서 반영됩니다.

GraphQL이 취하는 접근 방식은 상당히 다릅니다. GraphQL은 고정된 형식의 데이터 구조를 반환하는 엔드포인트 여러 가지를 가지는 것이 아니라, 일반적으로 단 하나의 엔드포인트만을 노출시킵니다. 반환되는 데이터의 구조가 고정적이지 않고 오히려 아주 유연하기 때문에 가능한 작업입니다. 따라서 클라이언트 측에서 필요한 데이터를 결정할 수 있습니다.

즉, 필요한 데이터가 무엇인지 서버에 알려주기 위하여 클라이언트가 보다 많은 정보를 보내야 한다는 뜻입니다. 여기서 이 정보쿼리라고 부릅니다.

기본적인 쿼리문

클라이언트가 서버에 보내는 쿼리문의 예시를 살펴봅시다.

{
  allPersons {
    name
  }
}

위의 쿼리문에서 allPersons 필드는 쿼리문의 루트 필드(root field)라고 부릅니다. 루트 필드 아래에 있는 모든 값들은 쿼리문의 페이로드(payload)라고 부릅니다. 위의 쿼리문에 명시된 페이로드는 name으로 유일합니다.

위의 쿼리문은 데이터베이스에 현재 저장된 모든 사람들의 명단을 반환합니다. 아래 코드는 예시 응답값입니다.

{
  "allPersons": [
    { "name": "Johnny"},
    { "name": "Sarah"},
    { "name": "Alice"}
  ]
}

응답에서 각 사람들은 오직 name 값만을 가지고 있고 age 값은 서버로부터 반환되지 않았다는 점에 주목하세요. 이는 name 필드만이 쿼리문 상에 명시되었기 때문입니다.

만약 클라이언트에서 각 사람들의 age 값도 필요하다면, 아래와 같이 쿼리문을 살짝 수정하여서 쿼리문의 페이로드에 새로운 필드를 추가하기만 하면 됩니다.

{
  allPersons {
    name
    age
  }
}

GraphQL의 가장 큰 장점 중 하나는 중첩된 정보를 자연스럽게 질의할 수 있다는 점입니다. 예를 들어, 한 Person이 작성한 모든 posts를 불러오고 싶다면, 타입의 구조에 맞추어 아래와 같이 요청하기만 하면 됩니다.

{
  allPersons {
    name
    age
    posts {
      title
    }
  }
}

인자를 담아서 쿼리문 보내기

GraphQL에서는 각 필드스키마에 정의된 규칙에 따라 0개 이상의 인자를 가질 수 있습니다. 예를 들어, allPersons 필드는 last 매개변수를 가지는 것으로 반환되는 사람 데이터의 수를 제한할 수 있습니다. 이에 대응하는 쿼리문은 아래와 같습니다.

{
  allPersons(last: 2) {
    name
  }
}

Mutation으로 데이터 기록하기

서버에 정보를 요청하는 것 다음으로, 대부분의 어플리케이션은 현재 백엔드에 저장된 데이터를 수정할 방법을 필요로 합니다. GraphQL에서는 이러한 작업을 뮤테이션(Mutation)이라고 부릅니다. 뮤테이션에는 보통 3가지 종류가 있습니다.

  • Create: 새로운 데이터 생성
  • Update: 기존의 데이터 수정
  • Delete: 기존의 데이터 삭제

뮤테이션은 쿼리문과 동일한 문법 구조를 가지지만, 반드시 mutation 키워드와 함께 시작해야 한다는 점이 다릅니다. 아래에는 새로운 Person을 생성하는 예시가 나와있습니다.

mutation {
  createPerson(name: "Bob", age: 36) {
    name
    age
  }
}

앞서 작성한 쿼리문과 비슷하게, 뮤테이션도 루트 필드를 가집니다. 위의 예시에서는 createPerson이 그것입니다. 또한, 필드가 인자를 가지는 것도 확인할 수 있습니다. createPerson 필드는 새로운 사람의 nameage을 가리키는 2개의 인자를 받습니다.

쿼리문에서와 마찬가지로 뮤테이션에도 페이로드를 지정할 수 있는데, 새롭게 생성되는 Person 객체가 가지는 속성들 중에서 선택하여 값을 확인할 수 있습니다. 지금 다루는 예시의 경우, nameage의 정보를 요청하였습니다. 물론 예시 코드의 경우, 뮤테이션의 인자로 값을 전달하는 시점에서 이미 각각의 값을 아는 상태이므로 별로 쓸모는 없습니다. 하지만, 뮤테이션을 보냄과 동시에 정보를 불러오는 것은, 서버에 요청을 한번만 보내고도 새로운 정보를 반환받을 수 있는 것이므로 매우 강력한 도구인 셈입니다!

위의 뮤테이션에 대한 서버의 응답은 아래와 같습니다.

"createPerson" {
  "name": "Bob",
  "age": 36,
}

새로운 객체가 생성될 때, 서버가 생성해준 고유한 ID 값을 GraphQL 타입이 갖게 되는 것은 흔히 볼 수 있는 패턴입니다. Person 타입을 확장하여, 아래와 같이 id 값을 추가할 수 있습니다.

type Person {
  id: ID!
  name: String!
  age: Int!
}

자, 이제 새로운 Person이 생성되면, 뮤테이션의 페이로드를 통하여 id의 값을 직접 요청할 수 있게 됩니다. 원래는 존재하지 않았던 새로운 정보입니다.

mutation {
  createPerson(name: "Alice", age: 36) {
    id
  }
}

구독을 통한 실시간 업데이트

서버와의 실시간 통신을 통한 주요 이벤트에 대한 즉각적인 알림은 오늘날의 많은 어플리케이션에서 요구되는 중요한 기능입니다. 이런 경우를 위하여 GraphQL은 구독(Subscription)이라는 개념을 제공합니다.

클라이언트가 어떤 이벤트를 구독하면, 클라이언트는 서버와 지속적인 연결을 형성하고 유지하게 됩니다. 특정 이벤트가 발생하면, 서버는 대응하는 데이터를 클라이언트에 푸시해줍니다. 전형적인 *"요청-응답 순환"을 따르는 쿼리문 또는 뮤테이션과 달리, 구독은 클라이언트를 향한 데이터 *흐름을 나타냅니다.

구독은 쿼리문과 뮤테이션과 동일한 문법을 사용하여 작성됩니다. 아래의 예시는 Person 타입에 대하여 발생하는 이벤트를 구독합니다.

subscription {
  newPerson {
    name
    age
  }
}

클라이언트가 위와 같이 서버에 구독하면, 이 둘 간에 연결이 형성됩니다. 그러면 새로운 Person을 생성하는 새로운 뮤테이션이 이루어질 때마다 서버는 아래와 같이 새로 생성된 사람에 대한 정보를 클라이언트에 전송하게 됩니다.

{
  "newPerson": {
    "name": "Jane",
    "age": 23
  }
}

스키마 정의

쿼리문, 뮤테이션, 구독 등이 무엇인지 기본적인 이해가 이루어졌으니, 이제 앞서 배운 개념들을 모두 한데 모아서, 앞서 보여드린 예시들을 실행할 수 있는 스키마 작성법을 배우도록 합시다.

스키마(Schema)는 GraphQL API를 다룰 때에 가장 중요한 개념 중 하나입니다. API가 할 수 있는 행위를 정해주고, 클라이언트가 데이터를 요청하는 방법을 정의합니다. 서버와 클라이언트 간의 계약과도 같은 것입니다.

대부분의 경우 스키마는 GraphQL 타입들의 집합입니다. 하지만 API를 위한 스키마를 작성할 때에는, 아래와 같은 특별한 루트 타입이 존재합니다.

type Query { ... }
type Mutation { ... }
type Subscription { ... }

Query, Mutation, Subscription 타입은 클라이언트가 보내는 요청을 위한 진입점입니다. 앞서 우리가 본 allPersons 쿼리문을 클라이언트가 사용할 수 있으려면, Query 타입이 아래와 같이 작성되어야 합니다.

type Query {
  allPersons: [Person!]!
}

allPersons는 API의 루트 필드라고 부릅니다. allPersons 필드에 대하여 last 인자를 전달하는 예시를 다시 떠올려보면, Query 타입을 아래와 같이 다시 작성할 수 있습니다.

type Query {
  allPersons(last: Int): [Person!]!
}

createPerson 뮤테이션도 마찬가지로, Mutation 타입에 아래와 같이 루트 필드를 추가할 수 있습니다.

type Mutation {
  createPerson(name: String!, age: Int!): Person!
}

이 루트 필드는 새로 생성되는 Personnameage라는 2개의 인자를 전달받을 수 있다는 점에 유의하세요.

마지막으로 구독의 경우, 아래와 같이 newPerson 루트 필드를 추가할 수 있습니다.

type Subscription {
  newPerson: Person!
}

지금까지 만든 것들을 모두 종합하면, 아래와 같이 이번 장에서 다룬 모든 쿼리와 뮤테이션을 갖춘 완전한 형태의 스키마가 완성됩니다.

type Query {
  allPersons(last: Int): [Person!]!
}

type Mutation {
  createPerson(name: String!, age: Int!): Person!
}

type Subscription {
  newPerson: Person!
}

type Person {
  name: String!
  age: Int!
  posts: [Post!]!
}

type Post {
  title: String!
  author: Person!
}

더 알아보기

GraphQL의 주요 개념들을 더 배우고 싶다면 아래의 시리즈 글들을 확인해보세요.

Quiz

GraphQL 구독은 어떻게 사용되는가?

  • 이벤트 기반의 실시간 기능
  • 스키마 기반의 실시간 기능
  • GraphQL 주간 뉴스레터를 구독하는 데에 사용
  • 쿼리와 뮤테이션을 합쳐서 데이터 읽기/쓰기를 허용