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

시작하기

이번 장에서는 GraphQL 서버를 개발하기 위한 프로젝트를 설정하고, 첫번째 GraphQL 쿼리를 구현해봅니다. 마지막에는 약간의 이론을 다루면서 GraphQL 스키마에 대하여 배웁니다.

프로젝트 생성하기

본 튜토리얼에서는 GraphQL 서버를 처음부터 만드는 방법을 배웁니다. 따라서 가장 처음 할 일은 GraphQL 서버의 각종 파일을 포함할 디렉토리를 생성하는 것입니다!

터미널을 열고, 적절한 위치로 이동한 뒤 아래의 명령어를 실행하세요.

mkdir hackernews-node
cd hackernews-node
yarn init -y

참고: 본 튜토리얼은 프로젝트 관리에 Yarn을 사용합니다. npm의 사용을 선호한다면, npm의 대응하는 명령어를 실행하셔도 됩니다.

이제 hackernews-node라는 디렉토리가 생성되고 package.json 파일과 함께 프로젝트가 초기화되었습니다. package.json 파일은 우리가 만들 Node 어플리케이션에 대한 구성 파일입니다. 여기에는 모든 의존성, 어플리케이션에 필요한 옵션(스크립트 등)이 적혀있습니다.

투박한 GraphQL 서버 생성하기

프로젝트 디렉토리가 제대로 생성되었다면, 이제 GraphQL 서버를 위한 진입점을 생성할 차례입니다. 이 파일은 index.js로 부르며, src라고 부르는 디렉토리 안에 생성할 것입니다.

터미널에서, src 디렉토리를 생성한 뒤, index.js라는 이름의 빈 파일을 생성합니다.
($ .../hackernews-node/)

mkdir src
touch src/index.js

참고: 위의 코드 블록에는 디렉토리 이름이 소괄호 안에 함께 표기되어있습니다. 이는 해당 터미널 명령어를 실행해야 하는 위치를 알려줍니다.

어플리케이션을 실행하려면, hackernews-node 디렉토리 안에서 node src/index.js를 실행하면 됩니다. 지금은 아무 일도 일어나지 않을 겁니다. 왜냐하면 index.js가 여전히 빈 파일이기 때문이죠. ¯_(ツ)_/¯

자, 이제 GraphQL 서버를 만들어봅시다! 제일 먼저 할 일은...(두근두근)... 바로 프로젝트에 의존성을 추가하는 것입니다.

터미널에서, 아래의 명령어를 실행합니다.
($ .../hackernews-node/)

yarn add graphql-yoga

graphql-yoga는 전기능 GraphQL 서버입니다. Express.js와 기타 라이브러리들을 기반으로 개발되었으며, 바로 배포가 가능한 정도의 GraphQL 서버를 만들 수 있도록 해줍니다.

이 서버가 제공하는 기능들의 예는 아래와 같습니다.

  • GraphQL 명세의 준수
  • 파일 업로드 지원
  • GraphQL 구독을 사용한 실시간 기능
  • TypeScript 지원
  • GraphQL Playground을 훌륭하게 지원
  • Express 미들웨어를 통한 확장성
  • GraphQL 스키마에서 별도로 정의한 지시자(Directive)를 리졸브
  • 쿼리 성능 추적
  • application/jsonapplication/graphql의 Content-type를 모두 허용
  • now, up, AWS Lambda, Heroku 등 다양한 서비스에서 작동

완벽합니다. 이제 코드를 작성해볼 시간입니다 🙌.

src/index.js 파일을 열고, 아래의 코드를 작성합니다.
($ .../hackernews-node/src/index.js)

const { GraphQLServer } = require('graphql-yoga')

// 1
const typeDefs = `
type Query {
  info: String!
}
`

// 2
const resolvers = {
 Query: {
    info: () => `Hackernews Clone의 API입니다`
  }
}

// 3
const server = new GraphQLServer({
  typeDefs,
  resolvers,
})
server.start(() => console.log(`http://localhost:4000에서 서버 가동중`))

