첫 번째 GraphQL API 만들기

게시 됨: 2022-03-11

머리말

몇 년 전 Facebook은 기본적으로 데이터 쿼리 및 조작을 위한 도메인별 언어인 GraphQL이라는 백엔드 API를 빌드하는 새로운 방법을 도입했습니다. 처음에는 별로 관심을 두지 않았지만 결국 Toptal에서 GraphQL을 기반으로 백엔드 API를 구현해야 하는 프로젝트에 참여하게 되었습니다. 그 때 REST에 대해 배운 지식을 GraphQL에 적용하는 방법을 배웠습니다.

매우 흥미로운 경험이었고 구현 기간 동안 REST API에서 사용되는 표준 접근 방식과 방법론을 보다 GraphQL 친화적인 방식으로 재고해야 했습니다. 이 기사에서는 GraphQL API를 처음 구현할 때 고려해야 할 일반적인 문제를 요약하려고 합니다.

필요한 라이브러리

GraphQL은 Facebook에서 내부적으로 개발했으며 2015년에 공개되었습니다. 2018년 후반에 GraphQL 프로젝트는 Facebook에서 새로 설립된 GraphQL Foundation으로 이동되었으며, 이 재단은 GraphQL 쿼리 언어 사양과 참조를 유지 관리하고 개발하는 비영리 Linux Foundation에서 호스팅합니다. 자바스크립트 구현.

GraphQL은 아직 젊은 기술이고 초기 참조 구현이 JavaScript에 사용 가능했기 때문에 이에 대한 가장 성숙한 라이브러리는 Node.js 에코시스템에 존재합니다. GraphQL용 오픈 소스 도구와 라이브러리를 제공하는 Apollo와 Prisma라는 두 회사도 있습니다. 이 기사의 예제 프로젝트는 JavaScript용 GraphQL의 참조 구현과 다음 두 회사에서 제공하는 라이브러리를 기반으로 합니다.

  • Graphql-js – JavaScript용 GraphQL의 참조 구현
  • Apollo-server – Express, Connect, Hapi, Koa 등을 위한 GraphQL 서버
  • Apollo-graphql-tools – SDL을 사용하여 GraphQL 스키마 빌드, 조롱 및 스티칭
  • Prisma-graphql-middleware – 미들웨어 기능에서 GraphQL 리졸버 분할

GraphQL 세계에서는 GraphQL 스키마를 사용하여 API를 설명하고, 이를 위해 사양은 The GraphQL SDL(스키마 정의 언어)이라는 자체 언어를 정의합니다. SDL은 사용이 매우 간단하고 직관적인 동시에 매우 강력하고 표현력이 뛰어납니다.

GraphQL 스키마를 생성하는 방법에는 코드 우선 접근 방식과 스키마 우선 접근 방식의 두 가지가 있습니다.

  • 코드 우선 접근 방식에서는 GraphQL 스키마를 graphql-js 라이브러리를 기반으로 하는 JavaScript 객체로 설명하고 SDL은 소스 코드에서 자동 생성됩니다.
  • 스키마 우선 접근 방식에서는 SDL에서 GraphQL 스키마를 설명하고 Apollo graphql-tools 라이브러리를 사용하여 비즈니스 로직을 연결합니다.

개인적으로 나는 스키마 우선 접근 방식을 선호하며 이 기사의 샘플 프로젝트에 사용할 것입니다. 우리는 고전적인 서점 예제를 구현하고 저자와 책을 만들기 위한 CRUD API와 사용자 관리 및 인증을 위한 API를 제공하는 백엔드를 만들 것입니다.

기본 GraphQL 서버 만들기

기본 GraphQL 서버를 실행하려면 새 프로젝트를 생성하고 npm으로 초기화하고 Babel을 구성해야 합니다. Babel을 구성하려면 먼저 다음 명령을 사용하여 필요한 라이브러리를 설치합니다.

 npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node

Babel을 설치한 후 프로젝트의 루트 디렉터리에 이름이 .babelrc 인 파일을 만들고 다음 구성을 복사합니다.

 { "presets": [ [ "@babel/env", { "targets": { "node": "current" } } ] ] }

또한 package.json 파일을 편집하고 scripts 섹션에 다음 명령을 추가합니다.

 { ... "scripts": { "serve": "babel-node index.js" }, ... }

