시작하기 전에
개요
현재 내가 근무하고 있는 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