GraphQL 스터디

GraphQL API 만들기

막이86 2023. 11. 10. 16:22
728x90

웹 앱 API 개발을 위한 GraphQL 을 요약한 내용입니다.

  • 서비스에 적용할 수 있는 기술 스택은 다양하나, 이 책에서는 자바스크립트를 사용하겠습니다.
  • 상당히 범용적인 내용을 배우기 때문에 나중에 다른 언어나 프레임워크를 쓰더라도 GraphQL 서비스의 전반적인 설계는 비슷해 보일 것입니다.
  • 다른 언어로 된 라이브러리에 관심이 있다면 GraphQL.org(https://graphql.org/) 사이트를 참조하세요
  • 2015년에 GraphQL 명세가 처음 공개되었습니다.
  • 서버 세부 구현 사항은 의도적으로 명세에서 생략했기 때문에 다양한 배경의 개발자들이 본인이 사용하기 편한 언어를 가지고 GraphQL을 사용할 수 있었습니다.
  • 페이스북 팀에서는 자바스크립트로 만든 레퍼런스 코드인 GraphQL.js를 공개했습니다.
  • 이 소스를 바탕으로 express-graphql이 만들어 졌습니다.
  • 이 책에서는 아폴로 팀에서 만든 오픈 소스 솔루션인 아폴로 서버를 사용하기로 했습니다.
    • 설정이 상당히 편하고 프로덕션 레벨에서 사용할 수 있는 여러 가지 기능을 제공하기 때문입니다.
    • 서브스크립션과 파일 업로드 기능을 제공합니다.
    • 데이터 소스 API로 이미 사용 중인 기존 서비스에 접근할 수 있습니다.
    • 아폴로 엔진을 손쉽고 간편하게 그리고 독립적으로 사용할 수 있습니다.
    • GraphQL 플레이그라운드도 제공하므로 브라우저에서 쿼리를 바로 작성할 수 있습니다.

5.1 프로젝트 세팅

  • 로컬에 빈 폴더를 하나 만들어 photo-share-api 프로젝트를 시작해 봅시다.
  1. 터미널에 npm init -y 명령을 입력해 빈 폴더에 npm 프로젝트를 새로 만듭니다.
  2. npm init -y
  3. 프로젝트에서 사용할 의존 모듈인 apollo-server, graphql, nodemon을 설치 합니다.
  4. npm install apollo-server graphql nodemon
  5. 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"
      }
    }
    
  6. 프로젝트 루트에 index.js 파일도 생성합니다.
  7. package.json의 main에 index.js를 써줍니다.
  8. { "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 객체에 이에 대응하는 리졸버 함수를 추가합니다.
    1. photos 라는 변수를 선언해 사진 정보를 담을 배열을 만듭니다.
      • 데이터 베이스는 나중에 연동
    2. totalPhotos 리졸버 함수에서 photos 배열의 길이를 반환하도록 수정합니다.
    3. postPhoto 리졸버 함수를 추가합니다.
      • 첫 번째 인자로 부모 객체에 대한 참조를 전달합니다.
      • 가끔가다 문서에 _, root, obj 가 나올 때가 있습니다. 이 경우 postPhoto 리졸버 함수의 부모는 Mutation 입니다.
      • 두 번째 인자는 바로 뮤테이션에 사용할 GraphQL 인자입니다.
      • args 변수는 필드가 두개 들어 있는 객체 입니다. 지금은 ({name, description}) 인자가 사진 객체 하나에 해당하므로 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 객체 리스트를 반환하는 코드를 작성합니다.
    1. Photo 객체를 타입 정의에 추가합니다.
    2. allPhotos 쿼리를 타입 정의에 추가합니다.
    3. postPhoto 뮤테이션은 Photo 타입 형태의 데이터 객체 리스트를 반환합니다.
    4. Photo 타입은 ID가 필요함으로, ID 값을 저장할 변수를 하나 만듭니다.
      • 보통은 서버에서 식별자나 타임스탬프 같은 변수를 생성해 ID로 사용합니다.
    5. ID 필드와 스프레드 연산자를 사용해 args에서 name과 description 필드를 받아 postPhoto 리졸버에서 새로운 사진 객체를 만듭니다.
    6. 뮤테이션에서 Boolean 값을 반환하는 대신에, Photo 타입 형태 객체를 반환하도록 만듭니다.
    순차적으로 값이 증가하는 변수를 가지고 고유 ID를 만드는 방법은 확장성이 매우 떨어집니다. 시연용으로는 상관이없으나, 실제 애플리케이션인 경우 대부분 데이터베이스에서 ID를 생성합니다.
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