Babel을 구성했으면 다음 명령을 사용하여 필요한 GraphQL 라이브러리를 설치합니다.

 npm install --save express apollo-server-express graphql graphql-tools graphql-tag

필요한 라이브러리를 설치한 후 최소한의 설정으로 GraphQL 서버를 실행하려면 다음 코드 조각을 index.js 파일에 복사하세요.

 import gql from 'graphql-tag'; import express from 'express'; import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'; const port = process.env.PORT || 8080; // Define APIs using GraphQL SDL const typeDefs = gql` type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } }; // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolvers maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });

그런 다음 npm run serve 명령을 사용하여 서버를 실행할 수 있습니다. 웹 브라우저에서 URL http://localhost:8080/graphql 로 이동하면 Playground라고 하는 GraphQL의 대화형 시각적 셸이 열립니다. GraphQL 쿼리 및 변형을 실행하고 결과 데이터를 확인합니다.

GraphQL 세계에서 API 함수는 쿼리, 변형 및 구독이라는 세 가지 세트로 나뉩니다.

  • 쿼리 는 클라이언트가 서버에서 필요한 데이터를 요청하는 데 사용됩니다.
  • 돌연변이 는 클라이언트가 서버에서 데이터를 생성/업데이트/삭제하는 데 사용됩니다.
  • 구독 은 클라이언트에서 서버에 대한 실시간 연결을 만들고 유지하는 데 사용됩니다. 이렇게 하면 클라이언트가 서버에서 이벤트를 가져와 그에 따라 작동할 수 있습니다.

우리 기사에서는 쿼리와 돌연변이에 대해서만 논의할 것입니다. 구독은 엄청난 주제입니다. 자체 기사가 필요하며 모든 API 구현에 필수는 아닙니다.

고급 스칼라 데이터 유형

GraphQL을 사용하고 얼마 지나지 않아 SDL이 기본 데이터 유형만 제공하고 모든 API의 중요한 부분인 Date, Time, DateTime과 같은 고급 스칼라 데이터 유형이 누락되었음을 발견하게 될 것입니다. 다행히도 이 문제를 해결하는 데 도움이 되는 라이브러리가 있으며 이를 graphql-iso-date라고 합니다. 설치 후 스키마에서 새로운 고급 스칼라 데이터 유형을 정의하고 라이브러리에서 제공하는 구현에 연결해야 합니다.

 import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date'; // Define APIs using GraphQL SDL const typeDefs = gql` scalar Date scalar Time scalar DateTime type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Date: GraphQLDate, Time: GraphQLTime, DateTime: GraphQLDateTime, Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } };

날짜 및 시간과 함께 사용 사례에 따라 유용할 수 있는 다른 흥미로운 스칼라 데이터 유형 구현도 있습니다. 예를 들어 그 중 하나는 graphql-type-json으로, GraphQL 스키마에서 동적 유형 지정을 사용하고 API를 사용하여 유형이 지정되지 않은 JSON 객체를 전달하거나 반환할 수 있는 기능을 제공합니다. 또한 라이브러리 graphql-scalar가 있어 고급 살균/검증/변환으로 사용자 정의 GraphQL 스칼라를 정의할 수 있습니다.

필요한 경우 위에 표시된 대로 사용자 지정 스칼라 데이터 유형을 정의하고 스키마에서 사용할 수도 있습니다. 이것은 어렵지 않지만 이에 대한 논의는 이 기사의 범위를 벗어납니다. 관심이 있는 경우 Apollo 문서에서 고급 정보를 찾을 수 있습니다.

분할 스키마

스키마에 더 많은 기능을 추가하면 스키마가 확장되기 시작하고 전체 정의 세트를 하나의 파일에 유지하는 것이 불가능하다는 것을 이해하게 되며 코드를 구성하고 확장성을 높이기 위해 이를 작은 조각으로 분할해야 합니다. 더 큰 크기. 다행히도 Apollo에서 제공하는 스키마 빌더 함수 makeExecutableSchema 는 배열 형태의 스키마 정의와 리졸버 맵도 허용합니다. 이를 통해 스키마와 리졸버 맵을 더 작은 부분으로 분할할 수 있습니다. 이것은 정확히 내가 샘플 프로젝트에서 수행한 작업입니다. API를 다음 부분으로 나누었습니다.

  • auth.api.graphql – 사용자 인증 및 등록을 위한 API
  • author.api.graphql – 작성자 항목을 위한 CRUD API
  • book.api.graphql – 도서 항목을 위한 CRUD API
  • root.api.graphql – 스키마 및 일반 정의의 루트(예: 고급 스칼라 유형)
  • user.api.graphql – 사용자 관리를 위한 CRUD API

