GraphQL를 알아보자

Seonup·2023년 11월 12일

본 시리즈는 정재남님의 풀스택 리액트 라이브코딩 - 간단한 쇼핑몰 만들기 강의 내용을 기반으로, 추가적인 학습을 통해 습득한 지식 또는 강의 코드를 다른 방법으로 구현한 경험을 작성하고 있습니다. 강의 코드(GitHub)를 확인하세요.

GraphQL 이란?

  • GraphQL은 API를 쉽게 설계하고 호출하는 데 사용되는 쿼리 언어로, REST API를 대체하기 위한 목적으로 만들어졌다.
  • 데이터베이스에 저장된 데이터를 효율적으로 가져오기 위한 언어인 sql과 달리 gql은 웹 클라이언트가 데이터를 서버로 부터 효율적으로 가져오는 것이 목적이다. 때문에 sql 문장은 백엔드에서 작성하고 호출하는 반면, gql 문장은 주로 클라이언트 시스템에서 작성하고 호출한다.
  • 요청하는 쿼리문의 구조와 응답 내용의 구조가 거의 동일하다.

GraphQL vs REST API

GraphQLREST
정의API를 생성하고 조작하기 위한 쿼리 언어, 아키텍처 스타일 및 도구 세트클라이언트와 서버 간의 정형 데이터 교환을 정의하는 일련의 규칙
용도크고 복잡하며 서로 연관된 데이터 소스리소스가 잘 정의된 간단한 데이터 소스
데이터 액세스단일 URL 엔드포인트가 존재하며, 쿼리 조합을 통해 불러오는 데이터의 종류를 결정함. 여러 리소스를 병렬로 요청할 수 있어 서버에서 병목 현상이 발생하는 것을 방지할 수 있음.리소스를 정의하는 URL 형태의 여러 엔드포인트가 존재하며, 각 Endpoint마다 데이터베이스 SQL 쿼리가 달라짐
반환 데이터클라이언트가 정의한 유연한 구조의 데이터서버가 정의한 고정된 구조의 데이터
데이터 구조 및 정의 방법클라이언트가 필요한 자유롭게 데이터를 요청할 수 있기 때문에 GraphQL 데이터는 엄격하게 형식이 지정됨. 따라서 클라이언트는 미리 결정되고 상호 이해되는 형식으로 데이터를 수신해야 함.리소스의 모양과 크기가 서버에 의해 결정되기 때문에 REST 데이터는 형식이 약하게 지정됨. 따라서 클라이언트는 형식이 지정된 데이터를 반환할 때 해석하는 방법을 결정해야 함.
오류 검사잘못된 요청일 경우 일반적으로 스키마 구조에 의해 거부됨. 그 결과 오류 메시지가 자동으로 생성됨.반환된 데이터가 유효한지 클라이언트가 확인해야 함

GraphQL을 사용하면 클라이언트가 여러 리소스를 병렬로 요청할 수 있는 이유?

GraphQL은 여러 필드를 가지는 단일 쿼리를 사용하면 클라이언트가 여러 리소스를 한번에 요청할 수 있다. 예시를 통해 살펴보자.

// GraphQL 스키마

type User {
  id: ID!
  name: String!
  email: String!
}

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

type Query {
  posts: [Post!]!
  user(id: ID!): User!
}
// 여러 리소스를 요청할 수 있는 단일 쿼리

query {
  posts {
    id
    title
    author {
      name
    }
  }
  user(id: "123") {
    name
    email
    posts {
      title
    }
  }
}

위 예제와 같은 스키마에서 클라이언트는 posts 필드를 선택하여 모든 게시물과 해당 게시물의 작성자를 한 번에 요청할 수 있으며, user 필드를 선택하여 특정 사용자의 이름, 이메일, 사용자가 올린 모든 게시물을 요청 할 수 있다.

쿼리는 여러 필드를 포함하고 있기 때문에 한번에 다양한 리소스를 요청할 수 있다. 더불어 쿼리에서 필요한 필드만 선택할 수도 있어 데이터를 효율적으로 가져올 수 있다.

스키마란?

