• 본 시리즈에서는 How to GraphQL의 Tutorial 문서들을 차례대로 번역합니다.
  • 본 시리즈는 GraphQL Basic and Advanced 시리즈에서 이어집니다. GraphQL을 처음 접하는 분들은 해당 시리즈를 먼저 읽고 오시는 것을 추천드립니다.
  • 이 글은 GraphQL-Node Tutorial - A Simple Query을 번역한 글입니다.
  • 오역 또는 의역이 있을 수 있습니다. 양해 부탁드리며, 수정할 필요한 부분은 댓글로 요청해주세요.

간단한 쿼리

이번 장에서는 Hacker News 클론 프로젝트의 기능을 제공하는 데에 사용할 첫번째 API 동작을 구현해봅니다. 바로 사용자들이 게시한 링크들의 피드를 쿼리하는 것입니다.

스키마 정의의 확장

우선 feed 쿼리를 구현해보겠습니다. feed 쿼리를 사용하면 Link 요소의 리스트를 반환받습니다. 일반적인 경우, API에 새로운 기능을 추가할 때는 과정이 매번 비슷합니다.

  1. 새로운 최상위 필드(필요한 경우, 새로운 데이터 타입도 추가)를 추가하여 GraphQL 스키마 정의를 확장
  2. 새로 추가된 필드에 대응하는 리졸버 함수를 구현

이 과정은 스키마-주도(Schema-Driven) 또는 스키마-우선(Schema-First) 개발이라고 불리기도 합니다.

그러면 첫번째 단계인, GraphQL 스키마 확장부터 시작해보도록 하겠습니다.

index.js 파일에서 typeDefs 상수의 내용을 다음과 같이 수정하세요.
($ .../hackernews-node/src/index.js)

const typeDefs = `
  type Query {
    info: String!
    feed: [Link!]!        // 수정
  }

  type Link {             // 수정
    id: ID!               // 수정
    description: String!  // 수정
    url: String!          // 수정
 }                        // 수정
`

아주 명쾌합니다. Link라는 새로운 타입을 정의했고, 이 타입은 Hacker News에 게시될 수 있는 링크를 나타냅니다. 각 Linkid, description, url을 가질 수 있습니다. 다음으로 Query 타입에 새로운 최상위 필드를 추가하면, 이제 Link 항목의 리스트를 반환할 수 있게 됩니다. 이 리스트는 절대로 null이 아님이 보장되고(적어도 빈 리스트를 반환할 것입니다), 이 리스트에 포함된 항목 또한 null이 아님이 보장됩니다. 두 개의 느낌표는 이런 의도로 사용된 것입니다.

리졸버 함수 구현

다음으로는 feed 쿼리를 위한 리졸버 함수를 구현해야 합니다. 사실, GraphQL 스키마에서는 단지 최상위 필드뿐 아니라, 모든 필드가 리졸버 함수를 가집니다. 따라서 Link 타입의 id, description, url 필드에도 대응하는 리졸버 함수를 만들어주어야 합니다.

index.js 파일에서 임시 데이터로 이루어진 리스트를 새로 만들고, resolvers 객체를 다음과 같이 수정하세요.
($ .../hackernews-node/src/index.js)

// 1
let links = [{                                   // 수정
  id: 'link-0',                                  // 수정
  url: 'www.howtographql.com',                   // 수정
  description: 'Fullstack tutorial for GraphQL'  // 수정
}]                                               // 수정

const resolvers = {
  Query: {
    info: () => `This is the API of a Hackernews Clone`,
    // 2
    feed: () => links,                           // 수정
  },
  // 3
  Link: {                                        // 수정
    id: (parent) => parent.id,                   // 수정
    description: (parent) => parent.description, // 수정
    url: (parent) => parent.url,                 // 수정
  }
}

주석 처리된 부분을 하나씩 살펴보도록 하겠습니다.

  1. links 변수는 실행 시간 중에 각 링크들을 저장하는 데에 사용되는 변수입니다. 현재로서는 모든 것이 데이터베이스가 아니라 메모리 상에 저장됩니다.
  2. feed라는 최상위 필드를 위한 새로운 리졸버를 추가했습니다. 리졸버의 이름은 항상 스키마 정의에서 대응하는 필드의 이름과 항상 같아야 한다는 것을 기억하시기 바랍니다.
  3. 마지막으로 스키마 정의에 따라, Link 타입의 각 필드에 대응하는 3가지 리졸버들을 새롭게 추가하였습니다. 리졸버에 인자로 전달된 parent가 무엇인지는 잠시 후에 살펴보도록 하겠습니다.