분할 스키마 동안 고려해야 할 한 가지 사항이 있습니다. 부분 중 하나는 루트 스키마여야 하고 다른 부분은 루트 스키마를 확장해야 합니다. 이것은 복잡하게 들리지만 실제로는 매우 간단합니다. 루트 스키마에서 쿼리 및 변형은 다음과 같이 정의됩니다.

 type Query { ... } type Mutation { ... }

그리고 다른 것들에서는 다음과 같이 정의됩니다.

 extend type Query { ... } extend type Mutation { ... }

그리고 그게 전부입니다.

인증 및 권한 부여

대부분의 API 구현에는 전역 액세스를 제한하고 일종의 규칙 기반 액세스 정책을 제공해야 하는 요구 사항이 있습니다. 이를 위해 우리는 코드에 인증 (사용자 ID 확인을 위한 인증)과 규칙 기반 액세스 정책을 시행하기 위한 권한 부여 를 도입해야 합니다.

REST 세계와 마찬가지로 GraphQL 세계에서는 일반적으로 인증을 위해 JSON 웹 토큰을 사용합니다. 전달된 JWT 토큰의 유효성을 검사하려면 들어오는 모든 요청을 가로채서 승인 헤더를 확인해야 합니다. 이를 위해 Apollo 서버를 생성하는 동안 함수를 컨텍스트 훅으로 등록할 수 있습니다. 이 훅은 모든 리졸버에서 공유되는 컨텍스트를 생성하는 현재 요청과 함께 호출됩니다. 이것은 다음과 같이 할 수 있습니다:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, context: ({ req, res }) => { const context = {}; // Verify jwt token const parts = req.headers.authorization ? req.headers.authorization.split(' ') : ['']; const token = parts.length === 2 && parts[0].toLowerCase() === 'bearer' ? parts[1] : undefined; context.authUser = token ? verify(token) : undefined; return context; } }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });

여기에서 사용자가 올바른 JWT 토큰을 전달하면 이를 확인하고 컨텍스트에 사용자 개체를 저장합니다. 그러면 요청 실행 중에 모든 해석기가 액세스할 수 있습니다.

사용자 ID를 확인했지만 API는 여전히 전 세계적으로 액세스할 수 있으며 사용자가 승인 없이 API를 호출하는 것을 막는 것은 없습니다. 이를 방지하는 한 가지 방법은 모든 리졸버에서 컨텍스트에서 사용자 객체를 직접 확인하는 것이지만 이는 많은 상용구 코드를 작성해야 하고 새 리졸버를 추가할 때 확인을 추가하는 것을 잊어버릴 수 있기 때문에 매우 오류가 발생하기 쉬운 접근 방식입니다. . REST API 프레임워크를 살펴보면 일반적으로 이러한 종류의 문제는 HTTP 요청 인터셉터를 사용하여 해결되지만 GraphQL의 경우 하나의 HTTP 요청에 여러 GraphQL 쿼리가 포함될 수 있기 때문에 의미가 없습니다. 쿼리의 원시 문자열 표현에만 액세스할 수 있고 수동으로 구문 분석해야 합니다. 이는 확실히 좋은 접근 방식이 아닙니다. 이 개념은 REST에서 GraphQL로 잘 번역되지 않습니다.

그래서 GraphQL 쿼리를 가로채기 위한 일종의 방법이 필요하며 이 방법을 prisma-graphql-middleware라고 합니다. 이 라이브러리를 사용하면 리졸버가 호출되기 전이나 후에 임의의 코드를 실행할 수 있습니다. 코드 재사용과 우려 사항의 명확한 분리를 가능하게 하여 코드 구조를 개선합니다.

GraphQL 커뮤니티는 이미 특정 사용 사례를 해결하는 Prisma 미들웨어 라이브러리를 기반으로 하는 멋진 미들웨어를 많이 만들었으며 사용자 권한 부여를 위해 API에 대한 권한 계층을 만드는 데 도움이 되는 graphql-shield라는 라이브러리가 있습니다.

graphql-shield를 설치한 후 다음과 같이 API에 대한 권한 계층을 도입할 수 있습니다.

 import { allow } from 'graphql-shield'; const isAuthorized = rule()( (obj, args, { authUser }, info) => authUser && true ); export const permissions = { Query: { '*': isAuthorized, sayHello: allow }, Mutation: { '*': isAuthorized, sayHello: allow } }

그리고 이 레이어를 다음과 같이 스키마에 미들웨어로 적용할 수 있습니다.

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

여기에서 쉴드 객체를 생성할 때 우리는 allowExternalErrors 를 true로 설정했습니다. 기본적으로 쉴드의 동작은 리졸버 내부에서 발생하는 오류를 포착하고 처리하는 것이고 이것은 내 샘플 애플리케이션에서 허용되지 않는 동작이기 때문입니다.

위의 예에서는 인증된 사용자에 대해서만 API에 대한 액세스를 제한했지만 쉴드는 매우 유연하며 이를 사용하여 사용자를 위한 매우 풍부한 권한 부여 스키마를 구현할 수 있습니다. 예를 들어 샘플 애플리케이션에는 USERUSER_MANAGER 두 가지 역할이 있으며 역할이 USER_MANAGER 인 사용자만 사용자 관리 기능을 호출할 수 있습니다. 이것은 다음과 같이 구현됩니다.

 export const isUserManager = rule()( (obj, args, { authUser }, info) => authUser && authUser.role === 'USER_MANAGER' ); export const permissions = { Query: { userById: isUserManager, users: isUserManager }, Mutation: { editUser: isUserManager, deleteUser: isUserManager } }

한 가지 더 언급하고 싶은 것은 프로젝트에서 미들웨어 기능을 구성하는 방법입니다. 스키마 정의 및 리졸버 맵과 마찬가지로 스키마별로 분할하여 별도의 파일에 보관하는 것이 더 낫지만 스키마 정의 및 리졸버 맵의 배열을 수락하고 이를 스티칭하는 Apollo 서버와 달리 Prisma 미들웨어 라이브러리는 이를 수행하지 않으며 하나의 미들웨어 맵 개체만 허용하므로 이를 분할하면 수동으로 다시 연결해야 합니다. 이 문제에 대한 내 솔루션을 보려면 샘플 프로젝트의 ApiExplorer 클래스를 참조하십시오.

확인

GraphQL SDL은 사용자 입력을 검증하기 위해 매우 제한된 기능을 제공합니다. 필수 필드와 선택 필드만 정의할 수 있습니다. 추가 유효성 검사 요구 사항은 수동으로 구현해야 합니다. 검증 규칙을 확인자 함수에 직접 적용할 수 있지만 이 기능은 실제로 여기에 속하지 않으며 이것은 사용자 GraphQL 미들웨어에 대한 또 다른 훌륭한 사용 사례입니다. 예를 들어 사용자 이름이 올바른 이메일 주소인지, 비밀번호 입력이 일치하는지, 비밀번호가 충분히 강력한지 검증해야 하는 사용자 가입 요청 입력 데이터를 사용하겠습니다. 이것은 다음과 같이 구현할 수 있습니다.

 import { UserInputError } from 'apollo-server-express'; import passwordValidator from 'password-validator'; import { isEmail } from 'validator'; const passwordSchema = new passwordValidator() .is().min(8) .is().max(20) .has().letters() .has().digits() .has().symbols() .has().not().spaces(); export const validators = { Mutation: { signup: (resolve, parent, args, context) => { const { email, password, rePassword } = args.signupReq; if (!isEmail(email)) { throw new UserInputError('Invalid Email address!'); } if (password !== rePassword) { throw new UserInputError('Passwords don\'t match!'); } if (!passwordSchema.validate(password)) { throw new UserInputError('Password is not strong enough!'); } return resolve(parent, args, context); } } }

그리고 다음과 같은 권한 레이어와 함께 유효성 검사기 레이어를 스키마에 미들웨어로 적용할 수 있습니다.

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, validators, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app })

