728x90
웹 앱 API 개발을 위한 GraphQL 을 요약한 내용입니다.
- 서비스에 적용할 수 있는 기술 스택은 다양하나, 이 책에서는 자바스크립트를 사용하겠습니다.
- 상당히 범용적인 내용을 배우기 때문에 나중에 다른 언어나 프레임워크를 쓰더라도 GraphQL 서비스의 전반적인 설계는 비슷해 보일 것입니다.
- 다른 언어로 된 라이브러리에 관심이 있다면 GraphQL.org(https://graphql.org/) 사이트를 참조하세요
- 2015년에 GraphQL 명세가 처음 공개되었습니다.
- 서버 세부 구현 사항은 의도적으로 명세에서 생략했기 때문에 다양한 배경의 개발자들이 본인이 사용하기 편한 언어를 가지고 GraphQL을 사용할 수 있었습니다.
- 페이스북 팀에서는 자바스크립트로 만든 레퍼런스 코드인 GraphQL.js를 공개했습니다.
- 이 소스를 바탕으로 express-graphql이 만들어 졌습니다.
- 이 책에서는 아폴로 팀에서 만든 오픈 소스 솔루션인 아폴로 서버를 사용하기로 했습니다.
- 설정이 상당히 편하고 프로덕션 레벨에서 사용할 수 있는 여러 가지 기능을 제공하기 때문입니다.
- 서브스크립션과 파일 업로드 기능을 제공합니다.
- 데이터 소스 API로 이미 사용 중인 기존 서비스에 접근할 수 있습니다.
- 아폴로 엔진을 손쉽고 간편하게 그리고 독립적으로 사용할 수 있습니다.
- GraphQL 플레이그라운드도 제공하므로 브라우저에서 쿼리를 바로 작성할 수 있습니다.
5.1 프로젝트 세팅
- 로컬에 빈 폴더를 하나 만들어 photo-share-api 프로젝트를 시작해 봅시다.
- 터미널에 npm init -y 명령을 입력해 빈 폴더에 npm 프로젝트를 새로 만듭니다.
- npm init -y
- 프로젝트에서 사용할 의존 모듈인 apollo-server, graphql, nodemon을 설치 합니다.
- npm install apollo-server graphql nodemon
- package.json 파일 안에 scripts 필드의 키 값으로 nodemone 관련 명령어를 작성합니다.
- "start": "nodemon -e js,json,graphql"
{ "name": "graph_ql_study", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "nodemon -e js,json,graphql", "test": "echo \\"Error: no test specified\\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/clghks/graphQL_study.git" }, "keywords": [], "author": "", "license": "ISC", "bugs": { "url": "<https://github.com/clghks/graphQL_study/issues>" }, "homepage": "<https://github.com/clghks/graphQL_study#readme>", "dependencies": { "apollo-server": "^2.18.2", "graphql": "^15.3.0", "nodemon": "^2.0.5" } }
- 프로젝트 루트에 index.js 파일도 생성합니다.
- package.json의 main에 index.js를 써줍니다.
- { "name": "graph_ql_study", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "nodemon -e js,json,graphql", "test": "echo \\"Error: no test specified\\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/clghks/graphQL_study.git" }, "keywords": [], "author": "", "license": "ISC", "bugs": { "url": "<https://github.com/clghks/graphQL_study/issues>" }, "homepage": "<https://github.com/clghks/graphQL_study#readme>", "dependencies": { "apollo-server": "^2.18.2", "graphql": "^15.3.0", "nodemon": "^2.0.5" } }
nodemon은 파일에서 바뀐 부분이 없는지를 감시하면서 변경 사항이 생길 때마다 서버를 재시작 합니다.
nodemon은 js. json, graphql 확장자 파일에서 생기는 변경 사항을 감시합니다.
5.2 리졸버(Resolver)
- 스키마에는 사용자가 작성할 수 있는 쿼리를 정의해 두고, 타입 간의 연관 관계를 적어 둡니다.
- 데이터 요구 사항에 대한 내용은 들어 있지만 실제로 데이터를 가져오는 일은 스키마가 아닌 리졸버의 몫입니다.
- 리졸버는 특정 필드의 데이터를 반환하는 함수입니다.
- 스키마에 정의된 타입과 형태(Shape)에 따라 데이터를 반환합니다.
- 비동기로 작성할 수 있으며, REST API, 데이터베이스, 혹은 기타 서비스의 데이터를 가져오거나 업데이트 작업을 할 수 있습니다.
- index.js 파일의 Query에 totalPhotos 필드를 추가합니다.
- typeDefs 변수에 스키마를 문자열 형태로 정의합니다.
- totalPhotos 같은 쿼리를 작성하려면 쿼리와 같은 이름을 가진 리졸버 함수가 반드시 있어야합니다.
- 타입 정의하는 곳에 필드에서 반환하는 데이터 타입을 적습니다.
- 그러면 리졸버 함수가 그 타입에 맞는 데이터를 어딘가에서 가져다가 반환해 줍니다.
- 리졸버 함수는 반드시 스키마 객체와 같은 typename을 가진 객체 아래 정의해 두어야 합니다.
- totalPhotos 필드는 쿼리 객체(type Query)에 속합니다. 이 필드에 댕으하는 리졸버 함수는 resolvers 객체의 Query 안에 들어 있어야합니다.
const typeDefs = `
type Query {
totalPhotos: Int!
}
`
const resolvers = {
Query: {
totalPhotos: () => 42
}
}
- 스키마를 생성하고 이에 관한 쿼리를 요청할 수 있는 환경을 갖추기 위해 아폴로 서버를 사용합니다.
// 1. apollo-server 를 불러옵니다.
const { ApolloServer } = require('apollo-server')
const typeDefs = `
type Query {
totalPhotos: Int!
}
`
const resolvers = {
Query: {
totalPhotos: () => 42
}
}
// 2. 서버 인스턴스를 새로 만듭니다.
// 3. typeDefs(스키마)와 리졸버를 객체에 넣어 전달합니다.
const server = new ApolloServer({
typeDefs,
resolvers
})
// 4. 웹 서버를 구동하기 위해 listen 메서드를 호출합니다.
server.listen().then(({url}) => console.log(`GraphQL Service running on ${url}`))
- npm start 명령을 실행 후에 http://localhost:4000/에 접속합니다.
- GraphQL 플레이그라운드에 접속을 할 수 있습니다.
- totalPhotos를 요청하는 쿼리
{
totalPhotos
}
- 응답 데이터
{
"data": {
"totalPhotos": 42
}
}
- 리졸버는 GraphQL 구현의 핵심입니다. 모든 필드는 그에 대응하는 리졸버 함수가 있어야하며, 이들 함수는 스키마의 규칙을 따라야만 합니다.
- 함수는 스키마에 정의된 필드와 반드시 동일한 이름을 가져야 하며, 스키마에 정의된 데이터 타입을 반환합니다.
5.2.1 루트 리졸버
- Query, Mutation, Subscription 루트 타입을 가지고 있습니다.
- 최상단 레벨에 위치하며, 이들을 통해 사용 가능한 모든 API 엔트리 포인트를 표현할 수 있습니다.
- postPhoto라는 필드를 만들고, String 타입의 name과 description을 인자로 받습니다.
const typeDefs = `
type Query {
totalPhotos: Int!
}
type Mutation {
postPhoto(name: String! description: String): Boolean!
}
`
- postPhoto 뮤테이션을 생성 후, resolvers 객체에 이에 대응하는 리졸버 함수를 추가합니다.
- photos 라는 변수를 선언해 사진 정보를 담을 배열을 만듭니다.
- 데이터 베이스는 나중에 연동
- totalPhotos 리졸버 함수에서 photos 배열의 길이를 반환하도록 수정합니다.
- postPhoto 리졸버 함수를 추가합니다.
- 첫 번째 인자로 부모 객체에 대한 참조를 전달합니다.
- 가끔가다 문서에 _, root, obj 가 나올 때가 있습니다. 이 경우 postPhoto 리졸버 함수의 부모는 Mutation 입니다.
- 두 번째 인자는 바로 뮤테이션에 사용할 GraphQL 인자입니다.
- args 변수는 필드가 두개 들어 있는 객체 입니다. 지금은 ({name, description}) 인자가 사진 객체 하나에 해당하므로 photos 배열에 인자를 바로 넣도록 합니다.
- photos 라는 변수를 선언해 사진 정보를 담을 배열을 만듭니다.
const { ApolloServer } = require('apollo-server')
// 1. 메모리에 사진을 저장할 때 사용할 데이터 타입
var photos = []
const typeDefs = `
type Query {
totalPhotos: Int!
}
type Mutation {
postPhoto(name: String! description: String): Boolean!
}
`
const resolvers = {
Query: {
// 2. 사진 배열의 길이를 반환합니다.
totalPhotos: () => photos.length
},
// 3. Mutation & postPhoto 리졸버 함수
Mutation: {
postPhoto (parent, args) {
photos.push(args)
return true
}
}
}
// 2. 서버 인스턴스를 새로 만듭니다.
// 3. typeDefs(스키마)와 리졸버를 객체에 넣어 전달합니다.
const server = new ApolloServer({
typeDefs,
resolvers
})
// 4. 웹 서버를 구동하기 위해 listen 메서드를 호출합니다.
server.listen().then(({url}) => console.log(`GraphQL Service running on ${url}`))
- postPhoto 뮤테이션을 테스트하는 코드 입니다 .
mutation newPhoto {
postPhoto(name: "Test", description: "zzzz")
}
- 응답 데이터 입니다.
{
"data": {
"postPhoto": true
}
}
- 변수를 사용할 수 있도록 뮤테이션을 수정해 봅시다.
mutation newPhoto($name: String!, $description: String) {
postPhoto(name: $name, description: $description)
}
- 쿼리에 변수를 추가했다면 문자열 변수에 값 데이터를 반드시 전달해 주어야합니다.
{
"name": "sample photo A",
"description": "A sample photo for our dataset"
}
5.2.2 타입 리졸버
- 리졸버 함수에서는 정수, 문자열, 불 같은 스칼라 타입 값 말고도 객체 역시 반환할 수 있습니다.
- 사진 앱에서 Photo 타입을 만들고, allPhotos 쿼리 필드에서 Photo 객체 리스트를 반환하는 코드를 작성합니다.
- Photo 객체를 타입 정의에 추가합니다.
- allPhotos 쿼리를 타입 정의에 추가합니다.
- postPhoto 뮤테이션은 Photo 타입 형태의 데이터 객체 리스트를 반환합니다.
- Photo 타입은 ID가 필요함으로, ID 값을 저장할 변수를 하나 만듭니다.
- 보통은 서버에서 식별자나 타임스탬프 같은 변수를 생성해 ID로 사용합니다.
- ID 필드와 스프레드 연산자를 사용해 args에서 name과 description 필드를 받아 postPhoto 리졸버에서 새로운 사진 객체를 만듭니다.
- 뮤테이션에서 Boolean 값을 반환하는 대신에, Photo 타입 형태 객체를 반환하도록 만듭니다.
const { ApolloServer } = require('apollo-server')
// 4. 고유 ID를 만들기 위해 값을 하나씩 증가시킬 변수입니다.
var _id = 0
var photos = []
const typeDefs = `
# 1. Photo 타입 정의를 추가합니다.
type Photo {
id: ID!
url: String!
name: String!
description: String
}
# 2. allPhotos에서 Photo 타입을 반환합니다.
type Query {
totalPhotos: Int!
allPhotos: [Photo!]!
}
# 3. 뮤테이션에서 새로 게시된 사진을 반환합니다.
type Mutation {
postPhoto(name: String! description: String): Photo!
}
`
const resolvers = {
Query: {
totalPhotos: () => photos.length,
allPhotos: () => photos
},
Mutation: {
postPhoto (parent, args) {
// 5. 새로운 사진을 만들고 id를 부여합니다.
var newPhoto = {
id: _id++,
...args
}
photos.push(newPhoto)
// 6. 새로 만든 사진을 반환합니다.
return newPhoto
}
}
}
const server = new ApolloServer({
typeDefs,
resolvers
})
server.listen().then(({url}) => console.log(`GraphQL Service running on ${url}`))
- postPhoto 동작을 확인하기 위해 뮤테이션을 수정합니다.
mutation newPhoto($name: String!, $description: String) {
postPhoto(name: $name, description: $description) {
id
name
description
}
}
- 응답 결과 입니다.
{
"data": {
"postPhoto": {
"id": "0",
"name": "sample photo A",
"description": "A sample photo for our dataset"
}
}
}
- 사진을 몇장 추가했다면, allPhotos 쿼리는 생성한 Photo 객체를 전부 반환할 수 있어야 합니다.
query listPhotos{
allPhotos {
id
name
description
}
}
- 응답 결과 입니다.
{
"data": {
"allPhotos": [
{
"id": "0",
"name": "sample photo A",
"description": "A sample photo for our dataset"
},
{
"id": "1",
"name": "sample photo A",
"description": "A sample photo for our dataset"
}
]
}
}
- Photo 스키마에는 null 값이 될 수 없는 url 필드도 추가했습니다.
- 셀렉션 세트에 url을 넣으면 어떻게 될까요?
query listPhotos{
allPhotos {
id
name
description
url
}
}
- 오류 응답 결과 입니다.
- 아직 URL 필드를 데이터 셋트에 추가하지 않았습니다.
{
"errors": [
{
"message": "Cannot return null for non-nullable field Photo.url.",
"locations": [
{
"line": 6,
"column": 5
}
],
"path": [
"allPhotos",
0,
"url"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"Error: Cannot return null for non-nullable field Photo.url.",
" at completeValue (C:\\\\graph_ql_workspaces\\\\graph_ql_study\\\\node_modules\\\\graphql\\\\execution\\\\execute.js:595:13)",
" at completeValueCatchingError (C:\\\\graph_ql_workspaces\\\\graph_ql_study\\\\node_modules\\\\graphql\\\\execution\\\\execute.js:530:19)",
" at resolveField (C:\\\\graph_ql_workspaces\\\\graph_ql_study\\\\node_modules\\\\graphql\\\\execution\\\\execute.js:461:10)",
" at executeFields (C:\\\\graph_ql_workspaces\\\\graph_ql_study\\\\node_modules\\\\graphql\\\\execution\\\\execute.js:297:18)",
" at collectAndExecuteSubfields (C:\\\\graph_ql_workspaces\\\\graph_ql_study\\\\node_modules\\\\graphql\\\\execution\\\\execute.js:748:10)",
" at completeObjectValue (C:\\\\graph_ql_workspaces\\\\graph_ql_study\\\\node_modules\\\\graphql\\\\execution\\\\execute.js:738:10)",
" at completeValue (C:\\\\graph_ql_workspaces\\\\graph_ql_study\\\\node_modules\\\\graphql\\\\execution\\\\execute.js:626:12)",
" at completeValue (C:\\\\graph_ql_workspaces\\\\graph_ql_study\\\\node_modules\\\\graphql\\\\execution\\\\execute.js:592:21)",
" at completeValueCatchingError (C:\\\\graph_ql_workspaces\\\\graph_ql_study\\\\node_modules\\\\graphql\\\\execution\\\\execute.js:530:19)",
" at C:\\\\graph_ql_workspaces\\\\graph_ql_study\\\\node_modules\\\\graphql\\\\execution\\\\execute.js:651:25"
]
}
}
}
],
"data": null
}
- URL은 자동으로 생성되기 때문에 직적 저장할 필요가 없습니다.
- 스키마 안의 리졸버에서 각각 대응 가능합니다.
- Photo 객체를 리졸버 리스트에 추가하고, 함수로 대응하고 싶은 필드를 정의하면 됩니다.
- 사진 앱의 경우에는 URL을 반환해 주는 함수를 추가하면 됩니다.
const resolvers = {
Query: {
totalPhotos: () => photos.length,
allPhotos: () => photos
},
Mutation: {
postPhoto (parent, args) {
var newPhoto = {
id: _id++,
...args
}
photos.push(newPhoto)
return newPhoto
}
},
Photo: {
url: parent => `http://localhost:4000/img/${parent.id}.jpg`
}
}
- Photo 리졸버는 트리비얼 리졸버(Trivial Resolver)라 불리는 루트에 추가 됩니다.
- 트리비얼 리졸버는 resolvers 객체의 최상위 레벨로 추가되나 필수로 설정해야하는 것은 아닙니다.
- Photo 객체의 URL 필드 문제를 해결하기 위해 트리비얼 리졸버를 활용합니다.
- 쿼리에 사진 URL이 들어있다면 해당 리졸버 함수가 호출됩니다.
- 함수에 전달되는 첫 번째 인자는 언제나 parent 객체 입니다.
- 현재 리졸빙 대상인 Photo 객체가 parent가 됩니다.
- GraphQL 스키마 정의는 곧 애플리케이션 요구 사항 정의와 같습니다.
- 리졸버를 상용하면 요구 사항에 얼마든지 유연하게 대처할 수 있습니다.
- 함수를 비동기로도 만들 수 있고, 스칼라 타입이나 객체를 반환할 수도 있스며 다양한 출처를 활용하여 데이터를 반환할 수도 있습니다.
- 리졸버는 단지 함수에 불과하며, GraphQL 스키마의 각 필드는 모두 짝이 되는 리졸버가 잇습니다.
728x90
'GraphQL 스터디' 카테고리의 다른 글
스키마 설계하기 (0) | 2023.11.10 |
---|---|
GraphQL 쿼리어 (0) | 2023.11.10 |
GraphQL에 오신 것을 환영합니다. (0) | 2023.11.10 |