위의 구현 사항을 테스트해보시기 바랍니다. 서버를 재시작하고(서버가 작동 중인 상태라면 CTRL+C를 눌러서 서버를 멈춘 뒤 node src/index.js를 다시 한번 실행하시면 됩니다), 브라우저에서 http://localhost:4000으로 접속하시면 됩니다. Playground의 문서를 쭉 읽어보면, feed라는 이름의 쿼리가 이제 사용가능해졌음을 확인할 수 있습니다.

0EQ5P9p.png

한번 아래의 쿼리를 전송해보세요.

query {
  feed {
    id
    url
    description
  }
}

아주 좋습니다. 서버는 links에 정의한 데이터를 반환해줄 겁니다.

{
  "data": {
    "feed": [
      {
        "id": "link-0",
        "url": "www.howtographql.com",
        "description": "Fullstack tutorial for GraphQL"
      }
    ]
  }
}

선택 집합에서 필드를 더하거나 빼보면서 쿼리를 다루어보면서 서버로부터 어떤 응답이 돌아오는지 보는지 관찰해보세요.

쿼리 리졸브 과정

이제 GraphQL 서버가 들어오는 요청을 실제로 어떻게 리졸브하는지에 대하여 짧게 다루어보겠습니다. 이미 보았듯이, GraphQL 쿼리에서 사용되는 필드들은 GraphQL 스키마의 타입 정의에 정의된 필드들입니다.

위에서 다루었던 쿼리를 다시 살펴보겠습니다.

query {
  feed {
    id
    url
    description
  }
}

쿼리에 사용된 feed, id, url, description, 이 4개의 필드는 모두 스키마 정의에서도 확인할 수 있습니다. 또한 우리는 스키마 정의가 갖는 모든 필드들은 저마다 하나씩 해당 필드만을 위하여 데이터를 반환해주는 리졸버 함수를 가진다는 것도 배웠습니다.

쿼리를 처리하여 데이터를 반환해주는 리졸브 과정이 어떻게 이루어지는지 이제 상상이 가시나요? GraphQL 서버가 하는 일은 바로 쿼리에 포함된 모든 필드에 대하여 리졸버 함수를 각각 호출하고, 쿼리의 모양에 따라 데이터를 잘 포장하여 응답으로 만드는 것입니다. 따라서 쿼리를 처리한다는 것은 리졸버 함수들의 호출을 잘 조율(Orchestrate)하는 일이라고 할 수 있습니다.

지금의 구현에서 하나 여전히 이상한 것은 바로 Link 타입의 리졸버입니다. 아주 단순하고 별 것 아닌 듯한 모습을 보이고 있습니다.

Link: {
  id: (parent) => parent.id,
  description: (parent) => parent.description,
  url: (parent) => parent.url,
}

우선, 모든 GraphQL 리졸버 함수는 실제로 4개의 인자를 입력받는다는 사실을 꼭 기억하시기 바랍니다. 나머지 3개의 인자는 현재 우리의 예제에서는 필요하지 않으므로, 단지 생략한 것일 뿐입니다. 걱정 마세요, 나머지에 대하여서는 차차 배우게 될 겁니다.

보통 parent(또는 root) 라고 불리는 첫번째 인자는 바로 직전 리졸버 실행 수준에서의 결과값입니다. 대체 이게 무슨 말일까요? 🤔

바로 직전에 보셨듯이, GraphQL 쿼리는 중첩될 수 있습니다. 각 중첩 수준(즉, 중첩된 중괄호들)은 리졸버 하나의 실행 수준에 대응합니다. 위의 쿼리는 따라서 2개의 실행 수준을 가집니다.

첫번째 수준에서는 feed 리졸버를 호출하고 links에 포함된 모든 데이터를 반환합니다. 두번째 실행 수준에서는, 직전의 리졸버 실행 수준에서 반환된 리스트에 포함된 각각의 항목들에 대하여 Link 타입의 리졸버를 호출합니다. 이것이 가능한 이유는 GraphQL 서버는 feedLink로 이루어진 리스트를 반환한다는 것을 스키마를 통하여 알기 때문입니다. 아주 영리하죠. 그 결과, Link가 가지는 3개의 리졸버에서 인자로 들어오는 parent 객체는 links 리스트의 각 항목입니다.

참고: 이에 대하여 더 자세하게 배우려면, 이 글을 확인하시기 바랍니다.

어떤 경우이든, Link 리졸버의 구현은 아주 간단하기 때문에, Link 리졸버는 구현을 생략하더라도 서버가 기존과 동일하게 정상적으로 작동할 것입니다.