GraphQL 기본 개념

GraphQL은 Facebook이 만든 API 쿼리 언어다. 서버 사이드 런타임으로, 정의한 타입 시스템을 기반으로 쿼리를 실행한다. 특정 데이터베이스에 종속되지 않고, 기존 코드와 데이터를 그대로 활용할 수 있다.

JavaScript, Python, Go, Java 등 다양한 언어에서 GraphQL을 지원한다. REST API와 다르게 클라이언트가 필요한 데이터만 정확히 요청할 수 있다.

REST의 한계

REST로 유저 정보와 게시글을 가져온다고 해보자.

GET /users/1
GET /users/1/posts

두 번 요청해야 한다. 아니면 백엔드에서 /users/1?include=posts 같은 걸 만들어줘야 한다. 화면마다 필요한 데이터가 다르면 엔드포인트가 계속 늘어난다.

또 다른 문제는 over-fetching이다. 유저 이름만 필요한데 API가 이메일, 주소, 가입일 등 모든 필드를 다 내려준다.

GraphQL의 해결 방식

GraphQL은 엔드포인트가 하나다. 클라이언트가 쿼리로 원하는 데이터 구조를 명시한다.

query {
  user(id: 1) {
    name
    posts {
      title
    }
  }
}

응답:

{
  "data": {
    "user": {
      "name": "홍길동",
      "posts": [
        { "title": "첫 번째 글" },
        { "title": "두 번째 글" }
      ]
    }
  }
}

쿼리와 결과의 모양이 똑같다. 요청한 필드만 정확히 오고, 한 번의 요청으로 연관된 데이터도 같이 가져올 수 있다.

기본 문법

Query (조회)

데이터를 읽을 때 쓴다.

query GetUsers {
  users {
    id
    name
  }
}

query 키워드와 operation name(GetUsers)은 생략 가능하다. 근데 디버깅할 때 서버 로그에서 어떤 요청인지 구분하기 쉬우니까 붙여주는 게 좋다.

{
  users {
    id
    name
  }
}

Arguments (인자)

특정 데이터를 필터링할 때 인자를 넘긴다.

{
  human(id: "1000") {
    name
    height
  }
}

Variables (변수)

하드코딩 대신 변수를 쓸 수 있다. 클라이언트에서 쿼리를 재사용할 때 매번 새 쿼리를 만들 필요 없이 변수만 바꿔서 넘기면 된다.

query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

변수 값은 별도로 전달한다:

{
  "episode": "JEDI"
}

Fragment (재사용 단위)

여러 쿼리에서 같은 필드를 반복적으로 가져올 때 일일이 쓰기 귀찮다. Fragment로 묶어서 spread operator(...)로 넣으면 된다.

fragment UserFields on User {
  id
  name
  email
}

query {
  user(id: "1") {
    ...UserFields
  }
  users {
    ...UserFields
  }
}

Fragment 안에서 변수도 쓸 수 있다:

query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}

Directives (지시어)

쿼리 구조를 동적으로 바꿀 때 쓴다.

  • @include(if: Boolean) - true면 해당 필드 포함
  • @skip(if: Boolean) - true면 해당 필드 제외
query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}
{
  "episode": "JEDI",
  "withFriends": false
}

withFriends가 false면 friends 필드가 응답에 포함되지 않는다.

Mutation (변경)

데이터를 생성, 수정, 삭제할 때 쓴다.

mutation CreateUser($name: String!, $email: String!) {
  createUser(name: $name, email: $email) {
    id
    name
  }
}

mutation 결과로 생성된 데이터를 바로 받을 수 있다.

복잡한 입력은 input type을 쓴다:

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "최고의 에피소드"
  }
}

ReviewInput은 input object type이다. 일반 object type과 달리 인자로 넘길 수 있다.

Schema 정의

서버에서 어떤 데이터를 제공할지 스키마로 정의한다.

type User {
  id: ID!
  name: String!
  email: String
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String
  author: User!
}

input ReviewInput {
  stars: Int!
  commentary: String
}

type Query {
  user(id: ID!): User
  users: [User!]!
  post(id: ID!): Post
}

type Mutation {
  createUser(name: String!, email: String): User!
  createPost(title: String!, content: String, authorId: ID!): Post!
  createReview(episode: Episode!, review: ReviewInput!): Review
}

!는 non-null을 의미한다. [Post!]!는 배열 자체도 null이 아니고, 배열 안의 요소도 null이 아니라는 뜻이다.

타입 시스템

GraphQL은 강타입 시스템을 가지고 있다.

스칼라 타입

  • Int: 정수
  • Float: 부동소수점
  • String: 문자열
  • Boolean: true/false
  • ID: 고유 식별자 (문자열로 직렬화됨)

커스텀 스칼라도 정의할 수 있다. 날짜 타입이 없어서 보통 DateTime 같은 걸 직접 만들어 쓴다.

REST vs GraphQL

RESTGraphQL
엔드포인트여러 개하나
응답 구조서버가 결정클라이언트가 결정
Over-fetching발생 가능없음
버전 관리/v1, /v2스키마 진화
캐싱HTTP 캐싱 쉬움별도 구현 필요

GraphQL이 항상 좋은 건 아니다. 단순한 CRUD API면 REST가 더 간단하다. 캐싱도 REST가 HTTP 레벨에서 쉽게 된다. 여러 클라이언트(웹, 앱)가 각각 다른 데이터를 필요로 할 때 GraphQL이 유리하다.

시작하기

Node.js에서는 Apollo Server를 많이 쓴다.

npm install @apollo/server graphql
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const typeDefs = `
  type Query {
    hello: String
  }
`;

const resolvers = {
  Query: {
    hello: () => 'Hello, GraphQL!'
  }
};

const server = new ApolloServer({ typeDefs, resolvers });

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 }
});

console.log(`Server ready at ${url}`);

http://localhost:4000에 접속하면 Apollo Studio가 뜨고, 거기서 쿼리를 테스트할 수 있다.

정리

  • GraphQL은 클라이언트가 필요한 데이터만 요청하는 쿼리 언어
  • Query로 조회, Mutation으로 변경
  • Variables로 쿼리 재사용, Fragment로 필드 재사용
  • Directives로 동적 쿼리 구조 변경
  • 스키마로 타입을 정의하고, Resolver로 실제 데이터를 반환
  • REST 대체가 아니라 상황에 맞게 선택