GraphQL은 API용 쿼리 언어이다. API의 데이터에 대한 쉬운 설명을 제공하고 클라이언트가 필요한 것을 정확하게 요청할 수 있는 능력을 제공한다.
GraphQL의 장점
- 프론트엔드 개발자는 백엔드 개발자가 REST API 개발을 마칠 때까지 기다리지 않아도 된다. 전체 개발 프로세스를 병렬로 작업할 수 있다.
- Overfetching과 Underfetching을 막아준다.
- Overfetching: 예를 들어 비디오 채널 정보에서 실제 사용하는 채널 정보는 채널 이름과 구독자 수뿐인데 채널 생성 날짜, url 등 더 많은 정보를 받아와서 많은 네트워크 비용을 사용하게 하는 것
- Underfetching: 요청에 대한 응답에 필요한 데이터가 부족하게 오는 것
- REST를 이용할 때 필요한 데이터를 만들기 위해서 여러 번 요청을 보내야 할 때, GraphQL은 한번의 요청으로 데이터를 가져올 수 있다.
- Schema를 작성하기 때문에 데이터가 어떻게 이루어져 있는지 알 수 있다.
- Type을 작성하기 때문에 요청과 응답에 Valid한 데이터가 오고 갈 수 있다.
GraphQL의 단점
- 프론트엔드 개발자가 GraphQL 사용법을 배워야 한다.
- 백엔드에 Schema 및 Type을 정해줘야 한다. (작은 앱이라도 번거롭다.)
- REST API보다 데이터를 캐싱하는 것이 까다롭다.
- REST API에서는 URL을 사용하여 리소스에 액세스하므로, 리소스 URL가 식별자이다. 따라서 리소스 수준에서 캐시할 수 있다.
- 반면 GraphQL에서는 동일한 엔티티에서 작동하더라도 각 쿼리가 다를 수 있기 때문에 매우 복잡하다. 하지만 GraphQL 위에 구축된 대부분의 라이브러리는 효율적인 캐싱 메커니즘을 제공한다.
사용하기
1. Express GraphQL Server 생성하기
GraphQL API 서버를 실행하는 가장 간단한 방법은 Node.js용으로 널리 사용되는 웹 애플리케이션 프레임워크인 Express를 사용하는 것이다.
1) npm 프로젝트에 express, express-graphql, graphql를 설치한다.
npm install express express-graphql graphql --save
2) 서버 생성
//server.js
const express = require('express');
const app = express();
const port = 4000;
app.listen(port, () => {
console.log(`Running a GraphQL API server at http://localhost:${port}/graphql`);
})
3) 스키마 작성
//server.js
const { buildSchema } = require('graphql');
const schema = buildSchema(`
type Query {
description: String
}
`);
4) API 작성
//server.js
const root = {
description: "hello world"
}
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root //응답값
}));
rootValue는 응답값이다. 위 API를
{
description
}
하고 요청하면
"data": {
description: "hello world"
}
형태로 데이터가 들어오게 될 것이다.
2. 서버 실행
터미널에 node server.js를 입력해 서버를 실행해준다.
3. API 요청하기
graphql의 경우 POST로 요청한다.
GraphiQL
웹 브라우저에서 GraphiQL 편집기를 사용하여 API를 테스트할 수 있다.
express-graphql 패키지 안에 들어있다.
//server.js
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true //<-*-*-
}));
서버를 실행한 후 http://localhost:4000/graphql에 들어가보면 다음 화면이 뜬다.
오른쪽 위 Docs를 누르면 다음과 같이 쿼리가 뜨고,
Query를 클릭하면 작성한 스키마대로 description이 보인다.
다음과 같이 입력하고 실행 버튼을 누르면 API의 응답을 확인할 수 있다.
GraphQL Tools
스키마가 많아지면, 소스코드가 복잡해지고 관리하기도 힘들어진다.
관련된 부분끼리 모듈화할 때 사용하는 것이 graphql-tools이다. 분리된 graphql 파일들을 다시 하나로 모아 합쳐준다.
1) 설치 npm install @graphql-tools/schema
2) type을 분리한다.
//comments/comments.graphql
type Query {
comments: [Comment]
}
type Comment {
id: ID!
text: String!
likes: Int
}
//posts/posts.graphql
type Query {
posts: [Post],
}
type Post {
id: ID!
title: String!
description: String!
comments: [Comment]
}
2) buildSchema 대신 makeExecutableSchema를 사용한다.
const { loadFilesSync } = require('@graphql-tools/load-files');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const schemaString = `
// type Query {
// description: String
// }
// `;
// 모든 폴더(**) 속, .graphql로 끝나는 모든 파일(*)
const loadedTypes = loadFilesSync('**/*', {
extensions: ['graphql'],
});
const schema = makeExecutableSchema({
// typeDefs: [schemaString]
typeDefs: loadedTypes
});
분리하지 않은 경우 주석처럼 문자열로 스키마를 작성하고 typeDefs에 배열 형태로 넣어주어도 된다.
하지만 분리한 경우에는 프로젝트 내에서 .graphql 파일을 찾아 넣어주어야 한다. 이때 loadFilesSync 메소드를 사용한다.
설치는 npm install @graphql-tools/laod-files
3) rootValue(기본값) 분리
//comments/comments.model.js
module.exports = [
{
id: 'post1',
title: 'It is a first post',
description: 'It is a first post description',
comments: [{
id: 'comment1',
text: 'It is a first comment',
likes: 1
}]
},
{
id: 'post2',
title: 'It is a second post',
description: 'It is a second post description',
comments: []
}
]
//server.js
const root = {
posts: require('./posts/posts.model'),
comments: require('./comments/comments.model')
}
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true
}));
Resolver
Resolver는 스키마의 단일 필드에 대한 데이터를 채우는 역할을 하는 함수이다. 백엔드 데이터베이스 또는 타사 API에서 데이터를 가져오는 것과 같이 원하는 대로 정의한 방식으로 데이터를 채울 수 있다.
예를 들어 특정 카테고리의 상품, 특정 가격대의 상품만 필터링하는 기능을 원하면, 그 부분은 resolver에서 해준다.
1. Resolver 정의하기
resolver는 makeExecutableSchema 안에 정의해 줄 수 있다.
const schema = makeExecutableSchema({
typeDefs: loadedTypes,
resolvers: {
Query: {
posts: (parent, args, context, info) => {
return parent.posts;
}
}
}
});
resolver 함수의 인자로는 parent, args, context, info가 들어갈 수 있다. args가 주로 사용된다.
- parent: 이 필드의 부모, 즉 resolver 체인의 이전 resolve에 대한 resolver의 반환 값이다.
- args
- 이 필드에 제공된 모든 GraphQL 인수를 포함하는 객체이다.
- API를 호출할 때 posts (postID: 1) 같이 인수를 넣어줬을 때 args로 들어오게 된다.
- context
- 특정 작업에 대해 실행 중인 모든 resolver 같에 공유되는 객체이다.
- 인증 정보, 데이터 로더 인스턴스 및 리졸버에서 추적할 기타 항목을 포함하여 작업별 상태를 공유하는 데에 사용한다.
const resolvers = {
Query: {
adminExample: (parent, args, context, info) => {
if (context.authScope !== ADMIN) {
throw new GraphQLError('not admin!', {
extensions: { code: 'UNAUTHENTICATED' }
});
}
}
}
};
- info: 필드 이름, 루트에서 필드까지의 경로 등을 포함하여 작업의 실행 상태에 대한 정보를 포함한다.
resolver 함수에서 비동기 처리할 수 있게 할 수 있다.
Query: {
posts: async (parent) => {
const product = await Promise.resolve(parent.posts);
return product;
},
comments: (parent) => {
return parent.comments;
}
}
2. Resolver 모듈화
1) resolver 함수를 분리해 따로 파일을 만들어 정의하고, loadFilesSync로 resolver 함수를 가져온다.
//comments/comments.resolvers.js
module.exports = {
Query: {
comments: () => {
...
}
}
}
//servers.js
const { loadFilesSync } = require('@graphql-tools/load-files');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const path = require('path');
const loadedResolvers = loadFilesSync(path.join(__dirname, "**/*.resolvers.js"));
const schema = makeExecutableSchema({
typeDefs: loadedTypes,
resolvers: loadedResolvers
});
2) model 함수를 만들어 로직을 작성한다.
로직은 따로 함수로 만들어 처리하여 resolver 파일을 간단하게 만들어준다.
//comments/comments.model.js
const comments = [
{
id: 'comment1',
text: 'It is a first comment',
likes: 1
}
];
function getAllComments() {
return comments;
}
module.exports = {
getAllComments,
}
//comments/comments.resolvers.js
const commentModel = require('./comments.model');
module.exports = {
Query: {
comments: () => {
return commentModel.getAllComments();
}
}
}
Mutation
GraphQL에서 데이터를 읽어오는 것(Read)은 query를 통해 할 수 있다.
Create, Update, Delete는 어떻게 할 수 있을까? Mutation을 이용한다.
1) Mutation 스키마 작성
//posts.graphql
type Mutation {
addNewPost(id: ID!, title: String!, description: String!)
}
2) 대응하는 Resolver 생성
//posts/posts.model.js
const posts = [...];
function addNewPost(id, title, description) {
const newPost = {
id,
title,
description,
comments: []
};
posts.push(newPost);
return newPost;
}
module.exports = {
...
addNewPost,
}
//posts.resolvers.js
const postModel = require('./posts.model');
module.exports = {
Query: {
...
},
Mutation: {
addNewPost: (_, args) => {
return postModel.addNewPost(args.id, args.title, args.description);
}
}
}
Apollo
GraphQL을 클라이언트, 서버 모두에서 편하게 사용할 수 있게 도와주는 라이브러리
Apollo Client을 사용하면 쿼리 캐싱, loading 상태 및 Error 처리, 서버와 데이터 동기화 유지가 가능하다.
Apollo Server는 Apollo 클라이언트를 포함한 모든 GraphQL 클라이언트와 호환되는 오픈 소스 GraphQL 서버이다. 모든 소스의 데이터를 사용할 수 있는 자체 문서화 가능한 GraphQL API를 구축하는 가장 좋은 방법이다.
Express를 사용한 기존 프로젝트를 Apollo v3로 바꿔보도록 하자.
1) apollo-server-express 패키지 설치: npm install apollo-server-express
https://www.apollographql.com/docs/apollo-server/v3/integrations/middleware
프로젝트에 따라 apollo-server, apollo-server-lambda 등 다른 패키지를 설치할 수 있으니 위 페이지를 참고하자.
2) server.js 파일 작성
const { loadFilesSync } = require('@graphql-tools/load-files');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { ApolloServer } = require('apollo-server-express');
const express = require('express');
const path = require('path');
const port = 4000;
// 모든 폴더(**) 속, .graphql로 끝나는 모든 파일(*)
const loadedTypes = loadFilesSync('**/*', {
extensions: ['graphql'],
});
const loadedResolvers = loadFilesSync(path.join(__dirname, "**/*.resolvers.js"))
async function startApolloServer() {
const app = express();
const schema = makeExecutableSchema({
typeDefs: loadedTypes,
resolvers: loadedResolvers
});
// This Apollo server object contains all the middleware and
// logic to handle incoming graphical requests.
const server = new ApolloServer({
schema
});
await server.start();
// Connect apollo middleware with express server
server.applyMiddleware({ app, path: '/graphql' });
app.listen(port, () => {
console.log('Running a GraphQL API server...');
});
}
startApolloServer();
node server.js로 서버를 실행하고 localhost:4000/graphql에 들어가면
Query your server을 누르면 Apollo를 위한 GraphiQL과 비슷한 형태의 Apollo Studio를 사용할 수 있다.
Apollo v4로 migration
https://www.apollographql.com/docs/apollo-server/migration/
Migrating to Apollo Server 4
www.apollographql.com
1) apollo-server-express 패키지를 지우고 @apollo/server, cors, body-parser 패키지를 설치한다.
패키지를 설치할 때 에러가 발생했는데, 그냥 강제로 설치해버린다.
2) server.js 작성
//server.js
const { loadFilesSync } = require('@graphql-tools/load-files');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const path = require('path');
const express = require('express');
const { ApolloServer } = require('@apollo/server');
const cors = require('cors');
const { json } = require('body-parser');
const { expressMiddleware } = require('@apollo/server/express4');
const port = 4000;
// 모든 폴더(**) 속, .graphql로 끝나는 모든 파일(*)
const loadedTypes = loadFilesSync('**/*', {
extensions: ['graphql'],
});
const loadedResolvers = loadFilesSync(path.join(__dirname, "**/*.resolvers.js"))
async function startApolloServer() {
const app = express();
const schema = makeExecutableSchema({
typeDefs: loadedTypes,
resolvers: loadedResolvers
});
// This Apollo server object contains all the middleware and
// logic to handle incoming graphical requests.
const server = new ApolloServer({
schema
});
await server.start();
// Connect apollo middleware with express server
app.use(
'/graphql',
cors(),
json(),
expressMiddleware(server, {
context: async ({ req }) => ({ token: req.headers.token })
})
);
app.listen(port, () => {
console.log('Running a GraphQL API server...');
});
}
startApolloServer();
'웹 개발' 카테고리의 다른 글
비동기 프로그래밍 (0) | 2024.03.08 |
---|---|
Virtual DOM과 (Real) DOM의 차이 (0) | 2024.03.05 |
localStorage와 sessionStorage (0) | 2023.08.20 |
마크다운 파일(.md)을 데이터로 추출 및 HTML로 변환 (0) | 2023.07.10 |
Greedy Algorithm (0) | 2023.07.05 |