GraphQL에 대해 알아보자(1)

Jimin Lee·2025년 1월 10일

GraphQL

목록 보기
1/3
post-thumbnail

아마 개발자 대부분은 RESTful API를 경험했을 것이라 생각한다.
RESTful API에서는 API별로 응답값이 고정되어 있다. 그리고 이 응답값은 다양한 경우에 대응하기 위해 최대한 관련있는 많은 데이터가 들어간다. 그래서 프론트엔드 개발자 입장에서는 사실 필요없는 데이터가 들어있는 경우도 많다. 필요한 경우 별도의 API가 만들어지긴 하나, 그만큼 추가적인 코스트가 든다.

아마 많은 프론트엔드 개발자가 '화면에 필요한 데이터만 받는' API가 있으면 좋겠다는 생각이 든 적이 있을 것이다. GraphQL은 이 문제를 해결해준다.



GraphQL이란

GraphQL(gql)이란 API를 위한 쿼리 언어(like sql)이다. 즉 클라이언트에서 gql이란 쿼리를 날리면 서버에서 해당 쿼리를 해석하여 결과를 처리하고 다시 클라이언트로 보내준다.



REST와의 비교


(출처: https://josipmisko.com/posts/rest-api-vs-graphql)

RESTful API의 경우 endpoint가 여러 개 존재하고 그 endpoint마다 DB SQL 쿼리가 달라진다.
예를 들어 전체 포스팅의 정보를 가져오는 API와 포스팅의 태그 정보만을 가져오는 API는 서로 다른 endpoint를 가지고 서로 다른 SQL 쿼리에 맵핑된다.

GET /post => select * from POST
GET /post/tags => select tags from POST 

하지만 GraphQL은 하나의 endpoint만 존재한다. 대신 쿼리 조합을 통해 데이터 조합을 결정한다.

// post의 title
{
  posts {
    title
}
// post의 tags의 이름
{
  posts {
    tags {
      name
    }
}

BFF와의 관계

여기까지 동작 원리를 살펴봤을 때 BFF(Backend-For-Frontend)와 동작 원리가 유사하다. 만약 서버가 하나라면 GraphQL을 사용하여 BFF를 구현할 수 있다. 하지만 MSA가 적용되어 있다면 서버가 나눠지면서 스키마도 나눠지므로 GraphQL 역시 BFF로서의 역할이 불가능하다. 이 경우 GraphQL Federation을 사용하여 해결할 수 있다. 여기선 GraphQL Federation에 대해 자세히 설명하진 않겠다. GraphQL Federation을 잘 활용하고 있다는 넷플릭스의 유튜브를 참고하자



GraphQL 사용하기

요청과 응답

GraphQL에서 요청-응답의 구조는 직관적이고 간단하다.

좌측이 요청이고 우측이 응답값이다. 성공하면 {data: Object}의 형태로, 실패하면 {errors: {message, locations}[]} 형태로 response가 전달된다. 그리고 응답값에서 보이듯이 요청한 name, friends에 해당하는 결과를 정확히 반환한다.
당연히 모든 쿼리가 다 되는 것은 아니고, 스키마에 정의된 쿼리를 기준으로 호출이 가능하다.


스키마와 타입 시스템

스키마

GraphQL의 스키마는 DB의 스키마와 유사한 역할을 한다. 이 스키마는 서버에서 정의되며, 클라이언트는 스키마를 토대로 쿼리를 호출한다.

type Post {
    id: ID!
    title: String!
    creationDate: Date!
    content: String!
	tags: [Category!]!
}

type Query {
    post(id: ID!): Post!;
    posts(): [Post!]!
}

type Mutation {
    addPost(newPostData: PostInput!): Post!
    updatePost(id: ID!, updatedData: PostInput!): Post!
    removePost(id: ID!): Boolean!
}

input PostInput {
    title: String!
    content: String!
}

타입 시스템

GraphQL은 독자적인 타입 시스템을 가지고 있다. 그 중에서도 query 타입과 mutation 타입은 특수한 타입이다.

type Query {
    post(id: String!): Post!;
	...
}

type Mutation {
    addPost(newPostData: PostInput!): Post!
	...
}

이 둘은 쿼리의 entry point 역할을 한다. 즉, 클라이언트에서

{
	post(id:'1000'){
    	title
    }
}

이런 query operation를 보내서 정상적인 응답값을 받을 수 있는 이유가 스키마의 Query에 정의되어 있기 때문이다. mutation도 마찬가지로 스키마에 정의되어 있어야 사용가능하다.

나머지 타입들은 타입스크립트와 유사하나 좀 더 다양한 타입을 지원하므로 나머지 자세한 내용은 공식 문서를 참고하는 것이 좋다.

type Post {
    id: ID!
    title: String!
    creationDate: Date!
    content: String!
	tags: [Category!]!
}
  • 오브젝트 타입 : Category
  • 필드 : id, title, creationDate, content, tags
  • 스칼라 타입 : String, ID, Int 등
  • 느낌표(!) : 필수 값을 의미(non-nullable)
  • 대괄호([, ]) : 배열을 의미(array)

Query & Mutation

위에서 제시된 스키마를 토대로 클라이언트에서 요청을 보내보자.

field

만약 특정 id에 해당하는 post의 title을 받아오는 gql을 작성하고자 한다면

{
	post(id: '1000'){
    	title
    }
}

이런 식으로 사용할 수 있다. 여기서 title은 필드라고 부른다.


operation type and name

query PostTitle {
	post(id: '1000'){
    	title
    }
}

여기서 사용된 query 키워드가 operation type이고 PostTitle은 operation name이라고 부른다. operation type은 query, mutation, subscription이 있으며 이름에서 예상되듯이 query는 R, mutation은 CUD 작업에 해당한다.
operation name은 사용자가 지정하는 함수 이름으로 디버깅을 위해 사용한다.

모든 graphQL 스키마는 query operation을 지원하므로 변수가 없는 쿼리에 한해서 query 키워드없이 아래 예시처럼 사용가능하다.

{
	post(id: '1000'){
    	title
    }
}

variables

변수 사용을 위해선 operation type과 operation name을 반드시 명시해야 한다.

 query getPost($id: ID!) {
      book(id: $id) {
        title
      }
    }

그리고 변수로 사용할 값에 $를 붙인다. 그리고 공식문서에서 변수를 할당하기 위해 query string에 직접적으로 문자열을 넣는 것은 지양하고 있다. 대신 요청 시에 {variables: Object}와 같은 JSON의 형태로 따로 전달할 것을 권장한다.

// ApolloClient 사용
export const getPostQuery = async (id: string) => {
  
  /** Wrong: 아래와 같은 직접적인 문자열 넣기는 지양한다.
  const useQuery = gql`
    {
      book(id: ${id}) {
        title
      }
    }
  `;
  **/
  
  const useQuery = gql`
    query getPost($id: ID!) {
      post(id: $id) {
        title
		content
      }
    }
  `;

  /* Good: variables를 key로 별도의 JSON을 전달한다. */
  return await query({
    query: useQuery,
    variables: {
      id,
    },
  });
};

mutations

query와 동일한 문법이 적용된다.

mutation CreatePost($post: PostInput!){
    addPost(newPostData: $post) {
      title
      content
    }
}

마찬가지로 variables를 따로 넣어줘야 한다.

// variables
{
	"post": {
    	"title": "안녕하세요",
      	"content": "반갑습니다"
    }
}

query와 mutation 사이의 큰 차이점은 쿼리 필드는 병렬로 실행되지만 뮤테이션 필드는 순차적으로 실행된다는 점이다.

즉, 하나의 요청에서 mutation을 2번 실행하면 1번 요청 실행 완료 => 2번 요청 실행이라는 뜻이다.

// operation
mutation {
	firstPost: removePost(id: '1')
    secondPost: removePost(id: '2')
}

이 경우 secondPost가 실행되기 위해선 firstPost가 실행되어야 한다.



동작 원리와 resolver

DB에서 데이터를 가져올 때는 SQL이라는 추상화된 명령어를 사용해서 개발자가 구체적인 동작 과정을 알지 못해도 데이터를 가져올 수 있다. GQL 역시 서버에 날리는 쿼리라고 생각한다면 클라이언트는 구체적인 쿼리의 동작 과정에 대해서는 알지 못해도 된다. 하지만 GQL을 받아서 처리하는 쪽에서는(주로 서버) 데이터를 가져오는 과정을 직접 구현해야 한다! GQL 쿼리문 파싱은 대부분의 라이브러리에서 처리해주지만 데이터를 가져오는 구체적인 과정은 resolver 함수가 담당하며 이 함수는 개발자가 직접 구현해야 한다.

GraphQL의 동작 원리를 살펴보면, 쿼리에 정의된 필드마다 resolver 함수가 호출된다. 이 resolver 함수는 만약 필드가 scalar 값(문자열, 숫자와 같은 primitive type)이라면 resolver 함수는 종료된다. 하지만 필드가 object value라면 query는 해당 object의 필드로 넘어간다. 이 과정은 leaf node 즉 scalar나 enum type을 만날 때까지 계속 된다.

{
	query {
     person(id: "24601") {
       name
       occupation
       location {
         name
         population
       }
     }
 }
}


(출처: https://relay.dev/docs/tutorial/graphql/)

이런 동작 원리를 보면 GraphQL의 이름에 왜 graph가 들어갔는지 알 수 있다. 데이터를 그래프로 보고 데이터를 찾는 과정이 DFS와 유사하기 때문이다.

자세한 동작 원리는 나중 포스팅에서 서버를 실제 구현해보면서 살펴보도록 한다.



GraphQL의 장점

앞서 살펴본 내용을 통해 GraphQL의 장점을 정리해보자

  • 필요한 데이터만 요청 가능하다.
    프론트엔드 개발자 입장에서 원하는 값만 내려받을 수 있어서 편하고, 서버 입장에서는 부하를 줄일 수 있다.

  • 단일 endpoint로 모든 리소스에 접근 가능하다.
    RESTful처럼 리소스 추가될 때마다 새로운 endpoint를 만들지 않아도 된다.

  • API 명세서를 만드는데 소모되는 비용이 적다.
    기존 RESTful 기반 API 명세서에서는 변경사항 관리를 꾸준히 해줘야 했다. 하지만 GraphQL에서는 스키마 자체가 명세서이고 Introspection 기능을 사용하면 스키마 정보를 실시간으로 공유할 수 있다.

  • 버전 관리를 단순화할 수 있다.
    RESTful은 리소스나 응답 구조가 크게 변경되면 새로운 버전을 만들어 v1, v2 등 버전으로 운영해야 하는 경우가 많다.
    반면에 GraphQL은 스키마 내에 기존 필드를 보존한 채로 새로운 필드를 추가하여 확장이 가능하여 버전을 여러 개 관리할 필요성이 줄어든다.



GraphQL를 도와주는 라이브러리

여기 들어가면 언어, 프레임워크 태그를 걸어서 GraphQL을 도와주는 라이브러리를 찾을 수 있다.
FE의 경우 상위 랭크된 라이브러리가

  • Apollo
  • Relay
  • AWS Amplify
  • Urql
    이렇게 4개가 있는데 Apollo와 Urql을 다음 포스팅에서 살펴보고자 한다.
    Amplify는 Nextjs 13.4.13 버전부터 동작하지 않는다고 하고 Relay도 Next app router 버전을 지원하지 않는다고 한다.

참고

공식 홈페이지
카카오 테크 - GraphQL 개념 설명
graphql 문서 한국어 버전
https://blog.doctor-cha.com/integrating-graphql-services-with-graphql-federation

0개의 댓글