구독
백엔드 데이터의 변경 사항에 대해 클라이언트에 실시간으로 알리는데 유용합니다.
Query,Mutation 과 달리 지속적인 연결을 유지해야 하기 때문에 graphql-ws 라이브러리를 통해 WebSocket으로 통신합니다.
구독은 이럴 때 사용합니다.
이 경우가 아니라면 쿼리를 사용하여 간헐적 폴링을 하거나 필요한 순간에만 쿼리 하세요.
또한 구독은 외부 데이터의 변경 사항을 구독하는 것입니다. 로컬 클라이언트 캐시의 변경사항 구독에 사용할 수 없습니다!
- 큰 개체에 대한 작은 증분 변경
- 큰 덩어리 객체에서 필요한 개별 필드의 업데이트가 발생했을때 가져오는 경우를 뜻합니다.
- 대기 시간이 짧은 실시간 업데이트
- 예를 들면 채팅 앱에서 실시간 메시지를 주고 받는 경우
Apollo Client 에서 지원하는 라이브러리
- graphql-ws (권장)
- subscriptions-transport-ws (구 방식)
구독 정의
서버, 클라이언트 측 모두에서 구독을 정의해야 합니다.
서버 측
- GraphQL 스키마에서 사용 가능한 구독을 유형의 필드로 정의합니다.
type Subscription {
commentAdded(postID: ID!): Comment
}
클라이언트 측
- Apollo 클라이언트가 실행할 각 구독의 모양을 정의합니다.
const COMMENTS_SUBSCRIPTION = gql`
subscription OnCommentAdded($postID: ID!) {
commentAdded(postID: $postID) {
id
content
}
}
`;
일반적인 쿼리와는 다릅니다. 서버에서 즉시 응답을 반환할 거란 기대 없습니다.
특정 이벤트가 발생했을 때에만 응답을 받습니다.
Setup
WebSocket 사용을 위한 라이브러리를 설치합니다.
npm install graphql-ws
이떄 Apollo Link 라는 것을 사용해야 합니다.
Apollo Link 란?
Apollo 클라이언트의 네트워크 통신을 사용자 지정하는 데 도움이 되는 라이브러리 입니다.
일반적으로 알고있는 middleware 와 같은 역할을 합니다. axios 로 따지면 axios intercepter 같은 녀석입니다.
import { ApolloLink } from '@apollo/client';
const timeStartLink = new ApolloLink((operation, forward) => {
operation.setContext({ start: new Date() }); // 무언가 수행
return forward(operation); // 다음으로 링크를 호출
});
그리고 이러한 링크를 연결 (체인)
import { ApolloLink } from '@apollo/client';
const roundTripLink = new ApolloLink((operation, forward) => {
// Called before operation is sent to server
operation.setContext({ start: new Date() });
return forward(operation).map((data) => {
// Called after server responds
const time = new Date() - operation.getContext().start;
console.log(`Operation ${operation.operationName} took ${time} to complete`);
return data;
});
});
Apollo Client 인스턴스에 link 추가하면 완료
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { RetryLink } from '@apollo/client/link/retry';
const directionalLink = new RetryLink().split(
(operation) => operation.getContext().version === 1,
new HttpLink({ uri: "http://localhost:4000/v1/graphql" }),
new HttpLink({ uri: "http://localhost:4000/v2/graphql" })
);
const client = new ApolloClient({
cache: new InMemoryCache(),
link: directionalLink
});
네 Apollo Link 를 사용하여 네트워크 통신 사이에 WebSocket 을 끼워넣는다는 의미로 이해할 수 있겠네요.
아주 간단하게 GraphQLWsLink 링크 체인에 추가만 해주면 됩니다.
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
const wsLink = new GraphQLWsLink(createClient({
url: 'ws://localhost:4000/subscriptions',
}));
그런데 의문인 것은 이렇게 하면 모든 네트워크 통신들이 WebSocket 으로 되는게 아닌가 싶습니다.
기존 Query/Mutation 은 HTTP 를 그대로 사용해야 하는데요.
그래서 boolean 검사 결과에 따라 HttpLink 또는 GraphQLWsLink 를 사용하도록 분할 기능을 제공합니다.
분할 기능을 이용하여 실행 중인 작업 유형에 알맞은 링크를 결합합니다.
import { split, HttpLink } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql'
});
const wsLink = new GraphQLWsLink(createClient({
url: 'ws://localhost:4000/subscriptions',
connectionParams: {
authToken: user.authToken, // 통신에 인증이 필요한 경우 이렇게 추가해야 합니다.
},
}));
// The split function takes three parameters:
//
// * A function that's called for each operation to execute
// * The Link to use for an operation if the function returns a "truthy" value
// * The Link to use for an operation if the function returns a "falsy" value
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link: splitLink, // 링크 연결
cache: new InMemoryCache()
});
구독 실행
리액트에서 useSubscription hook 을 사용하여 구독합니다.
useQuery hook과 거의 동일하게 동작하므로 어렵지 않습니다. 매우 간단합니다.
useSubscription hook 의 응답값의 data 에는 항상 최신 데이터가 들어가게 됩니다.
const COMMENTS_SUBSCRIPTION = gql`
subscription OnCommentAdded($postID: ID!) {
commentAdded(postID: $postID) {
id
content
}
}
`;
function LatestComment({ postID }) {
const { data, loading } = useSubscription(
COMMENTS_SUBSCRIPTION,
{ variables: { postID } }
);
return <h4>New comment: {!loading && data.commentAdded.content}</h4>;
}
블로그의 게시물에 대한 기존 댓글을 모두 가져오는 쿼리인 COMMENTS_QUERY 가 있고
새 댓글 추가 COMMENTS_SUBSCRIPTION 에 대한 구독을 하고 싶은 경우
useQuery 도 쓰고 useSubscription 도 써야 할까요?
이럴땐 useQuery 응답값 중 하나인 subscribeToMore 함수를 사용하면 됩니다.
이 함수는 무한스크롤 또는 페이지네이션 작업 시 사용되는 fetchMore 과 유사합니다.
- document : 실행할 구독을 나타냅니다.
- variables : 구독을 실행할 때 포함할 변수를 나타냅니다.
- updateQuery : 이전 쿼리의 현재 캐시된 결과와 subscriptionData를 GraphQL 서버에서 푸시한 결과와 결합하는 방법을 Apollo 클라이언트에 알려주는 기능입니다. 이 함수의 반환 값은 쿼리에 대해 현재 캐시된 결과를 완전히 대체합니다 .
function CommentsPageWithData({ params }) {
const { subscribeToMore, ...result } = useQuery(
COMMENTS_QUERY,
{ variables: { postID: params.postID } }
);
return (
<CommentsPage
{...result}
subscribeToNewComments={() =>
subscribeToMore({
document: COMMENTS_SUBSCRIPTION,
variables: { postID: params.postID },
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev;
const newFeedItem = subscriptionData.data.commentAdded;
return Object.assign({}, prev, {
post: {
comments: [newFeedItem, ...prev.post.comments]
}
});
}
})
}
/>
);
}
아래와 같이 컴포넌트 마운트시 한번 수행시켜놓으면 구독 됩니다.
즉, 새로운 댓글이 추가될 때마다 업데이트 받아볼 수 있게 됩니다.
export function CommentsPage({subscribeToNewComments}) {
useEffect(() => subscribeToNewComments(), []);
return <>...</>
}
적절하게 연계해서 사용하는게 쉽지는 않아 보이네요.
계속 사용해 보면서 사용법에 익숙해져야 될 것 같습니다.
'프론트엔드 개발 > GraphQL' 카테고리의 다른 글
Apollo Client 와 Relay 를 비교해보자 (1) | 2024.10.01 |
---|---|
Next.js 의 ServerSide 에서 Apollo Client Cache 데이터가 모든 사용자 간에 공유되는 이슈 (0) | 2023.12.19 |
React에서 Apollo Client 를 상태관리 라이브러리로 사용하기 (0) | 2023.04.16 |
Apollo graphql 스키마와 리졸버에 대해 알아보자. 사용법 (0) | 2023.02.12 |
Apollo 오디세이(ODYSSEY) graphql 강의 (0) | 2023.02.11 |