참고: 이 코드 블록에 파일 이름이 함께 적혀있습니다. 해당 파일에 위의 코드를 작성하면 됩니다. 또한 파일 이름을 클릭하면 GitHub 프로젝트의 대응하는 파일로 링크되므로, 파일 내의 어느 부분에 코드를 작성해야할지 감이 안 잡히는 분들께 도움이 될 것입니다. 또한, 코드 블록의 내용을 복사-붙여넣기를 할 수도 있지만, 모든 코드를 직접 작성해보는 것을 권장합니다. 학습 경험을 크게 향상시켜줄 것입니다.

좋아요! 번호를 붙인 주석을 하나씩 읽어보면서 무슨 일이 벌어지고 있는지 짚어봅시다.

  1. typeDefs 상수는 GraphQL 스키마(이에 대하여 뒤에서 자세히 다룹니다)를 정의한 것입니다. 여기서 정의한 것은 간단한 Query 타입으로, info라는 필드 하나를 가집니다. 이 필드는 String! 타입입니다. 타입 정의에서 느낌표가 붙는 것은 해당 필드가 null이 될 수 없다는 의미입니다.
  2. resolvers 객체는 GraphQL 스키마의 실제 구현입니다. 스키마의 구조가 typeDefs: Query.info의 구조와 같다는 점을 주목하시기 바랍니다.
  3. 마지막으로, 스키마와 리졸버를 함께 묶어서 GraphQLServer에 전달했습니다. GraphQLServergraphql-yoga로부터 불러왔습니다. 이를 통하여 서버가 어떤 API 동작을 수행할 수 있고, 어떻게 리졸브할지를 정하게 됩니다.

방금 만든 GraphQL 서버를 테스트해보도록 합시다!

GraphQL 서버 테스트

프로젝트의 최상위 디렉토리에서 아래의 명령어를 실행합니다.
($ .../hackernews-node/)

node src/index.js

터미널 출력 결과에 나오듯 서버는 현재 http://localhost:4000에서 실행되고 있습니다. API를 테스트하기 위하여 브라우저를 실행하고 해당 URL로 접속합니다.

브라우저에 출력된 것은 GraphQL Playground로, 직접 API를 사용해보며 API 기능을 둘러볼 수 있는 강력한 "GraphQL IDE"입니다.

9RC6x9S.png

오른쪽에 SCHEMA라고 적혀있는 초록색 버튼을 누르면, API 문서가 열립니다. 이 문서는 스키마 정의를 기반으로 자동 생성된 것으로, 스키마에 정의된 모든 API 동작과 데이터 타입이 적혀있습니다.

81Ho6YM.png

영광의 첫번째 GraphQL 쿼리를 전송해봅시다. 왼쪽 에디터 페이지에 아래 코드를 작성합니다.

query {
  info
}

이제 가운데의 Play 버튼을 클릭하여 서버에 쿼리를 전송해봅시다. 아니면, CMD+ENTER를 눌러도 됩니다.

EnW3HE5.png

축하합니다. 최초의 GraphQL 쿼리를 구현하고, 성공적으로 테스트했군요 🎉!

info:String! 필드를 정의할 때, 느낌표 표시는 해당 필드가 null이 될 수 없음을 의미한다고 말했던 것을 기억하시나요? 분명, 리졸버를 구현하는 사람은 해당 필드의 값을 제어할 수 있어야 합니다. 그러면 리졸버 구현에서, 실제 정보를 나타내는 String 대신 null을 반환한다면 어떻게 될까요? 실제로 해보도록 합시다!

index.js에서 resolvers의 내용을 다음과 같이 수정합시다.

($ .../hackernews-node/src/index.js)

const resolvers = {
  Query: {
    info: () => null, // 수정
  }
}

위 코드의 결과를 테스트하려면, 서버를 재시작해야 합니다. 우선, CTRL+C를 눌러서 서버를 정지시키고, node src/index.js 명령어를 실행해서 서버를 재시작합니다.

