본 시리즈는 정재남님의 풀스택 리액트 라이브코딩 - 간단한 쇼핑몰 만들기 강의 내용을 기반으로, 추가적인 학습을 통해 습득한 지식 또는 강의 코드를 다른 방법으로 구현한 경험을 작성하고 있습니다. 강의 코드(GitHub)를 확인하세요.
| GraphQL | REST | |
|---|---|---|
| 정의 | API를 생성하고 조작하기 위한 쿼리 언어, 아키텍처 스타일 및 도구 세트 | 클라이언트와 서버 간의 정형 데이터 교환을 정의하는 일련의 규칙 |
| 용도 | 크고 복잡하며 서로 연관된 데이터 소스 | 리소스가 잘 정의된 간단한 데이터 소스 |
| 데이터 액세스 | 단일 URL 엔드포인트가 존재하며, 쿼리 조합을 통해 불러오는 데이터의 종류를 결정함. 여러 리소스를 병렬로 요청할 수 있어 서버에서 병목 현상이 발생하는 것을 방지할 수 있음. | 리소스를 정의하는 URL 형태의 여러 엔드포인트가 존재하며, 각 Endpoint마다 데이터베이스 SQL 쿼리가 달라짐 |
| 반환 데이터 | 클라이언트가 정의한 유연한 구조의 데이터 | 서버가 정의한 고정된 구조의 데이터 |
| 데이터 구조 및 정의 방법 | 클라이언트가 필요한 자유롭게 데이터를 요청할 수 있기 때문에 GraphQL 데이터는 엄격하게 형식이 지정됨. 따라서 클라이언트는 미리 결정되고 상호 이해되는 형식으로 데이터를 수신해야 함. | 리소스의 모양과 크기가 서버에 의해 결정되기 때문에 REST 데이터는 형식이 약하게 지정됨. 따라서 클라이언트는 형식이 지정된 데이터를 반환할 때 해석하는 방법을 결정해야 함. |
| 오류 검사 | 잘못된 요청일 경우 일반적으로 스키마 구조에 의해 거부됨. 그 결과 오류 메시지가 자동으로 생성됨. | 반환된 데이터가 유효한지 클라이언트가 확인해야 함 |
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은 스키마로 타입 시스템을 사용하여 데이터 모델을 정의한다.
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란 클라이언트에서 서버로 전송되는 GraphQL 요청의 유형을 말하며, query, mutation, subscription이 있다. 서버에서 요청하는 작업의 유형을 명확하게 나타내므로, 클라이언트와 서버 간의 통신을 효율적으로 만든다.
query란 서버에 데이터를 요청하는 데 사용하는 작업으로, REST API에서의 GET 메서드에 해당한다. (CRUD의 R)
mutation이란 서버의 데이터를 수정하거나 생성하는 데 사용하는 작업으로, REST API에서의 POST, PATCH, DELETE 메서드에 해당한다. (CRUD의 CUD)
데이터의 변경 사항을 구독하는 작업으로, 실시간 데이터를 가져올 수 있다.
then 체이닝이나 async/await의 사용이 가능하다.request(url, query, variables);
url: GraphQL의 EndPoint === 요청이 전송될 서버query: 서버에서 응답할 query 형태variablesmethod, 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.
에러 메세지를 읽어보면 deleteCart는 ID! 타입을 반환하기 때문에 하위 필드를 가지면 안된다고 작성되어 있다.
에러 메세지를 보고 찾아본 결과, 이 에러는 graphql의 필드에 대해 잘못 이해하고 있었기 때문에 생긴 문제였다.
{})가 반환하는 값에 대해 열거하는 코드블럭이라고 생각했다.{})는 코드블럭이 아닌 객체이며, 반환 받는 객체 내부에 포함된 field를 열거할 수 있다.정리하면, 서버의 resolver에 작성한 deleteCart 필드가 반환하는 값은 객체가 아닌 ID(string) 타입의 id이며, 다른 field를 전달하지 않는다. 따라서 deleteCart 필드는 subField가 없어야 한다.
// client
export const DELETE_CART = gql`
mutation DELETE_CART($id: ID!) {
deleteCart(id: $id)
}
`;
필드는 특정 객체에 대한 쿼리 요청을 할 때 필요한 문법이다.