N + 1 쿼리

GraphQL API에서 발생하고 종종 간과되는 고려해야 할 또 다른 문제는 N + 1 쿼리입니다. 이 문제는 스키마에 정의된 유형 간에 일대다 관계가 있을 때 발생합니다. 예를 들어 이를 보여주기 위해 샘플 프로젝트의 book API를 사용하겠습니다.

 extend type Query { books: [Book!]! ... } extend type Mutation { ... } type Book { id: ID! creator: User! createdAt: DateTime! updatedAt: DateTime! authors: [Author!]! title: String! about: String language: String genre: String isbn13: String isbn10: String publisher: String publishDate: Date hardcover: Int } type User { id: ID! createdAt: DateTime! updatedAt: DateTime! fullName: String! email: String! }

여기에서 User 유형은 Book 유형과 일대다 관계를 갖고 있으며 이 관계는 Book 의 작성자 필드로 표시됩니다. 이 스키마에 대한 해석기 맵은 다음과 같이 정의됩니다.

 export const resolvers = { Query: { books: (obj, args, context, info) => { return bookService.findAll(); }, ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { return userService.findById(creatorId); }, ... } }

이 API를 사용하여 책 쿼리를 실행하고 SQL 문 로그를 보면 다음과 같은 내용을 볼 수 있습니다.

 select `books`.* from `books` select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? ...

