공부하려고 fullstackopen.com/en/part8/graph_ql_server 번역 및 정리한 글
Schema & Query
type Person {
name: String!
phone: String
street: String!
city: String!
id: ID!
}
type Query {
personCount: Int!
allPersons: [Person!]!
findPerson(name: String!): Person
}
- schema에 타입들을 지정
Person:
- 문자열은 GraphQL의 스칼라 타입 중 하나
- !: 필수. Non-Null.
- ID: 문자열, unique
Query
모든 GraphQL schema는 어떤 쿼리가 API에 만들어져야 하는지 알려주는 Query를 기술한다. 이를테면 클라이언트에서 어떤 쿼리를 서버로 보내고, 쿼리는 어떤 파라미터를 가질 수 있는지, 어떤 데이터를 쿼리가 리턴하는지를 설명한다.
쿼리는 schema에 기술된 필드를 리턴할 수 있다.
GraphQL 쿼리와 리턴된 JSON 객체는 직접적으로 연결되고, ( REST와 비교하자면, REST에서 URL과 요청 타입은 리턴된 데이터의 형식과 아무런 상관이 없다. ) 서버와 클라이언트 사이에서 교환되는 데이터만 서술한다. 데이터베이스랑은 아무 상관이 없다. GraphQL API가 사용하는 데이터는 관계형 데이터베이스, 다큐먼트 데이터베이스, 다른 서버에 저장될 수 있다.
아폴로 서버
const { ApolloServer, gql } = require('apollo-server')
// ...
const server = new ApolloServer({
typeDefs,
resolvers,
})
- typeDefs : GraphQL schema
- resolvers : 서버의 resolvers를 담은 객체. resolver는 GraphQL 쿼리가 어떻게 응답 되어야 할지 정의하는 코드
const resolvers = {
Query: {
personCount: () => persons.length,
allPersons: () => persons,
findPerson: (root, args) =>
persons.find(p => p.name === args.name)
}
}
type Query {
personCount: Int!
allPersons: [Person!]!
findPerson(name: String!): Person
}
- 리졸버는 schema에 기술된 쿼리에 상응
- schema에 기술된 모든 쿼리가 작성된 필드가 쿼리 내부에 위치
쿼리:
query {
personCount
}
리졸버:
( ) => persons.length
∴ 쿼리에 대한 응답은 persons 배열의 length
GraphQL-playground
node filename.js 으로 실행. 개발툴.
Resolver의 파라미터
query {
findPerson(name: "Arto Hellas") {
phone
city
street
}
}
//...
(root, args) => persons.find(p => p.name === args.name)
- 두번째 파라미터 args, 쿼리의 파라미터를 받는다. args.name
- 리졸버는 첫번째 파라미터 root는 불필요
모든 리졸버는 네 개의 파라미터를 받는다. (자바스크립트는 파라미터를 정의할 필요가 없음)
fieldName(obj, args, context, info) { result }
(www.graphql-tools.com/docs/resolvers/#resolver-function-signature)
Default Resolver
서버가 쿼리의 정의된 필드를 정확히 보내는 방법?
GraphQL 서버는 schema의 모든 타입, 모든 필드에 대해 리졸버를 정의해야 한다. (앞에서는 Query 타입에 대해서만 리졸버를 정의했다.)
Person 타입 필드에 대해서는 리졸브를 정의하지 않았지만, 아폴로가 알아서 디폴트 리졸버를 정의한다.
const resolvers = {
Query: {
personCount: () => persons.length,
allPersons: () => persons,
findPerson: (root, args) => persons.find(p => p.name === args.name)
},
Person: {
name: (root) => root.name,
phone: (root) => root.phone,
street: (root) => root.street,
city: (root) => root.city,
id: (root) => root.id
}
}
//...
Person: {
street: (root) => "Manhattan",
city: (root) => "New York"
}
객체의 해당 필드 값을 그대로 리턴하는 리졸버. 리졸버의 첫번째 파라미터인 root로 객체에 접근한다.
타입의 필드 일부에 대해서만 리졸버를 정의하는 것도 가능하다. 나머지는 디폴트 리졸브가 처리한다.
+ 아예 하드코딩 해놓는 것도 가능하다.
중첩된 객체
type Address {
street: String!
city: String!
}
type Person {
name: String!
phone: String
address: Address!
id: ID!
}
type Query {
personCount: Int!
allPersons: [Person!]!
findPerson(name: String!): Person
}
//...
query {
findPerson(name: "Arto Hellas") {
phone
address {
city
street
}
}
}
{
"data": {
"findPerson": {
"phone": "040-123543",
"address": {
"city": "Espoo",
"street": "Tapiolankatu 5 A"
}
}
}
}
데이터는 이전처럼 저장해도 된다.
let persons = [
{
name: "Arto Hellas",
phone: "040-123543",
street: "Tapiolankatu 5 A",
city: "Espoo",
id: "3d594650-3436-11e9-bc57-8b80ba54c431"
},
// ...
]
서버에 저장되는 person 객체는 schema에 기술된 GraphQL 타입 Person 객체와 정확히 동일하지 않아도 된다.
Person 타입과 달리 Address 타입은 서버의 고유한 데이터 구조에 저장되지 않으므로 id 필드를 갖지 않는다. 배열에 저장되는 객체에는 address 필드가 없기 때문에 디폴트 리졸브로는 불충분하다. 직접 가공해야 한다.
const resolvers = {
Query: {
personCount: () => persons.length,
allPersons: () => persons,
findPerson: (root, args) =>
persons.find(p => p.name === args.name)
},
Person: {
address: (root) => {
return {
street: root.street,
city: root.city
}
}
}
}
- Person 객체가 리턴될 때마다 name, phone, id는 디폴트 리졸버를 통해 그대로 리턴되고, address 필드는 직접 정의한 리졸버를 통해 형성된다. 리졸버 함수의 root 파라미터는 person 객체를 가리킨다.
Mutations
새로운 데이터를 추가할 때 기능을 추가해봅시다. GraphQL에서는 변화를 발생시키는 모든 작동이 mutation을 통해 수행된다. Mutation은 schema에서 type Mutation의 키로 서술된다.
type Mutation {
addPerson(
name: String!
phone: String
street: String!
city: String!
): Person
}
- person의 세부사항을 파라미터로 받았고, phone 파라미터는 유일하게 null일 수 있다.
- 리턴 값은 Person 타입 - 동작이 성공하면 값을 받고, 아니면 null.
- id는 서버에서 생성하도록 따로 생성하지 않았다.
뮤테이션의 리졸버:
const { v1: uuid } = require('uuid')
// ...
const resolvers = {
// ...
Mutation: {
addPerson: (root, args) => {
const person = { ...args, id: uuid() }
persons = persons.concat(person) // 기존 배열에 추가
return person // 저장된 값 반환
}
}
}
mutation {
addPerson( // 입력해야할 값
name: "Pekka Mikkola"
phone: "045-2374321"
street: "Vilppulantie 25"
city: "Helsinki"
) { // 포맷
name
phone
address{
city
street
}
id
}
}
// persons 배열에는 다음과 같이 저장된다.
{
name: "Pekka Mikkola",
phone: "045-2374321",
street: "Vilppulantie 25",
city: "Helsinki",
id: "2b24e0b0-343c-11e9-8c2a-cb57c2bf804f"
}
// 하지만 mutation에 대한 응답은 다음과 같다.
{
"data": {
"addPerson": {
"name": "Pekka Mikkola",
"phone": "045-2374321",
"address": {
"city": "Helsinki",
"street": "Vilppulantie 25"
},
"id": "2b24e0b0-343c-11e9-8c2a-cb57c2bf804f"
}
}
}
= Person 타입의 address 필드 리졸버는 response 객체의 포맷을 올바르게 수정한다.
에러 핸들링
데이터를 저장하려는데 파라미터 형식이 맞지 않을 때, GraphQL은 자체 유효성 검사를 통해 에러를 핸들링 하기도 한다.
Mutation에 적용 되어야 하는 규칙들은 직접 추가해야한다. 이런 규칙들은 아폴로 서버의 에러 핸들링 매커니즘이 담당한다.
const { ApolloServer, UserInputError, gql } = require('apollo-server')
// ...
const resolvers = {
// ..
Mutation: {
addPerson: (root, args) => {
// 이미 존재하는 이름
if (persons.find(p => p.name === args.name)) {
throw new UserInputError('Name must be unique', {
invalidArgs: args.name,
})
}
const person = { ...args, id: uuid() }
persons = persons.concat(person)
return person
}
}
}
enum
핸드폰 번호가 있는/없는 사람만 넘겨 받는 쿼리를 작성해봅시다.
query {
allPersons(phone: YES) {
name
phone
}
}
query {
allPersons(phone: NO) {
name
}
}
schema를 다음과 같이 수정
enum YesNo {
YES
NO
}
type Query {
personCount: Int!
allPersons(phone: YesNo): [Person!]!
findPerson(name: String!): Person
}
resolver는 다음과 같이 수정
Query: {
personCount: () => persons.length,
allPersons: (root, args) => {
if (!args.phone) {
return persons
}
const byPhone = (person) =>
args.phone === 'YES' ? person.phone : !person.phone
return persons.filter(byPhone)
},
findPerson: (root, args) =>
persons.find(p => p.name === args.name)
},
Mutation #2 기존 정보 수정
Mutation에 editNumber 추가
type Mutation {
addPerson(
name: String!
phone: String
street: String!
city: String!
): Person
editNumber(
name: String!
phone: String!
): Person
}
리졸버 수정
Mutation: {
// ...
editNumber: (root, args) => {
const person = persons.find(p => p.name === args.name)
if (!person) {
return null
}
const updatedPerson = { ...person, phone: args.phone }
persons = persons.map(p => p.name === args.name ? updatedPerson : p)
return updatedPerson
}
}
+ 쿼리
복수의 Query 필드를 결합하거나 "분리된 쿼리들"을 하나의 쿼리로 만드는 것도 가능하다.
query {
havePhone: allPersons(phone: YES){
name
}
phoneless: allPersons(phone: NO){
name
}
}
'GraphQL' 카테고리의 다른 글
Fullstack Part 8 #5 Fragments and subscriptions (0) | 2021.03.12 |
---|---|
Fullstack Part 8 #4 Login & Cache update (0) | 2021.03.11 |
Fullstack Part 8 #3 DB (0) | 2021.03.10 |
Fullstack part 8 #2 React and GraphQL (0) | 2021.03.09 |
REST와 GraphQL (0) | 2021.03.08 |
댓글