GraphQL - Node Tutorial - 08. Realtime GraphQL Subscriptions

cadenzah·2019년 10월 27일
5

GraphQL - Node Tutorial

목록 보기
8/10
post-thumbnail

알립니다

이 번역 시리즈는 2019년 10월 경에 작성되었습니다. 원본인 GraphQL - Node 튜토리얼은 현재 새로운 버전으로 새롭게 작성되었습니다. 따라서 이 글은 Deprecated된 글임을 알려드립니다.

실시간 GraphQL 구독

이번 장에서는 GraphQL 구독을 구현하여 실시간 기능을 어플리케이션에 추가하는 방법을 배웁니다. GraphQL 서버에서 노출할 아래의 두 가지 구독 기능을 구현하는 것이 목표입니다.

  • 새로운 Link생성되었을 때 구독 중인 클라이언트에 실시간 갱신을 전송
  • Link추천을 받았을 때 구독 중인 클라이언트에 실시간 갱신을 전송

GraphQL 전송이란?

구독(Subscription)이란 특정 이벤트가 발생했을 때 서버가 클라이언트로 데이터를 전송해주는 GraphQL 기능입니다. 구독은 WebSocket을 사용하여 구현하는 것이 일반적입니다. 그러한 구성에서는 서버가 구독 중인 클라이언트와 지속적인 연결을 유지합니다. 이것은 기존에 API와 상호작용하던 방식인 "요청-응답 사이클"을 깨는 것이기도 합니다.

그 대신, 처음에 클라이언트는 관심있는 이벤트를 명시한 구독 쿼리를 전송하여, 서버와 길게 지속되는 연결을 형성합니다. 특정 이벤트가 발생할 때마다, 서버는 이 연결을 사용하여 구독 중인 (하나 또는 둘 이상의) 클라이언트에 이벤트 데이터를 푸시합니다.

Prisma를 활용한 구독

다행히, Prisma는 구독 기능을 제대로 지원합니다.

Prisma의 데이터 모델 각각에 대하여 Prisma는 다음과 같은 이벤트들을 구독할 수 있도록 해줍니다.

  • 새로운 모델이 생성되었을 때(created)
  • 기존의 모델이 수정되었을 때(updated)
  • 기존의 모델이 삭제되었을 때(deleted)

Prisma 클라이언트가 가지는 $subscribe 메서드를 사용하면 위의 이벤트들을 구독할 수 있습니다.

이야기는 이만하면 되었고, 직접 코드를 써보도록 하죠! 클라이언트가 새로 생성되는 Link 요소에 대하여 구독할 수 있도록, 구독 기능을 구현해보도록 하겠습니다.

쿼리와 뮤테이션과 마찬가지로, 가장 먼저 할 일은 GraphQL 스키마 정의를 확장하는 것입니다.

어플리케이션의 스키마를 열고 Subscription 타입을 추가합니다.
($ .../hackernews-node/src/schema.graphql)

type Subscription {
  newLink: Link
}

다음으로, newLink 필드에 대한 리졸버를 구현합니다. 구독을 다루는 리졸버는 쿼리와 뮤테이션의 경우와는 다소 차이가 있습니다.

  1. 데이터를 직접 반환하지 않고, AsyncIterator를 반환합니다. 이것은 이후에 GraphQL 서버 측에서 클라이언트에 이벤트 데이터를 푸시하는 데에 사용됩니다.

  2. 구독 리졸버는 객체로 감싸지며, subscribe 필드를 통하여 제공됩니다. 또한, AsyncIterator가 발생시키는 데이터로부터 실제로 데이터를 반환하는 resolve 필드를 별도로 제공해야 합니다.

리졸버 구현 코드를 모듈화하기 위하여, 우선 Subscription.js라는 파일을 새로 생성합니다.
($ .../hackernews-node)

touch src/resolvers/Subscription.js

새로 생성한 파일에 구독 리졸버를 아래와 같은 방식으로 구현합니다.

function newLinkSubscribe(parent, args, context, info) {
  return context.prisma.$subscribe.link({ mutation_in: ['CREATED'] }).node()
}

const newLink = {
  subscribe: newLinkSubscribe,
  resolve: payload => {
    return payload
  },
}

module.exports = {
  newLink,
}

