🖊️

Apollo Client

Tags
React
graphql
apollo
state manage
feed back
Date
2021/05/25

시작하기 전에

앞서 study 했던 SWR 과 내용이 거의 비슷합니다. 때문에 상세히 설명하지는 않겠습니다. 주로 어떻게 사용하는지, 사용했는지 위주로 하겠습니다

개요

현재 내가 근무하고 있는 BDL 팀에서는 graphql 을 적극적으로 사용하고 있다. 그리고 상태관리로서는 mobx (mobx-state-tree) 를 사용하고 있다.
근래 apollo-client 에 대한 글들을 많이 보게 되어서 새로운 프로젝트를 들어가면서 apollo client 를 한 번 도입해보려고 한다.

about apollo client

원래 Apollo Client는 GraphQL을 사용해 서버와 통신을 하며 반환 값을 캐시로 보관하는 상태 관리 라이브러리이다. 하지만 일부 서비스는 서버 없이 완전히 독립적으로 작동할 수 있고, Apollo Client는 그런 경우에도 로컬 상태를 관리할 수 있다. 즉, Apollo Client만 사용해 전역 상태관리를 할 수 있다.
물론 서버가 있는 경우에도 GraphQL 통신으로 가져온 상태와 로컬 상태 모두 함께 관리할 수 있다. 그렇기 때문에 이미 클라이언트에서 Apollo Client를 사용하고 있다면 굳이 Redux나 MobX같은 추가적인 상태관리 라이브러리를 사용하지 않고도 충분히 전역 상태관리를 적용할 수 있다.

작동 원리

cache 는 network layer 의 캐시가 아니라 apollo client 의 cache (마치 useMemo 와 같이)

로컬 상태 관리도 가능하다?

Local-only field

const cache = new InMemoryCache({ typePolicies: { // Type policy map Product: { fields: { // Field policy map for the Product type isInCart: { // Field policy for the isInCart field read(_, { variables }) { // The read function for the isInCart field return localStorage.getItem('CART').includes( variables.productId ); } } } } } });
JavaScript
const GET_PRODUCT_DETAILS = gql` query ProductDetails($productId: ID!) { product(id: $productId) { name price isInCart @client } } `;
JavaScript
read function 을 통해서 local field 의 값을 정의할 수 있다
query document 에서 field 뒤에 @client 를 붙이면 local-only field 로 인식한다
하나의 쿼리에 local-only fields 와 server fetched fields 를 모두 fetch 할 수 있다

Reactive variables

import { makeVar } from '@apollo/client'; //create const cartItemsVar = makeVar([]); // read // Output: [] console.log(cartItemsVar()); // modify cartItemsVar([100, 101, 102]); // Output: [100, 101, 102] console.log(cartItemsVar());
JavaScript
Apollo client cache 가 아니다! (그냥 store 라이브러리와 비슷하다고 보면 될 것 같다)

Collaboration

// cart.js export const GET_CART_ITEMS = gql` query GetCartItems { cartItems @client } `; export function Cart() { const { data, loading, error } = useQuery(GET_CART_ITEMS); if (loading) return <Loading />; if (error) return <p>ERROR: {error.message}</p>; return ( <div class="cart"> <Header>My Cart</Header> {data && data.cartItems.length === 0 ? ( <p>No items in your cart</p> ) : ( <Fragment> {data && data.cartItems.map(productId => ( <CartItem key={productId} /> ))} </Fragment> )} </div> ); }
JavaScript
// cache.js export const cartItemsVar = makeVar([]); export const cache = new InMemoryCache({ typePolicies: { Query: { fields: { cartItems: { read() { return cartItemsVar(); } } } } } });
JavaScript

after apollo client 3.2

import { useReactiveVar } from '@apollo/client'; export function Cart() { const cartItems = useReactiveVar(cartItemsVar); return ( <div class="cart"> <Header>My Cart</Header> {cartItems.length === 0 ? ( <p>No items in your cart</p> ) : ( <Fragment> {cartItems.map(productId => ( <CartItem key={productId} /> ))} </Fragment> )} </div> ); }
JavaScript

왜 apollo client 를 사용했는가

새로운 프로젝트를 시작하면서 store 에 구조를 정의하고 적용하는 것에 대한 피로
프로젝트의 요구사항이 복잡하지 않았다.
SWR 을 보면서 data fetching 라이브러리를 통해서 상태관리를 하는 것에 대한 장점을 알기는 했으나 적용해볼 기회가 없었다.
마침 우리 팀에서는 graphql(apollo) 을 적극적으로 사용하고 있었다.
⇒ SWR 말고 apollo client 를 사용해보자

그래서 사용해보니 좋던가?

결론부터 말하자면 처음에는 난잡했지만 결국엔 좋았다. 당연히 처음 사용해보는 라이브러리이다 보니 가끔 막히거나 찾아서 해결해야하는 부분들이 있었다. 그리고 store 라이브러리를 사용할 때보다 hook 에 대한 이해를 더 요구하는 점에서 러닝 커브가 조금 더 있었던 것 같다 (심하진 않았다). 하지만 store 를 사용하면서 느끼는 피로감 (fetch action 정의, 결과 (loading, error, result) 각각 정의 등등)을 해소할 수 있었다. 구체적으로 느낀 장점은 SWR 에서 느낀 결론 과 비슷하다.

How to use? (In our team project)

기본 설정

// src/apolloClient.ts import { ApolloClient, InMemoryCache } from '@apollo/client'; const cache = new InMemoryCache(); const client = new ApolloClient({ cache, }); export default client;
JavaScript
// src/App.tsx import { ApolloProvider } from '@apollo/client'; import client from 'src/apolloClient'; const App = () => ( <ApolloProvider client={client}> <AppComponent /> </ApolloProvider> ); export default App;
JavaScript

auth

import { setContext } from "@apollo/client/link/context"; import { ApolloClient, InMemoryCache, HttpLink, NormalizedCacheObject, } from "@apollo/client"; const httpLik = new HttpLink({ uri: clientSideEndPoint, }); const authLink = setContext(async (_, { headers }) => { // 매 query 마다 header를 정의할 수 있도록 const token = await getAccessToken(); return { headers: { ...headers, accept: "*/*", "auth": token, }, }; }); client = new ApolloClient({ link: authLink.concat(httpLik), cache: new InMemoryCache(), });
JavaScript

hooks

useQuery

code

useMutation

code

pagination

code

참고