본문 바로가기

JavaScript/GraphQL

GraphQL 사용해보기

 

1. REST API의 한계

  • REST API는 URL과 HTTP Method(get, post, ...)를 조합해 사용하기 때문에 다양한 Endpoint가 존재합니다.(복잡성 증가)
  • Over-Fetching으로 인해 필요하지 않은 데이터까지 모두 받게 되어 낭비가 되는 상황이 발생합니다.
  • Under-Fetching으로 인해 한 번의 요청으로 필요한 데이터를 받지 못해 여러 번 요청을 해야 하는 상황이 발생합니다.

 

2. GraphQL?

  • 위와 같은 REST API의 한계를 보완하기 위해 만들어진 API입니다.
  • Graph Query Language는 Facebook에서 개발한 API로써, 서버 런타임이며 SQL과 같은 쿼리 언어입니다.
  • SQL은 데이터베이스에서 데이터를 효율적으로 가져오기 위한 질의 언어이지만, GQL은 클라이언트 측이 서버 측에서 데이터를 효율적으로 가져오기 위한 질의 언어입니다.
  • 정확히 어떤 데이터가 필요한지 서버에 질의하면, 서버는 클라이언트 측에서 요청한 내용을 처리합니다.

 

 

3. GraphQL 사용해보기

프로젝트 생성

$ mkdir graphql-start && cd graphql-start
$ npm init -y
$ npm i apollo-server graphql
$ npm i nodemon -D
  • apollo-server : GraphQL을 제공하는 서버를 만들 수 있게 도와주는 Apollo 패키지입니다.
  • nodemon : 파일이 수정되었을 때 서버에 바로 적용해주는 패키지입니다.

 

index.js 생성

const { ApolloServer, gql } = require('apollo-server');

// The GraphQL schema
const typeDefs = gql`
  type Query {
    "A simple type for getting started!"
    hello: String
  }
`;

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    hello: () => 'world',
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});
  • typeDefs : gql의 스키마(Object, Query, Mutation, Input)를 지정하는 부분으로, gql 함수에 Tagged Template Literals을 사용해 스키마를 지정합니다.
  • resolvers : 스키마에 대한 구현체를 작성하는 부분으로, 요청에 대한 DB 연결 등의 비즈니스 로직이 들어갑니다. 예제에선 'hello'를 요청하면 'world'를 응답하도록 작성되어 있습니다.

 

package.json의 "scripts" 내용 변경

{
  ...
  "scripts": {
    "dev": "nodemon index.js"
  },
  ...
}

 

Apollo 서버를 실행

$ npm run dev

 

브라우저에서 localhost:4000으로 접속 후 Playground 실행

hello를 질의하면 응답으로 world가 온 것을 확인할 수 있다

 

여러개의 데이터 조회하기

typeDefs Query에 배열 타입 books를 추가합니다.

const { ApolloServer, gql } = require('apollo-server');

// The GraphQL schema
const typeDefs = gql`
  type Query {
    "A simple type for getting started!"
    hello: String
    books: [Book]
  }

  type Book {
    bookId: Int
    title: String
    message: String
    author: String
    url: String
  }
`;

...

 

resolvers Query에 books 구현체를 작성합니다.

const { ApolloServer, gql } = require('apollo-server');
const { readFileSync } = require("fs");
const { join } = require('path')

...

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    hello: () => 'world',
    books: () => {
      // DB 연결 등의 작업
      // 아래에선 DB 대신 books.json 파일의 내용을 이용하고 있다
      return JSON.parse( readFileSync( join(__dirname, 'books.json') ).toString() );
    }
  },
};

...
/* books.json */

[
  {
    "bookId": 1,
    "title": "title test",
    "message": "message test",
    "author": "author test",
    "url": "url test"
  },
  {
    "bookId": 2,
    "title": "title test2",
    "message": "message test2",
    "author": "author test2",
    "url": "url test2"
  }
]

 

이제 서버에 쿼리를 해서 결과를 확인합니다.

query {
  books {
    bookId
    title
    author
  }
}

특정 데이터 조회하기

typeDefs Query에 bookId를 인자로 받고, Book 타입의 데이터를 리턴하는 스키마를 추가합니다.

const typeDefs = gql`
  type Query {
    ...
    book(bookId: Int): Book
  }

  type Book {
    bookId: Int
    title: String
    message: String
    author: String
    url: String
  }
`;

 

resolvers Query에 book에 대한 구현체를 작성합니다.

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    ...
    book: (parent, args, context, info) => {
      const books = JSON.parse(readFileSync(join(__dirname, 'books.json')).toString());
      return books.find( book => book.bookId === args.bookId );
    }
  },
};

 

쿼리를 통해 결과를 확인합니다.

query {
  book(bookId: 1) {
    title
    author
  }
}

 

데이터 추가하기

typeDefs Mutation에 데이터 추가에 대한 스키마를 작성합니다.

// The GraphQL schema
const typeDefs = gql`
  type Query {
    ...
  }
  
  type Mutation {
    addBook(title: String, message: String, author: String, url: String): Book
  }

  type Book {
    bookId: Int
    title: String
    message: String
    author: String
    url: String
  }
`;

 

resolvers Mutation에 구현체를 작성합니다.

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    ...
  },
  Mutation: {
    addBook: (parent, args, context, info) => {
      const books = JSON.parse(readFileSync(join(__dirname, 'books.json')).toString());

      const maxId = Math.max(...books.map( book => book.bookId ));
      const newBook = { ...args, bookId: maxId + 1 };
      writeFileSync(
        join(__dirname, "books.json"),
        JSON.stringify([...books, newBook ])
      );
      
      return newBook;
    }
  }
};

 

이제 Mutation 요청을 보내서 결과를 확인합니다.

Mutation {
  addBook(title: "title test3", message: "message test3") {
    title
    message
  }
}
/* books.json */

[
  ...,
  {
    "bookId": 3,
    "title": "title test3",
    "message": "message test3"
  }
]

 

데이터 수정/삭제도 마찬가지로 typeDefs, resolvers의 Mutation에 코드를 추가해서 설정할 수 있습니다.