쉽게 추측할 수 있습니다. 실행하는 동안 책 목록을 반환하는 책 쿼리에 대해 해석기가 먼저 호출된 다음 각 책 개체를 작성자 필드 해석기라고 했으며 이 동작으로 인해 N + 1개의 데이터베이스 쿼리가 발생했습니다. 우리가 데이터베이스를 폭발시키고 싶지 않다면 그러한 종류의 행동은 그다지 좋지 않습니다.

N + 1 쿼리 문제를 해결하기 위해 Facebook 개발자는 DataLoader라는 매우 흥미로운 솔루션을 만들었습니다. 이 솔루션은 README 페이지에 다음과 같이 설명되어 있습니다.

"DataLoader는 일괄 처리 및 캐싱을 통해 데이터베이스 또는 웹 서비스와 같은 다양한 원격 데이터 소스에 대해 단순하고 일관된 API를 제공하기 위해 애플리케이션의 데이터 가져오기 계층의 일부로 사용되는 일반 유틸리티입니다."

DataLoader가 어떻게 작동하는지 이해하는 것은 그리 간단하지 않으므로 위에서 설명한 문제를 해결하는 예제를 먼저 보고 그 뒤에 있는 논리를 설명하겠습니다.

샘플 프로젝트에서 DataLoader는 작성자 필드에 대해 다음과 같이 정의됩니다.

 export class UserDataLoader extends DataLoader { constructor() { const batchLoader = userIds => { return userService .findByIds(userIds) .then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) ); }; super(batchLoader); } static getInstance(context) { if (!context.userDataLoader) { context.userDataLoader = new UserDataLoader(); } return context.userDataLoader; } }

UserDataLoader를 정의했으면 다음과 같이 작성자 필드의 해석기를 변경할 수 있습니다.

 export const resolvers = { Query: { ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { const userDataLoader = UserDataLoader.getInstance(context); return userDataLoader.load(creatorId); }, ... } }

변경 사항을 적용한 후 books 쿼리를 다시 실행하고 SQL 문 로그를 보면 다음과 같은 내용을 볼 수 있습니다.

 select `books`.* from `books` select `users`.* from `users` where `id` in (?)

여기에서 N + 1 데이터베이스 쿼리가 두 개의 쿼리로 축소되었음을 알 수 있습니다. 첫 번째 쿼리는 책 목록을 선택하고 두 번째 쿼리는 책 목록에서 작성자로 표시되는 사용자 목록을 선택합니다. 이제 DataLoader가 이 결과를 얻는 방법을 설명하겠습니다.

DataLoader의 주요 기능은 일괄 처리입니다. 단일 실행 단계에서 DataLoader는 모든 개별 로드 함수 호출의 모든 고유 ID를 수집한 다음 요청된 모든 ID로 배치 함수를 호출합니다. 기억해야 할 한 가지 중요한 점은 DataLoaders의 인스턴스는 재사용할 수 없다는 것입니다. 배치 함수가 호출되면 반환된 값은 인스턴스에 영원히 캐시됩니다. 이 동작으로 인해 각 실행 단계마다 DataLoader의 새 인스턴스를 생성해야 합니다. 이를 달성하기 위해 DataLoader의 인스턴스가 컨텍스트 개체에 있는지 확인하고 발견되지 않으면 새로 만드는 정적 getInstance 함수를 만들었습니다. 각 실행 단계에 대해 새 컨텍스트 개체가 생성되고 모든 해석기에서 공유된다는 점을 기억하십시오.

