쿼알못 GraphQL 기초(CRUD) 간단 정리

Table of Content

GraphQL

GraphQL은 서버 등에서 API를 제공하기 위한 쿼리 언어입니다. RESTful API를 대체할 수 있는 방법이며, RESTful API와의 차이점으로 GET과 POST 방식만 사용하고 PATCH, DELETE 방식은 사용하지 않는다는 점 등이 있습니다. 자세한 내용은 https://graphql-kr.github.io을 참고…

여기선 GraphQL을 사용하여 간단하게 CRUD(Create, Read, Update, Delete) 하는 방법을 알아보겠습니다.

테스트 환경 구축

node 프로젝트에 graphql, express, express-graphql 라이브러리를 설치합니다. 이는 각각 GraphQL 라이브러리, Express 서버, Express 서버에서 GraphQL을 사용할 수 있게 해주는 라이브러리입니다.

$ mkdir graphql-test
$ npm init -y
$ npm i graphql express express-graphql

또한 테스트를 위해 Postman이라는 프로그램이 필요합니다. https://www.postman.com 에서 다운받을 수 있습니다.

Hello GraphQL!

Hello GraphQL! 문자열을 반환하는 GraphQL 쿼리를 만들어 실행해보겠습니다.

자바스크립트 코드로 GraphQL 쿼리 실행

const { graphql, buildSchema } = require('graphql');

/* 스키마(구조) 생성 */
// id: 정수형, body: 문자열
const schema = buildSchema(`
  type Query{
    id: Int, 
    body: String
  }
`);

/* GraphQL 쿼리 호출 시 작동할 함수 정의 */
const root = {
  id: () => 1,
  body: () => 'Hello GraphQL!'
}

/* GraphQL 쿼리 실행: 두번째 매개변수 값이 GraphQL 쿼리 */
graphql(schema, '{id}', root).then(response => console.log(response));
graphql(schema, '{body}', root).then(response => console.log(response));
graphql(schema, '{id, body}', root).then(response => console.log(response));

위의 코드를 node로 실행시키면 아래와 같은 결과가 나타납니다.

{ data: [Object: null prototype] { id: 1 } }
{ data: [Object: null prototype] { body: 'Hello GraphQL!' } }
{ data: [Object: null prototype] { id: 1, body: 'Hello GraphQL!' } }

Express 서버를 통해 GraphQL 쿼리 실행

const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');

/* 스키마(구조) 생성 */
// id: 정수형, body: 문자열
const schema = buildSchema(`
  type Query{
    id: Int, 
    body: String
  }
`);

/* GraphQL 쿼리 호출 시 작동할 함수 정의 */
const root = {
  id: () => 1,
  body: () => 'Hello GraphQL!'
}

/* Express 서버 실행 */
const app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true, // GUI 설정
}));
app.listen(4000, () => console.log('running server port 4000'));

graphiql 속성 값이 true이므로 localhost:4000/graphql 주소로 접속하면 GraphQL을 테스트할 수 있는 웹 페이지가 나타납니다. 여기서 { id }, { body } 또는 { id, body } 을 입력한 후 재생(?) 버튼을 누르면 다음과 같은 결과가 나타납니다.

{
  "data": {
    "id": 1,
    "body": "Hello GraphQL!"
  }
}

url로 직접 GraphQL 쿼리를 실행하려면 graphql 속성 값을 false로 설정한 후 아래와 같이 http://localhost:4000/graphql?query=쿼리 주소로 접속을 시도하면 됩니다.

GraphQL 스키마 및 함수 생성

GraphQL로 CRUD(Create, Read, Update, Delete) 하는 방법을 간단히 알아보겠습니다.

먼저 GraphQL을 사용하기 위한 모듈을 가져옵니다.

const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');

CRUD할 데이터를 정의합니다. 보통 데이터베이스에 데이터를 조회, 추가, 수정 또는 삭제를 하지만 여기서는 대충 배열로 CRUD를 해보겠습니다.

// CRUD할 데이터(배열)
const animeList = [
  { id: 1, name: '날씨의 아이', score: 100, description: '우리들의 사랑이, 우리들의 목소리가 나아가라고 말해!' }, 
  { id: 2, name: '귀멸의 칼날', score: 100, description: '숯을 다 팔아왔는데... 오늘은 운수가 좋더니만...' },
  { id: 3, name: '미드나잇 가스펠', score: 100, description: '몽환적, 난해함 but 그 속에 담긴 삶에 대한 고찰과 교훈' }
];