자, 기존과 마찬가지로 쿼리를 다시 전송해봅시다. 이번에는, 다음과 같은 오류가 반환될 것입니다. Error: Cannot return null for non-nullable field Query.info.

VLVE5Vv.png

서버의 기반에서 작동하고 있는 graphql-js의 참조 구현은 리졸버의 반환 타입이 GraphQL 스키마 상의 타입 정의를 잘 따르고 있음을 보장합니다. 다시 말해서, 당신이 바보같은 실수를 하지 않도록 지켜준다는 거죠!

사실 이것은 GraphQL의 주요 장점 중 하나입니다. 즉, API가 스키마 정의에 약속된 그대로 작동하도록 감시하는 것입니다. 이렇게 하면 GraphQL 스키마에 접근하는 그 누구라도 API의 동작 또는 데이터 구조에 대하여 100% 신뢰할 수 있게 됩니다.

GraphQL 스키마에 대하여

모든 GraphQL API의 핵심에는 GraphQL 스키마가 존재합니다. 따라서 조금은 다루고 넘어가야 합니다.

참고: 본 튜토리얼에서는 이 주제의 아주 일부만을 다룹니다. GraphQL 스키마에 대하여, 그리고 GraphQL API에서 GraphQL 스키마가 가지는 역할에 대하여 보다 깊게 배우고 싶다면, 이 글이 아주 흘륭하므로 꼭 읽어보시기 바랍니다.

GraphQL 스키마는 주로 스키마 정의 언어(Schema Definition Language; SDL)를 사용하여 작성합니다. SDL은 (Java, TypeScript, Swift, Go 등의 다른 강타입 프로그래밍 언어와 마찬가지로) 데이터 구조를 정의할 수 있는 타입 체계를 갖추고 있습니다.

이게 GraphQL 서버에서 API를 설계하는 데에 무슨 도움이 될까요? 모든 GraphQL 스키마는 3가지의 특별한 최상위 타입을 가집니다. 각각은 Query, Mutation, Subscription입니다. 각 최상위 타입은 GraphQL이 제공하는 3가지 동작 타입인 쿼리, 뮤테이션, 구독에 대응합니다. 각 최상위 타입이 가지는 필드들은 최상위 필드라고 부르며 사용가능한 API 동작을 정의합니다.

예를 들어, 직전에 사용했었던 간단한 GraphQL 스키마를 보겠습니다.

type Query {
  info: String!
}

위 스키마는 info라는 단 하나의 최상위 필드만을 갖습니다. GraphQL API의 쿼리, 뮤테이션, 구독을 작성할 때에는 항상 최상위 필드로 시작해야 합니다. 지금의 경우 최상위 필드 단 하나만 존재하기 때문에, API가 허용할 수 있는 쿼리는 단 하나의 가능성만 존재하는 것입니다.

조금 더 심화된 예시를 살펴보겠습니다.

type Query {
  users: [User!]!
  user(id: ID!): User
}

type Mutation {
  createUser(name: String!): User!
}

type User {
  id: ID!
  name: String!
}

위의 코드에서는 3가지 최상위 필드를 가집니다. Query 타입의 useruser, 그리고 Mutation 타입의 createUser입니다. User 타입은 반드시 추가적으로 정의가 이루어져야 합니다. 그렇지 않으면 스키마 정의가 완성되지 않기 때문입니다.

위의 스키마 정의로부터 끌어낼 수 있는 API 동작은 무엇이 있을까요? 우선, 모든 API 동작은 항상 최상위 필드에서 시작해야한다는 사실을 위에서 배웠습니다. 하지만, 최상위 필드의 타입이 또다른 객체 타입인 경우 어떤 일이 벌어지는지는 아직 배우지 않았습니다. 그러니까, 위의 예시에서 최상위 필드의 타입이 [User!]!, User, User!인 경우 말이죠. 바로 직전 예시에서 info라는 최상위 필드는 String 타입이었고, 이것은 Scalar 타입입니다.

  • typeDefs: 어플리케이션의 스키마에서 가져온 타입 정의입니다.
  • resolvers: 어플리케이션의 스키마로부터 Query, Mutation, Subscription 타입과 각 타입이 가진 필드들을 반영한 자바스크립트 객체입니다. 이 객체에는 어플리케이션 스키마 상의 각 필드와 동일한 이름을 가진 함수가 들어있습니다.
  • context: 리졸버 체인을 거쳐서 전달된 객체로, 모든 리졸버는 이 객체에 대하여 읽기 및 쓰기 동작을 수행할 수 있습니다.