스키마는 GraphQL API가 제공하는 데이터 타입을 정의하는 구조체(structure)로, gql 쿼리의 진입점(EndPoint)이다. GraphQL은 스키마로 타입 시스템을 사용하여 데이터 모델을 정의한다.

  • GraphQL 쿼리에서 허용되는 모든 데이터 유형과 연결되어 있으며, 데이터를 읽기 위한 쿼리나 데이터를 수정하기 위한 뮤테이션 등을 정의한다.
  • 이는 클라이언트에게 API의 구조와 사용 가능한 쿼리에 대한 정보를 제공한다.
  • 클라이언트는 이 정보를 기반으로 요청을 작성할 수 있다.
  • GraphQL은 런타임에서 쿼리의 유효성을 검사하고, 표준화된 JSON 형식으로 결과를 반환한다.

extend 키워드

  • 기존에 정의된 스키마에 새로운 타입이나 필드를 추가하는 것이다. 예를 들어, 기존에 정의된 Query 타입에 새로운 필드를 추가할 때 extend 키워드를 사용할 수 있다.
  • 기존의 스키마를 수정할 수 있기 때문에, 스키마의 변경 사항이 바로 적용되며, 타입과 필드를 더 쉽게 관리할 수 있다.
  • 스키마의 변경이 필요한 경우에 사용한다.

예시

아래와 같이 extend 키워드를 사용하여 Query의 type을 정의해주면 Query는 cartSchema에 정의된 Query 타입의 필드와 productSchema에 정의된 Query 타입의 필드를 모두 추가하기 때문에 맨 하단의 코드와 같은 확장된 필드를 가질 수 있다.

const cartSchema = gql`
  type CartItem {
    id: ID!
    amount: String!
    product: Product!
  }

  extend type Query {
    cart: [CartItem!]
  }
`;

const productSchema = gql`
  type Product {
    id: ID!
    title: String!
    imageUrl: String!
    price: Int!
    description: String!
    createdAt: String!
  }

  extend type Query {
    products: [Product!]
    product(id: ID!): Product!
  }
`;
// Query 타입의 필드가 확장된 결과
gql`
  type Query {
    cart: [CartItem!]
    products: [Product!]
    product(id: ID!): Product!
  }
`;
  • 기존에 정의된 스키마에 의존하는 다른 스키마를 정의하는 방식
  • 이 방식은 스키마의 변경이 용이하지 않지만, 여러 스키마에서 동일한 타입을 참조해야 하는 경우에 유용하다.
  • 여러 스키마에서 동일한 타입을 참조해야 하는 경우에 사용한다.

예시

CartItem 타입에서 product 필드가 참조하고 있는 것은 다른 스키마인 productSchema의 Product 타입이다. 이처럼 Link 스키마는 다른 스키마의 타입을 참조할 수 있도록 제공한다.

const cartSchema = gql`
  type CartItem {
    id: ID!
    amount: String!
    product: Product!
  }

  extend type Query {
    cart: [CartItem!]
  }
`;

const productSchema = gql`
  type Product {
    id: ID!
    title: String!
    imageUrl: String!
    price: Int!
    description: String!
    createdAt: String!
  }

  extend type Query {
    products: [Product!]
    product(id: ID!): Product!
  }
`;

const linkSchema = gql`
  type Query {
    _: Boolean
  }

  type Mutation {
    _: Boolean
  }
`;

export default [linkSchema, productSchema, cartSchema];

operation과 종류

operation란 클라이언트에서 서버로 전송되는 GraphQL 요청의 유형을 말하며, query, mutation, subscription이 있다. 서버에서 요청하는 작업의 유형을 명확하게 나타내므로, 클라이언트와 서버 간의 통신을 효율적으로 만든다.

query

query란 서버에 데이터를 요청하는 데 사용하는 작업으로, REST API에서의 GET 메서드에 해당한다. (CRUD의 R)

mutation

mutation이란 서버의 데이터를 수정하거나 생성하는 데 사용하는 작업으로, REST API에서의 POST, PATCH, DELETE 메서드에 해당한다. (CRUD의 CUD)

subscription

데이터의 변경 사항을 구독하는 작업으로, 실시간 데이터를 가져올 수 있다.