GraphQL 스키마를 생성해줍니다.

/* 스키마(구조) 생성 */
// type Anime: Anime 데이터(배열) 정보
// type Query: Read 쿼리를 수행할 함수 정보
// input AnimeInput: Create, Update 쿼리를 수행하기 위한 데이터 정보
// type Mutation: Create, Update, Delete 쿼리를 수행할 함수 정보
const schema = buildSchema(`
  type Anime {
    id: ID!,
    name: String,
    score: Int,
    description: String
  }

  type Query {
    getAnime(id: ID!): Anime,
    getAnimes(startId: Int!, endId: Int!): [Anime]
  }

  input AnimeInput {
    name: String,
    score: Int,
    description: String
  }

  type Mutation {
    addAnime(input: AnimeInput): Anime,
    updateAnime(id: ID!, input: AnimeInput!): Anime
    deleteAnime(id: ID!): String
  }
`);

type Anime와 input AnimeInput의 id, name, score, description에 값의 타입을 설정했습니다. type Anime는 animeList 배열에 저장된 객체의 구조를, input AnimeInput은 animeList 배열에 저장할 객체의 구조를 명시합니다. id가 빠진 이유는 서버에서 id를 자동으로 생성해줄 것이기 때문입니다. 그리고 타입 뒤에 느낌표(!)를 붙이면 해당 값이 비어있을 수 없음을 뜻합니다.

주요 값 타입 종류는 다음과 같습니다.

  • Int: 부호가 있는 32비트 정수.
  • Float: 부호가 있는 부동소수점 값.
  • String: UTF-8 문자열.
  • Boolean: true 또는 false.
  • ID: 고유식별자(유니크한 값만 지정됨을 명시)

type Query와 type Mutation에는 GraphQL 쿼리를 수행할 함수의 매개변수 타입과 반환 타입을 명시해줍니다. 예를 들어…

  • getAnime(id: ID!): Anime는 getAnime() 함수가 id값을 반드시 매개변수로 받으며 type Anime를 반환한다는 뜻입니다.
  • getAnimes(startId: Int!, endId: Int!): [Anime]는 getAnimes() 함수가 startId값과 endId 값을 반드시 매개변수로 받으며 type Anime의 배열을 반환함을 뜻합니다.

type Mutation에는 Create, Update, Delete (POST 방식으로 작동하는) 함수가 받을 매개변수 타입과 반환 타입을 정의해줍니다.

GraphQL 쿼리 호출 시 작동할 함수를 만들어줍니다.

/* GraphQL 쿼리 호출 시 작동할 함수 정의 */
const root = {
  // Create
  addAnime: ({ input }) => {
    input.id = parseInt(animeList.length + 1);
    animeList.push(input);
    console.log('Anime Added! current AnimeList:', animeList);
    return root.getAnime({ id: input.id });
  },

  // Read
  getAnime: ({ id }) => animeList.find(anime => anime.id === parseInt(id)),
  getAnimes: ({ startId, endId }) => animeList.filter(anime => anime.id >= startId && anime.id <= endId),

  // Update
  updateAnime: ({ id, input }) => {
    const index = animeList.findIndex(anime => anime.id === parseInt(id));
    animeList[index] = {
      id: parseInt(id),
      ...input
    }
    console.log('Anime Updated! current AnimeList:', animeList);
    return animeList[index];
  },

  // Delete
  deleteAnime: ({ id }) => {
    const index = animeList.findIndex(anime => anime.id === parseInt(id));
    animeList.splice(index, 1);
    console.log('Anime deleted! current AnimeList:', animeList)
    return 'Anime Deleted!';
  }
}

마지막으로 Express 서버를 기동하기 위한 코드를 작성해줍니다.

/* Express 서버 코드*/
const app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true, // localhost:4000/graphql 주소로 접속 시 GraphQL 테스트 페이지 표시 여부
}));

app.listen(4000, () => console.log('running server port 4000'));

GraphQL 쿼리 작성 및 실행

많이 헷갈립니다…

Create

Postman에서 http://localhost:4000/graphql 주소로 아래 쿼리를 POST 방식으로 날려주면 데이터가 추가됩니다. 응답 내용과 콘솔 화면에 찍히는 화면도 별도로 확인해봅시다.

