거의 알고리즘 일기장

GraphQL 찍먹기 본문

web

GraphQL 찍먹기

건우권 2022. 3. 6. 23:44

graphql이란?

A query language for your API

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

간단히 요약하면


1. api 요청을 sql에서 query 요청하는것처럼 할 수 있다.

 

예를 들면

//request
{
    pokemon{
        name
        type
        status {
            HP
            Defence
        }
    }
}

 

이런식으로 

 

2. 이렇게 함으로써의 장점은

 

1) 내가 만약에 pokemon의 일부데이터만 가져오고 싶을때도 그렇게 할수있다. 그러니까 백앤드에서 제공하는 데이터에 한해서 custom api를 만들수 있는거임.

2) 그러므로 백엔드에게 restapi처럼 이거 만들어줘라, 저거 만들어줘라 안해도됨. 

3) 백앤드도 api 공장처럼 안찍어내도됨, swagger 작성 안해도됨. <- gql playground에서 docs, schema 볼수있음.

4) 자연히 여러개의 데이터 요청을 한번의 call에 할수있으므로 server 자원도 아낄수있는겨.

 

이외에도 버전에 새로운 필드를 만드는데, 사이드 이펙트가 없다던가 하는 장점이 있음.


실습

실습의 순서는 이렇다.

1. gql server를 만든다. (서버는 처음해봐서 좀 코드 퀄리티가 거지같을수있으니 이해좀)

2. client에서 apollo client를 이용해서 api 요청을 해본다.

사용한 데이터는 하단 github의 데이터를 사용하였다.
https://github.com/fanzeyi/pokemon.json/blob/master/pokedex.json


server

사용 스택: graphql-yoga, js, jsonwebtoken

query: getPokemons(auth 필요), getPokemon(auth 필요)

mutation: addPokemon(auth 필요), deletePokemon(auth 필요), authenticate

//refresh token 코드는 아직 구현 안해서 무시해도 됨.
const POKEMONS = require("./pokemon.json");
const { GraphQLServer } = require("graphql-yoga");
const JWT = require("jsonwebtoken");
const GUID = require("guid");

const users = {
  dev: {
    id: 1,
    password: "123",
  },
  prod: {
    id: 2,
    password: "123",
  },
};

const JWT_SECRET = process.env.JWT_SECRET || "secret";
const refreshTokens = {};

const typeDefs = `
  type Query {
    getPokemons(page: Int!, size: Int!): [Pokemon]
    getPokemon(id: Int!): Pokemon
  }
  type Pokemon {
    id: Int!,
    name: Name!,
    type: [String!],
    base: Base!
  }
  type Name {
    english: String!
    japanese: String!
    chinese: String!
    french: String!
  }
  type Base {
    HP: Int!
    Attack: Int!
    Defense: Int!
    Speed: Int! 
  }
  type Mutation {
    addPokemon(name: String!, type: String!): ID!
    deletePokemon(id: Int!): ID!
    authenticate(name: String!, password: String!): String!
    refresh: String!
  }
`;

const getPokemons = (page, size) => {
  const begin = page * size;
  const end = page * size + size;
  return POKEMONS.slice(begin, end);
};

const getPokemon = (id) => {
  return POKEMONS.find((element) => element.id === id);
};

const authenticate = (name, password) => {
  if (users[name] && users[name].password === password) {
    return JWT.sign({ data: name }, JWT_SECRET, { expiresIn: "10m" });
  } else {
    throw new Error("Invalid credentials");
  }
};

const refresh = (refreshToken) => {
  const token = JWT.verify(refresh, JWT_SECRET);

  if (token.data in refreshTokens) {
    return JWT.sign({ data: refreshTokens[token.data] }, JWT_SECRET, {
      expiresIn: "1h",
    });
  }
};

const resolvers = {
  Query: {
    //page, size에 맞춰 pokemon array를 return한다.
    getPokemons: (_parent, { page, size }, context) => {
      if (!users[context.name]) {
        throw new Error("please sign in.");
      }
      return getPokemons(page, size);
    },
    getPokemon: (_parent, { id }, context) => {
      if (!users[context.name]) {
        throw new Error(`please sign in. context: ${JSON.stringify(context)}`);
      }
      return getPokemon(id);
    },
  },
  Mutation: {
    addPokemon: (_parent, { name, type }, context) => {
      if (!users[context.name]) {
        throw new Error("please sign in.");
      }
      return `ID: ${name}:${type}`;
    },
    deletePokemon: (_parent, { id }, context) => {
      if (!users[context.name]) {
        throw new Error("please sign in.");
      }

      return `ID: ${id}`;
    },
    authenticate: (_, { name, password }) => authenticate(name, password),
    refresh: (_, { refreshToken }) => refresh(refreshToken),
  },
};

const server = new GraphQLServer({
  cors: {
    origin: "http://localhost:3000",
    methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
    credentials: true,
  },
  context: ({ request }) => {
    const ctx = {
      name: null,
    };

    try {
      if (request.headers["x-access-token"]) {
        const token = JWT.verify(request.headers["x-access-token"], JWT_SECRET);
        ctx.name = token.data;
      }
    } catch (e) {}
    return ctx;
  },
  typeDefs,
  resolvers,
});
server.start(() => console.log("Server is running on http://localhost:4000"));

gql컨셉에 맞지않는 네이밍과 구조로 실습코드를 짠거같기는 하다. 좋은 의견있으면, 댓글 좀 부탁드립니다.


결과

header에 accessToken 없이 call
authenticate call
header에 accessToken 넣어서 call

잘 작동한다.


찍먹 후기 

찍먹 후기로는 gql은 rest api에서 원하는 데이터가 달라졌을때, api를 노가다처럼 찍어내야 하는 불편함, 협업에서의 front와 back의 불화의 원인중 하나를 해결해주는것처럼 보인다.

하지만, 약간의 러닝커브 & 인터넷에 자료가 rest로 짤때 보다는 적은점 등의 단점도 있을거 같다. 


참고

https://www.youtube.com/watch?v=zJvB2hnsXr0&t=1240s 

https://www.youtube.com/watch?v=QChEaOHauZY&t=1669s 

 

반응형

'web' 카테고리의 다른 글

react ref를 prop으로 전달하는 방법  (0) 2022.05.28
yarn berry, workspace에 대해서  (0) 2022.05.05
jsx, React.createElement, element  (0) 2022.02.12
ajax? fetch? axios?  (0) 2021.11.29
요즘 쓰는 git commit message convention  (0) 2021.11.27
Comments