graphql-request 패키지의 request 메서드란?

  • request 메서드: GraphQL의 query나 mutation을 지정한 HTTP EndPoint(url)로 POST 요청(변경 가능)을 보내는 메서드이다.
  • request 메서드는 Promise 객체를 반환하기 때문에 then 체이닝이나 async/await의 사용이 가능하다.
  • request 메서드가 호출될 때 GraphQL에서는 query와 variables를 모두 JSON 형태로 변환하여 전달한다.

작성 방법

request(url, query, variables);
  • url: GraphQL의 EndPoint === 요청이 전송될 서버
  • query: 서버에서 응답할 query 형태
  • variables
    • 객체 형태로, method, headers, body를 포함할 수 있다.
    • method: 작성하지 않았을 경우 기본값은 POST이며, GET, PUT, PATCH, DELETE을 작성할 수 있다.
    • body: method, headers와 달리 body라는 이름의 프로퍼티를 작성하지 않고, body로 전달할 프로퍼티 key/value 쌍의 객체로 작성한다. -> GraphQL이 서버에 요청할 때 암묵적으로 객체 내용을 HTTP 요청 바디에 담는 과정을 거친다.
    • variables에 작성한 프로퍼티는 query문에서 $를 붙여 변수명으로 참조할 수 있다.

예시

import { gql } from 'graphql-tag';
import { request } from 'graphql-request';

const query = gql`
  query GET_VALUE($id: string) {
    key1: value1,
    key2: value2,
  }
`;

const variables = {
  method: 'POST',
  headers: {
    Authorization: 'Bearer MY_TOKEN',
  },
  { id: 'ID' },
};

request('/graphql', query, variables);

부록: 에러로 이해하는 필드

장바구니 상품 삭제 로직에서 GraphQL 에러가 발생했다. delete mutation 요청시 query에 작성한 subField가 에러를 발생시킨 건데, 결론부터 말하자면 필드에 대한 이해가 부족했기 때문에 발생한 에러였다.

에러 발생 에러를 일으킨 코드와 에러 메세지를 살펴보자.

초기 구현

// server
const cartSchema = gql`
  extend type Mutation {
    deleteCart(id: ID!): ID!
  }
`;

const cartResolver: Resolvers = {
  Mutation: {
    deleteCart: (parent, { id }, { db }) => {
      const newCart = db.cart.filter((cartItem) => cartItem.id !== id);
      db.cart = newCart;
      setJSON(db.cart);
      return id;
    },
  },
};
// client
export const DELETE_CART = gql`
  mutation DELETE_CART($id: ID!) {
    deleteCart(id: $id) {
      id
    }
  }
`;

Error: Field "deleteCart" must not have a selection since type "ID!" has no subfields.: {"response":{"errors":[{"message":"Field \"deleteCart\" must not have a selection since type \"ID!\" has no subfields.","locations":[{"line":2,"column":23}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED","stacktrace":["GraphQLError: Field \"deleteCart\" must not have a selection since type \"ID!\" has no subfields.

스크린샷 2023-04-21 오후 1 54 33

에러 메세지를 읽어보면 deleteCart는 ID! 타입을 반환하기 때문에 하위 필드를 가지면 안된다고 작성되어 있다.

문제 원인 분석

에러 메세지를 보고 찾아본 결과, 이 에러는 graphql의 필드에 대해 잘못 이해하고 있었기 때문에 생긴 문제였다.

  • ❌ 나는 graphql의 field에 작성하는 중괄호({})가 반환하는 값에 대해 열거하는 코드블럭이라고 생각했다.
  • ⭕️ graphql의 field에 작성하는 중괄호({})는 코드블럭이 아닌 객체이며, 반환 받는 객체 내부에 포함된 field를 열거할 수 있다.

정리하면, 서버의 resolver에 작성한 deleteCart 필드가 반환하는 값은 객체가 아닌 ID(string) 타입의 id이며, 다른 field를 전달하지 않는다. 따라서 deleteCart 필드는 subField가 없어야 한다.

문제 해결

// client
export const DELETE_CART = gql`
  mutation DELETE_CART($id: ID!) {
    deleteCart(id: $id)
  }
`;

결론

필드는 특정 객체에 대한 쿼리 요청을 할 때 필요한 문법이다.

참고

profile
박선우

0개의 댓글