본문 바로가기

Client/Front-end

<FE> 프론트 관점에서 맛보는 GraphQL

1. 소개

* GraphQL이란?

 - 최근 GraphQL을 아냐는 질문을 받은 적이 있다. 프론트엔드에서 백엔드와의 api 소통에 대해 얘기하다가 나왔던 것 같다. 그 때는 정말 아무것도 몰라서 대답하지 못했지만 이후로 관심이 생겨서 이렇게 맛만 보게 되었다.

 

 - GraphQL은 페이스북에서 2015년 공개한 "쿼리 언어"이다. gql이라고도 불리는 이 언어는 api를 위해 만들어졌다. 공부를 하면서 계속 sql과 혼동이와서 db에서 이게 어떻게 되지? 하는 의문을 계속 가졌는데, sql은 DB에서 데이터를 효율적으로 가져오는 언어이고 gql은 "클라이언트"가 서버로부터 데이터를 효율적으로 가져오는 언어임을 명심하자.

 

 - api하면 떠오르는 단어가 있다. "REST API". REST API가 있는데 gql은 왜 등장했을까.

 

* REST API

 - REST API는 URL과 Method로 자원과 행위를 명시하여 효율적으로 서버에서 데이터를 받아오는 패러다임이다. 현재 대부분의 오픈api에서 사용되고 있지만 단점이 존재한다. 스타워즈 오픈 api(https://swapi.dev/)를 통해 이를 알아보자.

 

 - 먼저 Over Fetching 문제가 있다. 위의 사이트에서 5라는 id를 가진 사람의 정보를 요청해보자. 

 

{
	"name": "Leia Organa",
	"height": "150",
	"mass": "49",
	"hair_color": "brown",
	"skin_color": "light",
	"eye_color": "brown",
	"birth_year": "19BBY",
	"gender": "female",
	"homeworld": "https://swapi.dev/api/planets/2/",
	"films": [
		"https://swapi.dev/api/films/1/",
		"https://swapi.dev/api/films/2/",
		"https://swapi.dev/api/films/3/",
		"https://swapi.dev/api/films/6/"
	],
	"species": [],
	"vehicles": [
		"https://swapi.dev/api/vehicles/30/"
	],
	"starships": [],
	"created": "2014-12-10T15:20:09.791000Z",
	"edited": "2014-12-20T21:17:50.315000Z",
	"url": "https://swapi.dev/api/people/5/"
}

 

 - 어마어마한 데이터가 나왔다. 프론트에서 필요한 정보는 단지 이름과 키, 성별, 고향뿐이라면 자원을 낭비하는 꼴이 된다. 이러한 문제를 오버패칭이라 한다.

 

 - 두 번째는 위와는 반대의 의미를 가진 언더패칭이다. 단어에서 유추할 수 있듯이 데이터가 부족하다는 뜻이다. 위에서는 데이터가 너무 많다더니 이게 무슨 소리인가 싶을 것이다. 위의 예를 이어서 생각해보겠다. 필요한 정보가 이름, 키, 성별, 고향이라 하였다. 하지만 위의 정보라는 고향 행성의 이름이 무엇인지 중력이 무엇인지 알 수 없다. 그렇다면 위의 정보를 통해 행성의 정보를 얻어보자.

 

{
	"name": "Alderaan",
	"rotation_period": "24",
	"orbital_period": "364",
	"diameter": "12500",
	"climate": "temperate",
	"gravity": "1 standard",
	"terrain": "grasslands, mountains",
	"surface_water": "40",
	"population": "2000000000",
	"residents": [
		"https://swapi.dev/api/people/5/",
		"https://swapi.dev/api/people/68/",
		"https://swapi.dev/api/people/81/"
	],
	"films": [
		"https://swapi.dev/api/films/1/",
		"https://swapi.dev/api/films/6/"
	],
	"created": "2014-12-10T11:35:48.479000Z",
	"edited": "2014-12-20T20:58:18.420000Z",
	"url": "https://swapi.dev/api/planets/2/"
}

 

 - 많은 정보가 내려오긴 했지만 어쨌든 의도한 이름과 중력정보를 받을 수 있게 되었다. 즉 REST API에서는 원하는 정보가 부족하여 여러번 요청을 해야할 수도 있는 것이다.

 

 - 마지막 단점은 유연성 부족 문제이다. REST API를 설계해 본 사람이라면 알겠지만 너무 많은 엔드포인트 때문에 골머리를 앓게 된다. 수많은 조합을 만들어서 요청해야하고 이를 관리해야한다.

 

 - 뿐만 아니라 백엔드에서 정한 키 값을 반드시 사용해야한다. 이 때문에 많은 커뮤니케이션 비용이 들어간다. 만약 프론트에서 home이라고 설계하고 개발을 했는데 백엔드에서 homeworld라고 넘겨준다면? 생각만해도 끔찍하다.

 

 - 위와 같은 유연성 문제로 인해 복잡해질 수록 문서도 복잡해지고 최신화도 힘들어진다. 

 

2. 등장

 - 위에서 보았던 REST API의 문제들을 gql의 등장으로 해결할 수 있게 되었다. 참고로 gql도 일반적인 api와 마찬가지로 특정 플랫폼에 종속적이지 않다. 네트워크 방식에도 종속되지 않으며, 일반적으로 http POST메서드와 웹소켓 프로토콜을 활용하며, 필요에 따라 변형이 가능하다.

 

 - gql을 요청하기전에 내부설계를 잠깐 언급하자면, 타입스크립트의 타입처럼 작성되어 있다라고 생각하면 편하다. 만일 이 부분을 먼저 보고 사용하는 것을 보고싶다면 3.맛보기 부터 보는 것을 추천한다. 아래의 사용 예시는 스타워즈의 GraphQL 오픈api를 사용할 것이다.

 

* 오버패칭

 - 오버패칭은 필요하지 않은 데이터까지 받아오는 것을 의미했다. 위의 예제처럼 5 라는 id를 가진 사람의 이름, 키, 성별을 알고 싶다고 가정하자. (고향은 아래에서 따로 알아보자.)

 

query {
  person(personID: 5) {
    name
    height
    gender
  }
}

 

 - 위와 같은 간단한 쿼리를 작성하면 된다. person의 정보를 요청할 것이며 파라미터로 5를 넘겨준다는 의미이다. 이름과 키, 성별 정보를 달라는 의미이다. 위를 서버에 요청하면 답은 아래와 같다.

 

{
  "data": {
    "person": {
      "name": "Leia Organa",
      "height": 150,
      "gender": "female"
    }
  }
}

 

 - 오버패칭문제를 거의 완벽하게 해결했다. 

 

* 언더패칭

 - 위의 REST API에서는 5번 Id의 사람의 고향 행성 이름과 중력정보를 얻기 위해 한 번 더 요청을 보냈었다. 그렇다면 gql은 어떨까.

 

query {
  person(personID: 5) {
    name
    height
    gender
    homeworld {
      name
      gravity
    }
  }
}

 

 - 위의 쿼리 단 한번의 요청이면 고향 행성의 이름과 중력정보를 받아올 수 있다. 너무 놀랍기때문에 직접 입력해서 확인해보길 권장한다.

 

* 유연성 부족

 - 마지막은 유연성 부족문제였다. 위에서 볼 수 있듯이 엔드포인트관리가 거의 필요없다. gql은 이론적으로 하나의 엔드포인트만으로 모든 요청을 수행할 수 있다. (ex. /graphql)

 

 - 뿐만 아니라 키 값을 프론트가 원하는대로 수정할 수 있다. 만약 프론트에서 homeworld가 아니라 home이라는 키값으로 사용하고 있다면 어떻게 요청할까?

 - home:homeworld 가 끝이다.

 

3. 맛보기

 - 제목이 프론트관점이긴 하지만 서버측에서 어떤식으로 설계되어 있는지 궁금할 수 있다. 사실 프론트에서 사용하는 방법은 위에서 처럼 쿼리를 날리기만 하면 되니까 말이다. (물론 그렇게 간단하게만 사용하진 않는다...) 필자의 모국어는 JS이기 때문에 NodeJS를 기준으로 설명하는 점 양해바란다. 따라서 서버는 express로 설계하였다. 서비스는 간단한 sns서비스이다.

 

- 먼저 필요한 패키지를 설치하였다. gql은 쿼리언어이므로 이를 활용할 수 있도록 도와주는 아폴로를 설치하였다. 아폴로외에도 다양한 제공자들이 있으므로 찾아보길 바란다. 하지만 현재 express에서는 아폴로가 가장 많이 쓰이는 듯 하다.

 

yarn add express graphql apollo-server apollo-server-express

 

* 서버 앱 설계

 - 앱을 설계해보겠다.

 

// app.js
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { readMyDataBase } from './dbController.js';

const server = new ApolloServer({
  typeDefs: schema,
  resolvers,
  context: {
    db: {
      messages: readMyDataBase('messages'),
    },
  },
});

const app = express();

await server.start();

server.applyMiddleware({
  app,
  path: '/graphql',
  cors: {
    origin: ['http://localhost:3000', 'https://studio.apollographql.com'],
    credentials: true,
  },
});

await app.listen({ port: 8080 });
console.log('server listening on 8080...');

 

 - 아폴로서버로 서버를 만든 후 미들웨어에 express를 넣은 형태로 작성되었다. cors는 별도 라이브러리를 필요로하지 않고 바로 설정할 수 있다. 이 때, 클라이언트 url외에 아폴로 스튜디오가 들어가 있는데, 이 부분은 꼭 넣어줘야한다. 예시를 미리 테스트해볼 수 있는 환경이고, 아래에서 살펴 보겠다.

 

 - 서버 설정을 보면 typeDefs에 스키마를 불러와 넣었고, resolvers에 리졸버를 넣었다. 이 둘은 필수로 작성해야한다. context는 굳이 작성할 필요는 없지만 사용하는 것을 보여주기 위해 넣었다. context에는 여러 정보를 넣을 수 있는데, db라는 곳에 디비에 저장된 sns메세지들을 전부 통째로 불러와 넣었다. 필자의 db 는 단순 json 키벨류 db 이므로 매우 용량이 작기 때문이다.

 

* 서버 스키마

import { gql } from 'apollo-server-express';

const messageSchema = gql`
  type Message {
    id: ID!
    text: String!
    userId: ID!
    timestamp: Float #13자리 숫자
  }
  type Query {
    messages: [Message!]! # getMessages 배열
    message(id: ID!): Message! # getMessage
  }
  type Mutation {
    createMessage(text: String!, userId: ID!): Message!
    updateMessage(id: ID!, text: String!, userId: ID!): Message!
    deleteMessage(id: ID!, userId: ID!): ID!
  }
`;

export default messageSchema;

 

 - gql을 사용하기 위한 모듈을 불러와서 쿼리문을 작성했다. 여러가지 스키마를 묶어서 사용하기 위해 messageSchema만 따로 작성하였고, 이 포스팅에서는 이 부분만 다루도록 하겠다.

 

 - 먼저 Message라는 별도 타입을 만들어 주었다. 타임스탬프의 경우 정수이지만 쿼리 숫자제한 때문에 Float를 주었다.

 

 - Query는 Read라고 생각하자. 메세지 배열의 경우 대괄호를 이용하여 배열임을 병시하였다. 메시지 하나의 경우는 id라는 파라미터를 받아서 메시지를 반드시 반환하겠다는 의미이다.

 

 - Mutation은 CRUD에서 R을 제외한 것들이다. 함수와 같은 형태로 작성하고, 결과는 콜론 뒤에 작성한다.

 

* 서버 리졸버

 - 위에서 스키마로 정의를 했다면 리졸버는 각 행위를 명시하는 곳이다. 마치 라우터를 작성하는 것과 같다. 여기서는 비즈니스로직을 따로 분리하지는 않았지만 실제로는 분리하는 것을 권장한다.

 

const messageResolver = {
  Query: {
    messages: (parent, args, { db }) => {
      return db.messages;
    },
    message: (parent, { id = '' }, { db }) => {
      return db.messages.find((msg) => msg.id === id);
    },
  },
  Mutation: {
    createMessage: (parent, { text, userId }, { db }) => {
      const newMsg = {
        id: v4(), // uuid로 랜덤값 
        text: text,
        userId: userId,
        timestamp: Date.now(),
      };
      db.messages.unshift(newMsg);
      setMsgs(db.messages); // db로직
      return newMsg;
    },
    updateMessage: (parent, { id, text, userId }, { db }) => {
      // ...
    },
    deleteMessage: (parent, { id, userId }, { db }) => {
      // ...
    },
  },
};

 

 - 꽤 직관적이므로 쉽게 이해할 수 있을 것이다. 쿼리는 아까 말했듯 Read이고 Mutation은 CUD를 의미한다. 위에서 작성한 스키마를 토대로 작성하면 된다. 인자로 parent, args, context 순으로 받으며 주로 파라미터를 받는 args만 사용한다. context는 잠깐 언급했듯이 로그인한 사용자나 DB Access등의 중요 정보들을 미리 담아둔다. 우리는 위에서 db를 통째로 그냥 넣었다. (실제로는 비권장)

 

 -먼저 Query를 살펴보면 리턴값이 클라이언트가 요청했을 때의 반환 값이다. res.send와 같은 로직이 필요없다. 필자는 db에 메시지를 통째로 담아 두었으므로 바로 반환되도록 작성했다. 실제로는 디비에서 메세지들을 가져오는 로직을 작성하면 된다. 비동기 작업일 경우 async await를 활용하면 위와 비슷한 코드를 작성할 수 있게 된다.

 

 - 다음은 수정을 담당하는 Mutation이다. create만 살펴보면 인자를 받아서 새 메세지를 만들고 db를 수정하는 비즈니스 로직이 들어갔음을 알 수 있다. 그리고 스키마에서 작성했던 대로 원하는 형식을 갖춘 반환값을 리턴한다. 이 또한 비동기 작업일 경우 async await 를 활용하길 권장한다.

 

* Playground

 - 리졸버와 스키마를 잘 담아두었다면 제공되는 플레이 그라운드를 활용할 수 있다. 서버를 작동시키고, 위에서 작성했던 엔드포인트 /graphql로 접속해보자.

 

 

 - 위와 같은 곳에 접속이 될 것이다. Query your server를 클릭해보자.

 

 

 - 좌측을 보면 우리가 작성했던 스키마대로 문서가 작성되어 있을 뿐만 아니라 클릭하면 더 상세한 정보들을 볼 수 있다.

 

 

 - 쿼리도 작성해서 날려보면 직접 확인할 수 있다.

 

 

 - 글 작성도 아래에 인자를 작성하여 실행하면 잘 동작한다.

 

* 클라이언트 측

 - 클라이언트 측의 사용법은 크게 어렵지 않으므로 간단하게 짚고 넘어가겠다.

 

import { gql } from 'graphql-tag';

export const GET_MESSAGES = gql`
  query GET_MESSAGES {
    messages {
      id
      text
      userId
      timestamp
    }
  }
`;
export const CREATE_MESSAGE = gql`
  mutation CREATE_MESSAGE($text: String!, $userId: ID!) {
    createMessage(text: $text, userId: $userId) {
      id
      text
      userId
      timestamp
    }
  }
`;

 

 - graphql-tag와 같은 gql을 작성할 수 있는 라이브러리의 도움을 받아 쿼리문을 작성할 수 있다. 이와 같은 쿼리문을 react-query 와 같은 라이브러리와 함께 사용하면 좋을 듯하다.

 

 - react-query의 useQuery에 query를, useMutation에 mutation 을 사용하면 클라이언트에서 훌륭하게 상태를 관리할 수 있다. useQuery와 useMutation에 같은 쿼리 키를 줌으로써 동일한 캐시값을 관리하도록 하자.

 

 - 저번 글에서도 언급했듯이 상태라는 데이터를 관리하는 측면에서 데이터를 다루는 로직이 클라이언트에게 많이 위임되고 있음을 느낄 수 있었다.

 


참고

 

 

그래프QL - 위키백과, 우리 모두의 백과사전

그래프QL(영어: GraphQL)[1]은 페이스북이 2012년에 개발하여 2015년에 공개적으로 발표된 데이터 질의어이다.[2] 그래프QL은 REST 및 부속 웹서비스 아키텍처를 대체할 수 있다.[1] 클라이언트는 필요한

ko.wikipedia.org

 

GraphQL | A query language for your API

Evolve your APIwithout versions Add new fields and types to your GraphQL API without impacting existing queries. Aging fields can be deprecated and hidden from tools. By using a single evolving version, GraphQL APIs give apps continuous access to new featu

graphql.org

 

SWAPI - The Star Wars API

What is this? The Star Wars API, or "swapi" (Swah-pee) is the world's first quantified and programmatically-accessible data source for all the data from the Star Wars canon universe! We've taken all the rich contextual stuff from the universe and formatted

swapi.dev

 

찬미니즘

배움과 도전을 즐기는 공대생의 기록입니다.

c17an.netlify.app

 

풀스택 리액트 토이프로젝트 - REST, GraphQL (for FE개발자) - 인프런 | 강의

React 기반의 간단한 SNS 서비스를 만들면서 REST API 및 GraphQL을 연습합니다. 프론트엔드 개발을 위한 백엔드 환경을 쉽고 간단하게 만드는 방법을 소개합니다., - 강의 소개 | 인프런...

www.inflearn.com

 

 

react-query

react-query는 리액트 애플리케이션에서 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 보다 쉽게 다룰 수 있도록 도와주며 클라이언트 상태와 서버 상태를 명확히 구분하기 위해서 만들어진 라

velog.io

 

GraphQL 개념잡기

GraphQL은 페이스북에서 만든 쿼리 언어입니다. GrpahQL은 요즘 개발자들 사이에서 자주 입에 오르내리고 있으나, 2019년 7월 기준으로 얼리스테이지(early-stage)임은 분명합니다. 국내에서 GraphQL API를 O

tech.kakao.com