{
  "query": "mutation addAnime($input: AnimeInput) { addAnime(input: $input) { id } }",
  "variables": {
    "input": {
      "name": "부호형사 Balance: Unlimited",
      "score": 100,
      "description": "돈이 최고다!"
    }
  }
}

Read

Postman에서 http://localhost:4000/graphql 주소로 아래 쿼리를 GET 방식으로 날려주면 id에 해당하는 정보를 가져옵니다. 웹 브라우저에서 http://localhost:4000/graphql?query={getAnime(id:1){id,name,score,description}}로 접속해도 동일한 결과를 얻을 수 있습니다.

{
    getAnime(id: 1) { 
    id, name, score, description 
  }
}

아래 쿼리를 날리면 특정 ID 범위의 데이터를 얻어옵니다.

{
    getAnimes(startId: 2, endId: 4) { 
    id, name, score, description 
  }
}

Update

Postman에서 http://localhost:4000/graphql 주소로 아래 쿼리를 POST 방식으로 날려주면 id에 해당하는 정보를 수정합니다.

{
  "query": "mutation updateAnime( $id: ID!, $input: AnimeInput! ) { updateAnime( id: $id, input: $input) { id } }",
  "variables": {
    "id": 1,
    "input": {
      "name": "Weathering With You",
      "score": 100,
      "description": "Grand Escape!"
    }
  }
}

Delete

Postman에서 http://localhost:4000/graphql 주소로 아래 쿼리를 POST 방식으로 날려주면 id에 해당하는 정보를 삭제합니다.

{
  "query": "mutation deleteAnime( $id: ID! ) { deleteAnime( id: $id ) }",
  "variables": { "id": 1 }
}

전체 소스코드

const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');

// CRUD할 데이터(배열)
const animeList = [
  { id: 1, name: '날씨의 아이', score: 100, description: '우리들의 사랑이, 우리들의 목소리가 나아가라고 말해!' }, 
  { id: 2, name: '귀멸의 칼날', score: 100, description: '숯을 다 팔아왔는데... 오늘은 운수가 좋더니만...' },
  { id: 3, name: '미드나잇 가스펠', score: 100, description: '몽환적, 난해함 but 그 속에 담긴 삶에 대한 고찰과 교훈' }
];

/* 스키마(구조) 생성 */
// type Anime: CRUD할 Anime 데이터 정보
// type Query: Read 쿼리를 수행할 함수 정보
// input AnimeInput: Create, Update 쿼리를 수행하기 위한 데이터 정보
// type Mutation: Create, Update, Delete 쿼리를 수행할 함수 정보
const schema = buildSchema(`
  type Anime {
    id: ID!,
    name: String,
    score: Int,
    description: String
  }

  type Query {
    getAnime(id: ID!): Anime,
    getAnimes(startId: Int!, endId: Int!): [Anime]
  }

  input AnimeInput {
    name: String,
    score: Int,
    description: String
  }

  type Mutation {
    addAnime(input: AnimeInput): Anime,
    updateAnime(id: ID!, input: AnimeInput!): Anime
    deleteAnime(id: ID!): String
  }
`);

/* GraphQL 쿼리 호출 시 작동할 함수 정의 */
const root = {
  // Create
  addAnime: ({ input }) => {
    input.id = parseInt(animeList.length + 1);
    animeList.push(input);
    console.log('Anime Added! current AnimeList:', animeList);
    return root.getAnime({ id: input.id });
  },

  // Read
  getAnime: ({ id }) => animeList.find(anime => anime.id === parseInt(id)),
  getAnimes: ({ startId, endId }) => animeList.filter(anime => anime.id >= startId && anime.id <= endId),

  // Update
  updateAnime: ({ id, input }) => {
    const index = animeList.findIndex(anime => anime.id === parseInt(id));
    animeList[index] = {
      id: parseInt(id),
      ...input
    }
    console.log('Anime Updated! current AnimeList:', animeList);
    return animeList[index];
  },

  // Delete
  deleteAnime: ({ id }) => {
    const index = animeList.findIndex(anime => anime.id === parseInt(id));
    animeList.splice(index, 1);
    console.log('Anime deleted! current AnimeList:', animeList)
    return 'Anime Deleted!';
  }
}

/* Express 서버 코드*/
const app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true,
}));

app.listen(4000, () => console.log('running server port 4000'));

댓글 남기기