DataLoader의 일괄 로드 기능은 요청된 고유 ID의 배열을 수락하고 해당 개체의 배열로 확인되는 약속을 반환합니다. 일괄 로딩 함수를 작성할 때 두 가지 중요한 사항을 기억해야 합니다.

  1. 결과 배열은 요청된 ID 배열과 길이가 같아야 합니다. 예를 들어, ID [1, 2, 3] 을 요청한 경우 반환된 결과 배열에는 정확히 세 개의 객체가 포함되어야 합니다. [{ "id": 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }, { “id”: 3, “fullName”: “user3” }]
  2. 결과 배열의 각 인덱스는 요청된 ID 배열의 동일한 인덱스와 일치해야 합니다. 예를 들어, 요청된 ID 배열의 순서가 [3, 1, 2] 인 경우 반환된 결과 배열은 [{ "id": 3, “fullName”: “user3” }, { “id”: 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }]

이 예에서는 결과 순서가 다음 코드를 사용하여 요청된 ID 순서와 일치하는지 확인합니다.

 then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) )

보안

그리고 마지막으로 보안에 대해 언급하고 싶습니다. GraphQL을 사용하면 매우 유연한 API를 만들고 사용자에게 데이터 쿼리 방법에 대한 풍부한 기능을 제공할 수 있습니다. 이것은 응용 프로그램의 클라이언트 측에 상당한 권한을 부여하고 Ben 삼촌이 말했듯이 "큰 권한에는 큰 책임이 따릅니다." 적절한 보안이 없으면 악의적인 사용자가 값비싼 쿼리를 제출하고 우리 서버에 DoS(서비스 거부) 공격을 일으킬 수 있습니다.

API를 보호하기 위해 가장 먼저 할 수 있는 일은 GraphQL 스키마의 내부 검사를 비활성화하는 것입니다. 기본적으로 GraphQL API 서버는 GraphiQL 및 Apollo Playground와 같은 대화형 시각적 셸에서 일반적으로 사용되는 전체 스키마를 검사하는 기능을 제공하지만 악의적인 사용자가 API를 기반으로 복잡한 쿼리를 구성하는 데에도 매우 유용할 수 있습니다. . Apollo 서버를 생성할 때 introspection 매개변수를 false로 설정하여 이를 비활성화할 수 있습니다.

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

API를 보호하기 위해 다음으로 할 수 있는 일은 쿼리의 깊이를 제한하는 것입니다. 이는 데이터 유형 간에 순환 관계가 있는 경우 특히 중요합니다. 예를 들어 이 샘플에서 프로젝트 유형 Author 에는 야장 책이 있고 Book 유형에는 야전 저자가 있습니다. 이것은 분명히 순환적인 관계이며 악의적인 사용자가 다음과 같은 쿼리를 작성하는 것을 막는 것은 없습니다.

 query { authors { id, fullName books { id, title authors { id, fullName books { id, title, authors { id, fullName books { id, title authors { ... } } } } } } } }

중첩이 충분하면 이러한 쿼리가 서버를 쉽게 폭발시킬 수 있습니다. 쿼리의 깊이를 제한하기 위해 graphql-depth-limit라는 라이브러리를 사용할 수 있습니다. 설치가 완료되면 다음과 같이 Apollo Server를 생성할 때 깊이 제한을 적용할 수 있습니다.

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false, validationRules: [ depthLimit(5) ] }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

여기서는 최대 쿼리 깊이를 5로 제한했습니다.

Post Scriptum: REST에서 GraphQL로의 전환은 흥미롭습니다.

이 튜토리얼에서는 GraphQL API 구현을 시작할 때 만날 수 있는 일반적인 문제를 보여주려고 했습니다. 그러나 그 중 일부는 매우 얕은 코드 예제를 제공하고 크기 때문에 논의된 문제의 표면만 긁습니다. 이 때문에 더 완전한 코드 예제를 보려면 내 샘플 GraphQL API 프로젝트의 Git 저장소인 graphql-example을 참조하십시오.

결국 GraphQL은 정말 흥미로운 기술이라고 말하고 싶습니다. REST를 대체할 것인가? 아무도 모릅니다. 아마도 내일 빠르게 변화하는 IT 세계에서 API를 개발하는 더 나은 접근 방식이 나타날 것입니다. 그러나 GraphQL은 실제로 배울 가치가 있는 흥미로운 기술 범주에 속합니다.