오늘은 리액트에서 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 클라이언트 캐시를 유지하고 재수화 하는데 도움이 됩니다.
로컬 전용 필드를 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 |