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/falseID: 고유 식별자 (문자열로 직렬화됨)
커스텀 스칼라도 정의할 수 있다. 날짜 타입이 없어서 보통 DateTime 같은 걸 직접 만들어 쓴다.
REST vs GraphQL
| REST | GraphQL | |
|---|---|---|
| 엔드포인트 | 여러 개 | 하나 |
| 응답 구조 | 서버가 결정 | 클라이언트가 결정 |
| 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 대체가 아니라 상황에 맞게 선택