본문 바로가기
웹 개발

GraphQL 기초

by xosoy 2023. 9. 18.

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에서 해준다.

https://lab.wallarm.com/why-and-how-to-disable-introspection-query-for-graphql-apis/

 

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);
    }
  }
}

 

GraphiQL에서 확인한 결과

 

 


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에 들어가면

localhost:4000/graphql

Query your server을 누르면 Apollo를 위한 GraphiQL과 비슷한 형태의 Apollo Studio를 사용할 수 있다.

https://studio.apollographql.com/sandbox/explorer

 

 

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