코드는 상당히 간단명료합니다. 이전에 언급하였듯이, 구독 리졸버는 자바스크립트 객체의 subscribe 필드의 값으로서 제공됩니다.

마찬가지로 이전에 언급하였듯이, context가 가지는 prisma 인스턴스는 $subscribe 속성을 가집니다. 이 속성 메서드는 Prisma API를 사용하여 구독 작업을 대신 처리해줍니다. 이 메서드는 구독을 리졸브하고 이벤트 데이터를 푸시하는 데에 사용됩니다. 실시간 기능에 필요한 복잡한 일들은 Prisma가 알아서 처리해줍니다.

index.js 파일을 열고, Subscription 모듈을 불러오는 아래의 import 구문을 파일 최상단에 추가합니다.
($ .../hackernews-node/src/index.js

const Subscription = require('./resolvers/Subscription')

다음으로, resolvers 객체의 정의를 아래와 같이 수정합니다.
($ .../hackernews-node/src/index.js

const resolvers = {
  Query,
  Mutation,
  Subscription,     // 수정
  User,
  Link,
}

구독 테스트하기

모든 코드를 다 작성하였으면, 실시간 API를 테스트해볼 시간입니다. ⚡️ GraphQL Playground를 두 화면에 띄우고 테스트를 진행해볼 수 있습니다.

  • 아직 하지 않았다면, CTRL+C를 누르고, node src/index.js를 실행하여 서버를 재시작합니다.
  • 다음으로, 두 브라우저 화면을 켜고, 둘 다 http://localhost:4000으로 이동합니다.

아마 짐작하셨겠지만, 한쪽 Playground에서는 구독 쿼리를 전송하고, 이를 통하여 서버와 항구적인 웹소켓 연결을 생성합니다. 다른쪽 Playground는 구독 이벤트를 발생시키는 post 뮤테이션을 전송합니다.

한쪽 Playground에서 아래와 같은 구독을 전송합니다.

subscription {
  newLink {
      id
      url
      description
      postedBy {
        id
        name
        email
      }
  }
}

쿼리와 뮤테이션을 보냈을 때와는 달리, 동작 결과가 바로 화면에 나타나지 않습니다. 대신, 이벤트가 발생하기를 대기하고 있음을 나타내는 로딩 스피너가 나타납니다.

구독 이벤트를 발생시킬 시간입니다.

다른쪽 Playground에서 아래와 같이 post 뮤테이션을 전송합니다. 이 작업을 수행하려면 인증이 필요하다는 것을 기억하세요(인증 방법은 이전 장을 확인하시기 바랍니다).

mutation {
  post(
    url: "www.graphqlweekly.com"
    description: "Curated GraphQL content coming to your email inbox every Friday"
  ) {
    id
  }
}

추천 기능 추가하기

vote 뮤테이션 구현하기

다음으로 추가할 기능은 사용자들이 특정 링크에 대하여 추천을 할 수 있는 기능입니다. 가장 먼저 할 일은 데이터베이스에서 추천을 표현할 수 있도록 Prisma 데이터 모델을 확장하는 것입니다.

prisma/datamodel.prisma 파일을 열어서 아래와 같이 수정합니다.
($ .../hackernews-node/prisma/datamodel.prisma

type Link {
  id: ID! @id
  createdAt: DateTime! @createdAt
  description: String!
  url: String!
  postedBy: User
  votes: [Vote!]!           # 수정
}

type User {
  id: ID! @id
  name: String!
  email: String! @unique
  password: String!
  links: [Link!]!
  votes: [Vote!]!           # 수정
}

type Vote {                 # 수정
  id: ID! @id               # 수정
  link: Link!               # 수정
  user: User!               # 수정
}                           # 수정

보시다시피, 데이터 모델에 Vote라는 새로운 타입을 추가했습니다. User 타입과 Link 타입에 대하여 일대다 관계를 가지게 됩니다.

수정한 사항을 적용하고 Prisma 클라이언트 API를 갱신하려면 Prisma 서비스를 다시 배포해야 합니다. 그래야 새로운 Vote 타입에 대하여 CRUD 동작을 수행할 수 있습니다.

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

prisma deploy

이전에 설정한 Post-Deploy Hook 덕분에, 직접 prisma generate를 실행하여 Prisma 클라이언트를 갱신하지 않아도 됩니다.

스키마 주도 개발을 염두에 두고서, 이번에는 어플리케이션의 스키마 정의를 확장하여 GraphQL 서버가 vote 뮤테이션을 노출하도록 만들겠습니다.

$ .../hackernews-node/src/schema.graphql

type Mutation {
  post(url: String!, description: String!): Link!
  signup(email: String!, password: String!, name: String!): AuthPayload
  login(email: String!, password: String!): AuthPayload
  vote(linkId: ID!): Vote         # 수정

또한, 참조 타입 Vote는 GraphQL 스키마에도 정의되어야 합니다.
($ .../hackernews-node/src/schema.graphql

type Vote {
  id: ID!
  link: Link!
  user: User!
}

어떤 Link가 받은 추천을 모두 쿼리하는 것 또한 가능해야 하므로, schema.graphql 파일에 정의된 Link 타입 또한 수정해야 합니다.

schema.graphql 파일을 열고 Link 타입에 votes 필드를 추가합니다.
($ .../hackernews-node/src/schema.graphql

type Link {
  id: ID!
  description: String!
  url: String!
  postedBy: User
  votes: [Vote!]!    # 수정
}

그 다음은 이제 무엇인지 말 안해도 아시죠? 대응하는 리졸버 함수를 구현하겠습니다.

src/resolvers/Mutation.js 파일에 아래의 함수를 추가합니다.
($ .../hackernews-node/src/resolvers/Mutation.js

async function vote(parent, args, context, info) {
  // 1
  const userId = getUserId(context)

  // 2
  const linkExists = await context.prisma.$exists.vote({
    user: { id: userId },
    link: { id: args.linkId },
  })
  if (linkExists) {
    throw new Error(`Already voted for link: ${args.linkId}`)
  }

  // 3
  return context.prisma.createVote({
    user: { connect: { id: userId } },
    link: { connect: { id: args.linkId } },
  })
}

각 코드들을 분석해보겠습니다.

  1. post 리졸버를 만들 때와 비슷하게, 첫번째로 할 일은 요청으로 들어오는 JWT가 유효한지 getUserId 헬퍼 함수를 사용하여 검증하는 것입니다. 만약 유효하다면, 함수는 해당 요청을 보낸 UseruserId를 반환합니다. 만약 JWT가 유효하지 않다면, 함수는 예외를 발생시킵니다.

  2. prisma.$exists.vote(...) 함수는 처음 보실 겁니다. prisma 클라이언트는 각 모델에 대한 CRUD 메서드뿐 아니라, 각 모델마다 $exists 함수도 생성합니다. %exists 함수는 where라는 필터 객체를 인자로 받는데, 이를 통하여 해당 타입의 요소와 관련된 특정 조건을 명시할 수 있습니다. 데이터베이스 내에서 적어도 1개 이상의 요소가 해당 조건을 만족한다면, $exists 함수는 true를 반환합니다. 위 코드에서는 $exists 함수를 활용하여, 요청을 보낸 Userargs.linkId로 식별되는 Link를 추천하였는지 여부를 확인하는 데에 사용됩니다.

  3. $exists 함수가 false를 반환한다면, createVote 메서드를 사용하여 새로운 Vote를 생성하고, 이 VoteUserLink연결됩니다.

아참, vote 리졸버를 포함하도록 export 구문을 수정하는 것도 잊지 마세요.
($ .../hackernews-node/src/resolvers/Mutation.js

module.exports = {
  post,
  signup,
  login,
  vote,       // 수정
}

새롭게 생겨난 관계들을 GraphQL 스키마에서 파악할 수 있도록 처리하는 일도 필요합니다.

  • Link 타입에 votes
  • Vote 타입에 user
  • Vote 타입에 link

이전과 비슷하게, 새로운 필드에 대한 리졸버를 구현해야 합니다.

Link.js 파일을 열고 아래의 함수를 추가합니다.
($ .../hackernews-node/src/resolvers/Link.js

function votes(parent, args, context) {
  return context.prisma.link({ id: parent.id }).votes()
}

새로운 리졸버를 exports 객체에 추가하는 것도 잊지 마세요.
($ .../hackernews-node/src/resolvers/Link.js

module.exports = {
  postedBy,
  votes,      // 수정
}

Vote 타입이 형성하는 관계도 처리해주어야 합니다.

resolvers 디렉토리에 Vote.js 파일을 새로 생성합니다.
($ .../hackernews-node)

touch src/resolvers/Vote.js

다음으로, 해당 파일에 아래 코드를 추가합니다.
($ .../hackernews-node/src/resolvers/Vote.js

function link(parent, args, context) {
  return context.prisma.vote({ id: parent.id }).link()
}

마지막으로, Vote의 리졸버를 index.js 파일 내의 resolvers 객체에 포함시켜야 합니다.

index.js 파일을 열고, 최상단에 새로운 import 구문을 추가합니다.
($ .../hackernews-node/src/index.js

const Vote = require('./resolvers/Vote')

마지막으로, resolvers 객체에 Vote 리졸버를 포함시킵니다.
($ .../hackernews-node/src/index.js

const resolvers = {
  Query,
  Mutation,
  Subscription,
  User,
  Link,
  Vote,         // 수정
}

이제, GraphQL API에서 vote 뮤테이션을 사용할 수 있게 되었습니다! 👏

새로운 추천 구독하기

이번 장의 마지막 작업은 새로운 Vote가 생성되었을 때 작동하는 구독을 추가하는 것입니다. 앞서 newLink 쿼리에 대한 구독을 다룰 때와 비슷한 방식입니다.

GraphQL 스키마에서 Subscription 타입에 새로운 필드를 추가합니다.
($ .../hackernews-node/src/schema.graphql

type Subscription {
  newLink: Link
  newVote: Vote    # 수정
}

다음으로, 구독 타입의 새로운 필드에 대응하는 리졸버 함수를 추가합니다.

Subscription.js 파일에 아래의 코드를 추가합니다.
($ .../hackernews-node/src/resolvers/Subscription.js

function newVoteSubscribe(parent, args, context, info) {
  return context.prisma.$subscribe.vote({ mutation_in: ['CREATED'] }).node()
}

const newVote = {
  subscribe: newVoteSubscribe,
  resolve: payload => {
    return payload
  },
}

export 구문을 수정합니다.
($ .../hackernews-node/src/resolvers/Subscription.js

module.exports = {
  newLink,
  newVote,      // 수정
}

좋아요, 다 됐습니다! 이제 새로 만든 newVote 구독의 구현이 잘 되었는지 테스트할 수 있습니다.

  • 아직 하지 않았다면, CTRL+C를 누르고, node src/index.js를 실행하여 서버를 재시작합니다.
  • 다음으로, 새로운 브라우저 화면을 켜서 http://localhost:4000`으로 이동합니다.

아래의 구독을 사용해봅시다.

subscription {
  newVote {
    id
    link {
      url
      description
    }
    user {
      name
      email
    }
  }
}

이제 vote 뮤테이션을 발생시키면 됩니다. 아래의 예시 코드를 참고하시기 바랍니다. __LINK_ID__ 부분을 데이터베이스 상의 실제 Linkid값으로 대체하고 사용해야 합니다. 또한, 뮤테이션을 발생시킬 때 해당 요청이 인증되어야 합니다.

mutation {
  vote(linkId: "__LINK_ID__") {
    link {
      url
      description
    }
    user {
      name
      email
    }
  }
}

Quiz

다음 중 옳은 것은?

  • 구독은 "요청-응답" 사이클을 따른다
  • 구독은 MailChimp로 구현된다
  • 구독은 일반적으로 WebSockets을 사용하여 구현된다
  • 구독은 Query 타입으로 정의되고, @realtime 지시자를 표시한다

6개의 댓글

comment-user-thumbnail
2019년 12월 19일

정주행끝 !
유익했네요 감사합니다 ㅎㅎ

1개의 답글
comment-user-thumbnail
2020년 2월 6일

와 .... 이제 막 그래프큐엘에 아폴로서버익스프레스 붙여서 컨텍스트를 어떻게 쓸지 이리저리 고민중이였는데 프리즈마 너무좋네요 이걸로 도입해야겠어요 ㅎㅎ 좋은 글 감사합니다 ㅎㅎ

1개의 답글
comment-user-thumbnail
2020년 8월 11일

vote에 아래 link 코드 외에도
function link(parent, args, context) {
return context.prisma.vote({ id: parent.id }).link();
};

이와 같은 user 코드도 추가해야 될 것 같아요
function user(parent, args, context){
return context.prisma.vote({ id: parent.id }).user();
};

1개의 답글