알립니다
이 번역 시리즈는 2019년 10월 경에 작성되었습니다. 원본인 GraphQL - Node 튜토리얼은 현재 새로운 버전으로 새롭게 작성되었습니다. 따라서 이 글은 Deprecated된 글임을 알려드립니다.
- 본 시리즈에서는 How to GraphQL의 Tutorial 문서들을 차례대로 번역합니다.
- 본 시리즈는 GraphQL Basic and Advanced 시리즈에서 이어집니다. GraphQL을 처음 접하는 분들은 해당 시리즈를 먼저 읽고 오시는 것을 추천드립니다.
- 이 글은 GraphQL-Node Tutorial - Realtime GraphQL Subscriptions을 번역한 글입니다.
- 오역 또는 의역이 있을 수 있습니다. 양해 부탁드리며, 수정이 필요한 부분은 댓글로 요청해주세요.
이번 장에서는 GraphQL 구독을 구현하여 실시간 기능을 어플리케이션에 추가하는 방법을 배웁니다. GraphQL 서버에서 노출할 아래의 두 가지 구독 기능을 구현하는 것이 목표입니다.
Link
가 생성되었을 때 구독 중인 클라이언트에 실시간 갱신을 전송Link
가 추천을 받았을 때 구독 중인 클라이언트에 실시간 갱신을 전송구독(Subscription)이란 특정 이벤트가 발생했을 때 서버가 클라이언트로 데이터를 전송해주는 GraphQL 기능입니다. 구독은 WebSocket을 사용하여 구현하는 것이 일반적입니다. 그러한 구성에서는 서버가 구독 중인 클라이언트와 지속적인 연결을 유지합니다. 이것은 기존에 API와 상호작용하던 방식인 "요청-응답 사이클"을 깨는 것이기도 합니다.
그 대신, 처음에 클라이언트는 관심있는 이벤트를 명시한 구독 쿼리를 전송하여, 서버와 길게 지속되는 연결을 형성합니다. 특정 이벤트가 발생할 때마다, 서버는 이 연결을 사용하여 구독 중인 (하나 또는 둘 이상의) 클라이언트에 이벤트 데이터를 푸시합니다.
다행히, Prisma는 구독 기능을 제대로 지원합니다.
Prisma의 데이터 모델 각각에 대하여 Prisma는 다음과 같은 이벤트들을 구독할 수 있도록 해줍니다.
Prisma 클라이언트가 가지는 $subscribe
메서드를 사용하면 위의 이벤트들을 구독할 수 있습니다.
Link
요소에 구독하기이야기는 이만하면 되었고, 직접 코드를 써보도록 하죠! 클라이언트가 새로 생성되는 Link
요소에 대하여 구독할 수 있도록, 구독 기능을 구현해보도록 하겠습니다.
쿼리와 뮤테이션과 마찬가지로, 가장 먼저 할 일은 GraphQL 스키마 정의를 확장하는 것입니다.
어플리케이션의 스키마를 열고
Subscription
타입을 추가합니다.
($ .../hackernews-node/src/schema.graphql)type Subscription { newLink: Link }
다음으로, newLink
필드에 대한 리졸버를 구현합니다. 구독을 다루는 리졸버는 쿼리와 뮤테이션의 경우와는 다소 차이가 있습니다.
데이터를 직접 반환하지 않고, AsyncIterator
를 반환합니다. 이것은 이후에 GraphQL 서버 측에서 클라이언트에 이벤트 데이터를 푸시하는 데에 사용됩니다.
구독 리졸버는 객체로 감싸지며, 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.jsconst Subscription = require('./resolvers/Subscription')
다음으로,
resolvers
객체의 정의를 아래와 같이 수정합니다.
($ .../hackernews-node/src/index.jsconst resolvers = { Query, Mutation, Subscription, // 수정 User, Link, }
모든 코드를 다 작성하였으면, 실시간 API를 테스트해볼 시간입니다. ⚡️ GraphQL Playground를 두 화면에 띄우고 테스트를 진행해볼 수 있습니다.
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.prismatype 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.graphqltype Vote { id: ID! link: Link! user: User! }
어떤 Link
가 받은 추천을 모두 쿼리하는 것 또한 가능해야 하므로, schema.graphql
파일에 정의된 Link
타입 또한 수정해야 합니다.
schema.graphql
파일을 열고Link
타입에votes
필드를 추가합니다.
($ .../hackernews-node/src/schema.graphqltype Link { id: ID! description: String! url: String! postedBy: User votes: [Vote!]! # 수정 }
그 다음은 이제 무엇인지 말 안해도 아시죠? 대응하는 리졸버 함수를 구현하겠습니다.
src/resolvers/Mutation.js
파일에 아래의 함수를 추가합니다.
($ .../hackernews-node/src/resolvers/Mutation.jsasync 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 } }, }) }
각 코드들을 분석해보겠습니다.
post
리졸버를 만들 때와 비슷하게, 첫번째로 할 일은 요청으로 들어오는 JWT가 유효한지 getUserId
헬퍼 함수를 사용하여 검증하는 것입니다. 만약 유효하다면, 함수는 해당 요청을 보낸 User
의 userId
를 반환합니다. 만약 JWT가 유효하지 않다면, 함수는 예외를 발생시킵니다.
prisma.$exists.vote(...)
함수는 처음 보실 겁니다. prisma
클라이언트는 각 모델에 대한 CRUD 메서드뿐 아니라, 각 모델마다 $exists
함수도 생성합니다. %exists
함수는 where
라는 필터 객체를 인자로 받는데, 이를 통하여 해당 타입의 요소와 관련된 특정 조건을 명시할 수 있습니다. 데이터베이스 내에서 적어도 1개 이상의 요소가 해당 조건을 만족한다면, $exists
함수는 true
를 반환합니다. 위 코드에서는 $exists
함수를 활용하여, 요청을 보낸 User
가 args.linkId
로 식별되는 Link
를 추천하였는지 여부를 확인하는 데에 사용됩니다.
$exists
함수가 false
를 반환한다면, createVote
메서드를 사용하여 새로운 Vote
를 생성하고, 이 Vote
는 User
와 Link
에 연결됩니다.
아참,
vote
리졸버를 포함하도록 export 구문을 수정하는 것도 잊지 마세요.
($ .../hackernews-node/src/resolvers/Mutation.jsmodule.exports = { post, signup, login, vote, // 수정 }
새롭게 생겨난 관계들을 GraphQL 스키마에서 파악할 수 있도록 처리하는 일도 필요합니다.
Link
타입에 votes
Vote
타입에 user
Vote
타입에 link
이전과 비슷하게, 새로운 필드에 대한 리졸버를 구현해야 합니다.
Link.js
파일을 열고 아래의 함수를 추가합니다.
($ .../hackernews-node/src/resolvers/Link.jsfunction votes(parent, args, context) { return context.prisma.link({ id: parent.id }).votes() }
새로운 리졸버를 exports 객체에 추가하는 것도 잊지 마세요.
($ .../hackernews-node/src/resolvers/Link.jsmodule.exports = { postedBy, votes, // 수정 }
Vote
타입이 형성하는 관계도 처리해주어야 합니다.
resolvers
디렉토리에Vote.js
파일을 새로 생성합니다.
($ .../hackernews-node)touch src/resolvers/Vote.js
다음으로, 해당 파일에 아래 코드를 추가합니다.
($ .../hackernews-node/src/resolvers/Vote.jsfunction link(parent, args, context) { return context.prisma.vote({ id: parent.id }).link() }
마지막으로, Vote
의 리졸버를 index.js
파일 내의 resolvers
객체에 포함시켜야 합니다.
index.js
파일을 열고, 최상단에 새로운 import 구문을 추가합니다.
($ .../hackernews-node/src/index.jsconst Vote = require('./resolvers/Vote')
마지막으로,
resolvers
객체에Vote
리졸버를 포함시킵니다.
($ .../hackernews-node/src/index.jsconst resolvers = { Query, Mutation, Subscription, User, Link, Vote, // 수정 }
이제, GraphQL API에서 vote
뮤테이션을 사용할 수 있게 되었습니다! 👏
이번 장의 마지막 작업은 새로운 Vote
가 생성되었을 때 작동하는 구독을 추가하는 것입니다. 앞서 newLink
쿼리에 대한 구독을 다룰 때와 비슷한 방식입니다.
GraphQL 스키마에서
Subscription
타입에 새로운 필드를 추가합니다.
($ .../hackernews-node/src/schema.graphqltype Subscription { newLink: Link newVote: Vote # 수정 }
다음으로, 구독 타입의 새로운 필드에 대응하는 리졸버 함수를 추가합니다.
Subscription.js
파일에 아래의 코드를 추가합니다.
($ .../hackernews-node/src/resolvers/Subscription.jsfunction 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.jsmodule.exports = { newLink, newVote, // 수정 }
좋아요, 다 됐습니다! 이제 새로 만든 newVote
구독의 구현이 잘 되었는지 테스트할 수 있습니다.
node src/index.js
를 실행하여 서버를 재시작합니다.아래의 구독을 사용해봅시다.
subscription {
newVote {
id
link {
url
description
}
user {
name
email
}
}
}
이제 vote
뮤테이션을 발생시키면 됩니다. 아래의 예시 코드를 참고하시기 바랍니다. __LINK_ID__
부분을 데이터베이스 상의 실제 Link
의 id
값으로 대체하고 사용해야 합니다. 또한, 뮤테이션을 발생시킬 때 해당 요청이 인증되어야 합니다.
mutation {
vote(linkId: "__LINK_ID__") {
link {
url
description
}
user {
name
email
}
}
}
Quiz
다음 중 옳은 것은?
- 구독은 "요청-응답" 사이클을 따른다
- 구독은 MailChimp로 구현된다
- 구독은 일반적으로 WebSockets을 사용하여 구현된다
- 구독은
Query
타입으로 정의되고,@realtime
지시자를 표시한다
와 .... 이제 막 그래프큐엘에 아폴로서버익스프레스 붙여서 컨텍스트를 어떻게 쓸지 이리저리 고민중이였는데 프리즈마 너무좋네요 이걸로 도입해야겠어요 ㅎㅎ 좋은 글 감사합니다 ㅎㅎ
정주행끝 !
유익했네요 감사합니다 ㅎㅎ