오늘은 리액트에서 Apollo Client 를 사용하는 경우
Redux, Recoil, Jotai, Zustand 와 같은 상태관리 라이브러리를 사용하지 않고 Apollo Client 만을 사용하여 클라이언트 내부 상태관리하는 방법에 대해 알아보겠습니다.
주의할 점은 Apollo Client 가 GraphQL + 클라이언트 내부 상태관리도 가능하다는 것이지, GraphQL 를 사용하지 않는 환경에서 이미 잘 사용하고 있는 상태관리 라이브러리를 대체해서는 안됩니다 🙅♂️
이미 다 아시겠지만, Apollo Client 를 통해 Apollo Server 에 Query를 요청하는 방식부터 살펴봅시다.
Apollo Client 를 통해 서버에 Query 요청하는 방식
Apollo Client와 GraphQL API 설정
먼저, Apollo Client를 설정하고 GraphQL API와 상호 작용할 수 있도록 설정합니다.
이를 위해 @apollo/client 패키지를 설치하고, ApolloClient 인스턴스를 생성하고,
ApolloProvider를 사용하여 React 앱에 Apollo Client를 추가합니다.
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { ApolloProvider } from '@apollo/client/react';
const client = new ApolloClient({
uri: 'https://example.com/graphql',
cache: new InMemoryCache()
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
GraphQL 쿼리 작성 및 사용
이제 GraphQL API와 상호 작용하기 위한 쿼리를 작성하고 Apollo Client를 사용하여 데이터를 가져와 사용할 수 있습니다.
import { gql, useQuery } from '@apollo/client';
const GET_USERS = gql`
query getUsers {
users {
id
name
email
}
}
`;
function Users() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return (
<ul>
{data.users.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
);
}
지금까지 useQuery를 사용하여 서버에서 데이터를 가져오는 방법이었습니다.
아래는 예시는 조금 다르지만 흐름을 파악하기 위해 공식홈페이지에서 가져온 이미지 입니다.
오늘 저희가 해보고 싶은 것은 상태관리 라이브러리 없이 클라이언트 로컬에서 데이터를 관리하는 것 입니다.
아마도 위의 흐름도에서 서버와 오가는 부분이 없이 Apollo Client <-> Cache 부분만 있는 형태라고 생각하면 될 것 같습니다.
Apollo 클라이언트의 로컬 전용 필드
@client 지시자를 붙여서 GraphQL 서버의 스키마에 정의되지 않은 로컬 전용 필드를 지정해줄 수 있습니다.
참고로 하위 필드가 있는 필드에 @client 지시문을 적용하면 모든 하위 필드에도 자동 적용됩니다.
또한 로컬 전용 필드(isInCart) 서버에서 가져온 필드(name, price)가 모두 포함될 수 있다는 것을 알아야 합니다.
query ProductDetails($productId: ID!) {
product(id: $productId) {
name
price
isInCart @client # This is a local-only field
}
}
const GET_PRODUCT_DETAILS = gql`
query ProductDetails($productId: ID!) {
product(id: $productId) {
name
price
purchaseStatus @client {
isInCart # @client 가 자동 적용
isOnWishlist # @client 가 자동 적용
}
}
}
`;
그러면 이 로컬 전용 필드는 어디에 저장이 될까요?
InMemoryCache 생성시 각 필드에 대한 정책을 설정해줄 수 있는데요.
LocalStorage에 저장할 수 있습니다.
아래는 예시입니다.
동기식으로 데이터를 읽어옵니다.
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
);
}
}
}
}
}
});
비동기 작업과 유사한 방식으로도 구성할 수 있습니다.
new InMemoryCache({
typePolicies: {
Person: {
fields: {
isInCart: {
read(_, { variables, storage }) {
if (!storage.var) {
storage.var = makeVar(false);
setTimeout(() => {
storage.var(
localStorage.getItem('CART').includes(
variables.productId
)
);
}, 100);
}
return storage.var();
}
}
}
}
}
})
저장 & Mutation
2가지 Mutation 방법이 존재합니다.
- writeQuery 또는 writeFragment 사용
- 반응변수 사용
writeQuery 또는 writeFragment 사용
- writeQuery 또는 writeFragment 를 사용하여 캐시된 필드를 수정하면 해당 필드를 포함하는 모든 활성 쿼리가 자동으로 새로 고쳐집니다.
- 장점 : 캐시에 있는 로컬 전용 필드에 대한 필드 정책을 정의할 필요가 없습니다 .
- 단점 : 반응변수를 사용하는 것 보다는 많은 코드를 사용해야 합니다.
아래는 writeQuery 사용 예시입니다.
얘는 cache 인스턴스에 필드정책을 써주지 않고 로컬스토리지에서 확인하네요 ?
const cache = new InMemoryCache();
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache
});
const IS_LOGGED_IN = gql`
query IsUserLoggedIn {
isLoggedIn @client
}
`;
cache.writeQuery({
query: IS_LOGGED_IN,
data: {
isLoggedIn: !!localStorage.getItem("token"),
},
});
function App() {
const { data } = useQuery(IS_LOGGED_IN);
return data.isLoggedIn ? <Pages /> : <Login />;
}
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById("root"),
);
반응 변수
- Apollo Client 3의 새로운 기능 입니다.
- Apollo Client 캐시 외부의 로컬 상태를 나타내는 데 유용한 메커니즘 입니다.
- 반응형 변수는 캐시와 분리되어 있기 때문에 데이터 정규화를 적용하지 않아서 모든 유형 및 구조의 데이터를 저장 가능하며 GraphQL 구문을 사용하지 않고도 애플리케이션 어디에서나 상호 작용할 수 있습니다.
- 반응 변수를 수정하면 해당 변수에 의존하는 모든 활성 쿼리의 업데이트가 트리거 됩니다.
아래는 반응 변수 사용 예시 입니다.
// Cart.js
export const GET_CART_ITEMS = gql`
query GetCartItems {
cartItems @client
}
`;
// cache.js
import { makeVar } from '@apollo/client' ;
const cartItemsVar = makeVar ( [ ] ) ; // 반응 변수를 생성
export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
cartItems: {
read() {
return cartItemsVar(); // cartItems 이 read 쿼리될 때마다 반응 변수의 값을 반환
}
}
}
}
}
});
버튼 클릭시 반응 변수를 업데이트합니다. (카트에 새로운 product 를 추가!)
이 경우 cartItems 필드를 포함하는 모든 활성 쿼리에게 알립니다.
import { cartItemsVar } from './cache';
// ... other imports
export function AddToCartButton({ productId }) {
return (
<div class="add-to-cart-button">
<Button onClick={() => cartItemsVar([...cartItemsVar(), productId])}>
Add to Cart
</Button>
</div>
);
}
마침 Cart 컴포넌트에서 cartItems 필드를 Read 하는 쿼리를 사용하고 있었군요.
아까 버튼을 눌러서 카트에 새 product 를 추가를 해주었으니 반응 변수를 통해 알림을 받았을 것이고,
자동으로 데이터를 새로 가져와서 리렌더링 하게 됩니다.
// 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>
);
}
위와 같이 하지 않고 아래와 같이 직접 반응 변수를 읽고 반응할 수도 있습니다.
useReactiveVar hook을 사용하는 방법입니다.
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>
);
}
그러면 둘 중에 어느것을 사용해야 할까요?
ChatGPT 형님께 여쭤보니 반응 변수를 추천하네요.
제가 보기에도 반응 변수가 사용하기에는 훨씬 굉장히 편해보입니다.
writeQuery 가 데이터를 수동으로 관리한다는게 이해가 안되어서 더 물어보았습니다. 🤔
그건 반응 변수도 마찬가지 아닌가?..
아~ 반응 변수는 캐시와는 상관없이 별도로 관리되는 변수였다는걸 깜빡했습니다.
그래서 이 반응 변수를 변경하면 자동으로 감지해서 캐시도 업데이트 한다는걸 의미한 거였네요. 이제 이해 됐습니다!
세션 간 로컬 상태 유지
브라우저 새로고침 시에도 상태를 유지하는 기능입니다.
apollo3-cache-persist 라이브러리를 사용하면 세션 간 Apollo 클라이언트 캐시를 유지하고 재수화 하는데 도움이 됩니다.
apollo3-cache-persist
Simple persistence for all Apollo cache implementations. Latest version: 0.14.1, last published: 9 months ago. Start using apollo3-cache-persist in your project by running `npm i apollo3-cache-persist`. There are 31 other projects in the npm registry using
www.npmjs.com
로컬 전용 필드를 GraphQL 변수로 사용
GraphQL 쿼리가 변수를 사용하는 경우 해당 쿼리의 로컬 전용 필드는 해당 변수의 값을 제공할 수 있습니다 .
@export(as: "variableName") 와 같이 써야 합니다.
@export 지시문
- Apollo Client 3.4 버전부터 도입된 기능
- GraphQL 쿼리의 결과를 다른 쿼리에서 재사용할 수 있게 해주는 지시문입니다.
- 여러 쿼리에서 중복되는 결과를 다시 쿼리하는 것을 방지하여 클라이언트의 성능을 향상시킬 수 있습니다.
const GET_CURRENT_AUTHOR_POST_COUNT = gql`
query CurrentAuthorPostCount($authorId: Int!) {
currentAuthorId @client @export(as: "authorId")
postCount(authorId: $authorId) # currentAuthorId 쿼리 결과를 가져와서 사용!
}
`;
열심히 공부했으니 잘 써먹으러 가야겠습니다..
참고 : (공홈)https://www.apollographql.com/docs/react/local-state/local-state-management
'프론트엔드 개발 > GraphQL' 카테고리의 다른 글
Apollo Client 와 Relay 를 비교해보자 (1) | 2024.10.01 |
---|---|
Next.js 의 ServerSide 에서 Apollo Client Cache 데이터가 모든 사용자 간에 공유되는 이슈 (0) | 2023.12.19 |
Apollo Subscription 으로 GraphQL 서버와 WebSocket 통신하기 (0) | 2023.04.17 |
Apollo graphql 스키마와 리졸버에 대해 알아보자. 사용법 (0) | 2023.02.12 |
Apollo 오디세이(ODYSSEY) graphql 강의 (0) | 2023.02.11 |