REST API VS GraphQL

REST API로만 벡엔드 서버 API를 구현한 나로서는 굳이 GraphQL이란 것을 알아야할까 싶었다. 이 생각은 GraphQL로 간단한 프로젝트를 만들어 보며 완전히 바뀌게 되었다.

11분동안 정말 재밌게 설명해주신 유튜버가 있어 REST와 GraphQL에 대한 긴 설명은 이것으로 대체한다.

뭘 써야할 지 모르겠다면 GraphQL, Rest API 모두 만들어서 필요한 작업마다 다르게 써먹으면 된다. 일반적으로 파일 전송같은 경우 RESTful이 더 유리하다고 하고, CRUD 작업이 대부분이라면 GraphQL이 훨씬 편하다.

Node.js에서 GraphQL 구축하기

우선 express에서 GraphQL을 사용하기 위해 관련 패키지를 설치한다. (물론 express 세팅은 끝난 가정하이다)

1
2
$ yarn add graphql
$ yarn add express-graphql

간단하게 GraphQL은 스키마 + 리졸버로 구성된다. 스키마 안에 변수나 함수를 정의하고, 리졸버에서 함수를 구현하면 된다.

우선 스키마를 정의하자. 나는 모듈화를 위해 파일을 용도에 맞게 쪼개서 쓰기로 했다.

스키마 만들기

schema.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { buildSchema } = require('graphql');

module.exports = buildSchema(`
input ProductInput {
name: String,
price: Int,
description: String
}

type Product {
id: ID!,
name: String,
price: Int,
description: String
}

type Query {
getProduct(id: ID!): Product
}

type Mutation {
addProduct(input: ProductInput): Int
}
`);

먼저 type Querytype Mutation을 보자. 일반적으로 GET 작업은 Query로, POST 작업은 Mutation으로 정의한다.

Query

getProduct라는 함수는 ID 유형의 id 변수를 인자로 받고, Product 객체를 반환한다는 의미이다. (여기서 ID는 MySQL에서의 일반적인 PK로 생각하면 된다)

이제 반환할 Product 객체에 뭐가 들었는지 type Product로 정의해주면 된다. 나만의 자료형을 만든다고 생각하면 쉽다.

참고로 !가 붙은 변수는 무조건 해당 인자는 받아야 한다는 required의 의미와 비슷한 표시이다.

Mutation

addProduct는 말그대로 POST 메소드로 상품을 등록하는 함수이다. 입력값이 ProductInput으로 되어 있는데, Product type을 만들 때와 비슷하게 클라이언트의 입력 값을 미리 정의해둔 것이다. (일반적으로 뒤에 Input을 붙인다고 한다)

Resolver 만들기

이제 getProductaddProduct의 함수를 구현하면 거의 끝이다.

rootValue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const products = require('./products');
const defaultProducts = require('./defaultProducts');

module.exports = {
getProduct: ({ id }) => {
return products.find((product) => product.id === parseInt(id));
},
addProduct: ({ input }) => {
input.id = products.length === 0 ? 1 : products[products.length - 1].id + 1;
products.push(input);
return input.id;
},
};

나는 products라는 데이터 배열을 모듈화하여 사용하였다. 스키마에서 선언한 인자를 그대로 받아 적절한 함수를 짜고, 미리 정해놓은 리턴 자료형대로 반환하면 된다.

Express로 라우팅하기

1
2
3
4
5
6
7
8
this.app.use(
'/graphql',
graphqlHTTP({
schema,
rootValue,
graphiql: true, // support GUI
}),
);

이제 /graphql이라는 경로로 GraphQL API를 라우팅하면 모든 구축이 끝난다. 스키마는 schema로, 리졸버는 rootValue라는 값으로 받는다. (그렇기 때문에 난 파일 이름을 똑같이 만들었다) graphiql은 URL에서 /graphql로 접속했을 때 쿼리와 뮤테이션을 써볼 수 있는 GUI 환경을 제공한다. 당연히 보안을 위해 배포시에는 graphiqlfalse로 세팅해야 한다.

클라이언트에서 데이터 주고 받기

이제 쿼리와 뮤테이션을 각각 GET, POST 요청으로 데이터를 주고 받을 수 있다. fetch, axios 등 방법은 많은데 fetch는 브라우저 호환에 문제가 있다고 하여 axios를 사용하기로 했다.

GET 요청

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const apiUri = 'http://localhost:3000/graphql';
function getProduct() {
axios
.get(apiUri, {
params: {
query: `{getProduct(id : ${pid.value}) {id price name description}}`,
},
})
.then((result) => {
console.log(result)
})
.catch((err) => {
console.log(err);
});
}

스키마에서 짜놓은 형식대로 쿼리를 GET으로 넘기면 된다. axios 자체적으로 promise를 지원하기 때문에 then, catch를 사용하면 에러 핸들링이 수월하다. 이제 result를 가지고 데이터를 가공하여 클라이언트에게 제공할 수 있다.

POST 요청

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function postProduct(e) {
axios
.post(apiUri, {
query: 'mutation addProduct($input: ProductInput) { addProduct(input: $input)}',
variables: {
input: {
price: parseInt(this.price.value),
name: String(this.name.value),
description: String(this.description.value),
},
},
operationName: null,
})
.then((result) => {
console.log(result);
})
.catch((err) => {
console.log(err);
});
}

GET과 방식은 거의 똑같은데, POST 메소드이므로 params가 아닌 query를 넘긴다. $를 사용하여 변수를 variables로 빼서 사용할 수도 있고, 백틱을 사용해서 하드코딩해도 상관은 없다.

매력적인 GraphQL

사실 기존의 서버 API를 바꾸는 것은 많은 시간과 노력이 든다. 하지만 REST API와 GraphQL를 한번에 사용할 수도 있으므로 클라이언트(프론트엔드) 부분만 살짝 손대주면 나중을 생각할 때 훨씬 간결한 CRUD 환경을 구축할 수 있다. 현재도 폭발적인 인기를 받아 많은 발전이 이뤄지고 있다고 한다.

GraphQL의 CRUD 작업을 시뮬레이션할 수 있는 서비스 graphql-crud-demo 를 개발했는데, 다음 글에 포스팅해보겠다.