최상위 필드의 타입이 객체 타입이면, 해당 객체 타입에 포함된 필드를 사용하여 쿼리(또는 뮤테이션/구독)를 확장할 수 있습니다. 이렇게 확장된 부분을 선택 집합(Selection Set)이라고 부릅니다.

위의 스키마를 구현한 GraphQL API에 허용되는 동작들의 예시는 다음과 같습니다.

# 모든 사용자 정보에 대한 쿼리
query {
  users {
    id
    name
  }
}

# ID를 사용하여 단일 사용자 정보에 대한 쿼리
query {
  user(id: "user-1") {
    id
    name
  }
}

# 새로운 사용자 생성
mutation {
  createUser(name: "Bob") {
    id
    name
  }
}

몇 가지 주목할 점들이 있습니다.

  • 위의 예시들에서는 반환되는 User 객체들에 대하여 항상 idname을 쿼리했지만, 둘 중 하나는 생략해도 됩니다. 하지만 객체 타입에 대하여 쿼리할 때에는 선택 집합 내에 적어도 1개 이상의 필드를 요청해야 한다는 사실을 기억하시기 바랍니다.
  • 선택 집합 내의 필드들의 경우, 최상위 필드가 반드시 null 이외의 값을 반환하는지, 또는 여러 항목을 반환하는지 등의 여부는 중요하지 않습니다. 예를 들어 위의 스키마의 경우, 3가지 최상위 필드는 모두 똑같은 User 타입에 대하여 각기 다른 타입 한정자를 사용하고 있습니다.
    • users 필드의 경우, 반환 타입이 [User!]!인 것은 반환값이 User 항목으로 이루어진 리스트(리스트 자체도 null일 수 없다)이라는 의미입니다. 또한 리스트의 각 항목은 null일 수 없습니다. 따라서, 빈 리스트를 받거나, null이 아닌 User 객체로 이루어진 리스트를 받을 것임이 항상 보장됩니다.
    • user(id: ID!) 필드의 경우, 반환 타입이 User인 것은 반환값이 null 또는 User 객체라는 의미입니다.
    • createUser(name: String!) 필드의 경우, 반환 타입이 User!인 것은 이 동작이 항상 User 객체를 반환한다는 의미입니다.

이러한 정보를 제대로 제공한다면, Prisma 인스턴스는 당신의 데이터베이스 서비스에 완전히 접근할 수 있게 되고, 들어오는 요청을 리졸브하는 데에 사용될 수 있게 됩니다.

휴, 이 정도면 이론은 충분합니다 😠. 이제 다시 코드를 작성하러 가봅시다!

Quiz

GraphQL API에서 최상위 필드의 역할로 옳은 것은?

  • 3가지 최상위 필드는 Query, Mutation, Subscription이다.
  • 최상위 필드는 사용 가능한 API 동작을 구현한다.
  • 최상위 필드는 사용 가능한 API 동작을 정의한다.
  • 최상위 필드는 리졸버의 또다른 이름이다.

코멘트

본문 중 GraphQL 스키마를 설명하는 파트에 뜬금없이 typeDefs / resolvers / context 프로퍼티에 대한 설명이 나오는데, 앞뒤로 관련 내용을 전혀 다루지 않다가 갑툭튀하는 설명이어서 제 생각에는 순서 상으로 다른 글에 있어야 하는 내용인데 실수로 이번 글에 포함된 글인 듯 합니다. 만약 원문이 수정된다면 대응하여 수정하도록 하겠습니다.