-
Notifications
You must be signed in to change notification settings - Fork 5
Apollo Client의 상태 관리 방법
작성자: J054 김진관
Apollo Client는 local state와 remotely fetched state를 하나의 API로 관리할 수 있다.
사용자는 localStorage
나 Apollo Client cache 등과 같이 원하는 방식으로 어플리케이션의 local state를 저장할 수 있다. 사용자는 local data를 특정 field로 쿼리를 보낼 때 local data를 어떻게 fetch할 지 정의하면 된다. 또한 하나의 쿼리에 local에서 받아올 field와 remote에서 받아올 field를 모두 포함할 수 있다.
위의 flow를 위해서 Apollo Client 3에서는 두 가지 보완적인 local state 관리 방법을 제시한다. field policies
와 reactive variables
Apollo Client 3.0 이상에서 지원
Field policies는 GraphQL server의 스키마에서 정의하지 않은 field에 대해 요청을 할 때 어떻게 동작할 지를 정의할 수 있도록 해준다. local-only fields라고 정의하면 localStorage
나 reactive variables
와 같이 데이터가 어디에 있든지 가져올 수 있다.
local-only fields라고 정의된 fields의 value는 정의한 방식으로 local data를 읽어온다. 이렇게 정의하면 remote와 local fields 모두를 하나의 쿼리에 담을 수 있다.
우선 특정 field에 대한 field policy를 정의한다. field policy는 하나의 GraphQL 랴 field가 Apollo Client cache에서 fetch되는 방법과 기록되는 방법에 대한 custom logic이다. local-only와 remotely fetched field 모두에 대한 policy를 정의할 수 있다.
정의된 field policy는 InMemoryCache
의 생성자의 parameter로 제공된다.
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
);
}
}
}
}
}
});
위의 예시에서는 read
함수를 정의하고 있다. read 함수를 갖고 있는 field에 대해 쿼리를 보내면, cache는 이 함수를 불러서 field의 값을 계산한다. 이 경우에는 localStorage
에 있는 CART
배열의 productID를 확인해서 반환한다.
read
함수는 다음과 같은 로직을 포함할 수 있다.
- 다른 cache operation을 실행
- 데이터를 준비, validate, sanitize하는 helper 유틸이나 라이브러리 호출
- seperate store에서 데이터 fetch
- 사용량 측정 logging
local-only field는 read 함수를 정의한 것은 아니다. Apollo Client는 기본적으로 local-only field에 대해서 default cache lookup을 수행한다.
const GET_PRODUCT_DETAILS = gql`
query ProductDetails($productId: ID!) {
product(id: $productId) {
name
price
isInCart @client
}
}
`;
@client
directive는 해당 field가 local-only field라는 것을 알려준다. 따라서 이 field에 대해서는 Apollo Client가 server로 요청을 보낼 때 생략한다. 최종 결과는 모든 local과 remote field를 받아온 후에 반환된다.
만약 subfield가 있는 field에 @client
directive를 선언하면 해당 field의 모든 subfield는 자동으로 @client
로 지정된다.
const GET_PRODUCT_DETAILS = gql`
query ProductDetails($productId: ID!) {
product(id: $productId) {
name
price
purchaseStatus @client {
isInCart
isOnWishlist
}
}
}
`;
Apollo Client는 local state가 어떻게 저장되어 있는지에 상관 없이 local state에 대해 요청을 보낼 수 있다. Apollo Client에서는 local state를 나타내는(저장하는) 두가지 유용한 option을 제공한다.
- Reactive variables
- Apollo Client cache
Reactive variables는 Apollo Client 3에 새로 나온 Apollo Client cache 외부에 local state를 저장하는 하나의 방식이다. 다음과 같은 특징이 있다.
- Reactive variables는 GraphQL operation 없이 어디에서든지 읽고 변경할 수 있다.
- Apollo Client cache와는 다르게 data noramlization을 강제하지 않는다. 어떠한 형태로든 저장할 수 있다.
- 만약 reactive variable에 의존하고 있는 어떤 field의 값이 변경된다면 해당 field를 포함하고 있는 모든 query는 자동으로 재실행된다. + UI 컴포넌트도
export const GET_CART_ITEMS = gql`
query GetCartItems {
cartItems @client
}
`;
import { makeVar } from '@apollo/client';
export const cartItemsVar = makeVar([]);
변수의 현재 값을 가져오고 싶다면 cartItemsVar()
함수를 호출하면 되고 새로운 value를 설정하려면 cartItemsVar(newValue)
를 호출하면 된다.
export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
cartItems: {
read() {
return cartItemsVar();
}
}
}
}
}
});
//AddToCartButton.js
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.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>
);
}
Cart
컴포넌트가 GET_CART_ITEMS
쿼리를 사용하고 있기 때문에 cartItemsVar
이 변경될 때마다 자동으로 refresh된다.
추가적으로 Apollo Client 3.2.0부터는 userReactiveVar
hook을 사용해서 reactive variable을 직접적으로 읽어올 수 있다.
// Cart.js
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>
);
}
이전의 useQuery
예제에서는 cartItemsVar
이 업데이트될 때마다 현재 mout된 모든 Cart
컴포넌트들이 다시 렌더링된다. useReactiveVar
없이 cartItemsVar()
를 호출하면 종속성이 캡쳐되지 않으므로 향후 변수 업데이트는 구성 요소를 다시 렌더링하지 않습니다. (아마도 cartItemsVar() 함수를 직접 부른다는 얘기일 듯...?) 이 두가지 접근 방식은 서로 다른 상황에서 유용하다.
Apollo Client cache는 여러 이점이 있지만 대부분 reactive variables를 사용할 때보다 코드가 길 것이다.
- cache를 사용하면 local-only fields에 대한 field policy를 정의할 필요가 없다.
read
함수를 정의하지 않은 경우에는 기본적으로 Apollo Client가 cache에 있는 값을 직접적으로 가져오려고 시도한다. -
writeQuery
나writeFragment
로 cache field를 변경하면, 해당 field를 포함하고 있는 모든 query들이 자동으로 재실행된다.
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 />;
}
Apollo Client cache에 저장하더라도 read
함수를 정의할 수 있다.
값을 변경하는 것은 field를 어떻게 저장했는지에 따라 달라진다.
- reactive variable을 사용한 경우, reactive variable의 새로운 값을 지정해주기만 하면된다. 그러면 Apollo Client가 자동으로 변화를 감지하고 영향을 받은 field를 포함하는 모든 operation을 자동으로 재실행한다.
-
cache를 사용한 경우,
writeQuery
,writeFragment
또는cache.modify
중에 하나를 호출하면 된다. 이 경우에도 영향 받는 모든 operation을 실행한다. -
**localStorage
와 같은 다른 storage method를 사용한 경우**, 새로운 값을 할당할 수 있는 적절한 메소드를 호출해야한다. 그 다음,cache.evict
를 호출해서 영향을 받는 operation이 재실행되도록 강제할 수 있다. 호출할 때 field의 id나 local-only field의 이름을 제공할 수 있다.
@export(as: "varibaleName")
directive를 사용하면 된다.
const GET_CURRENT_AUTHOR_POST_COUNT = gql`
query CurrentAuthorPostCount($authorId: Int!) {
currentAuthorId @client @export(as: "authorId")
postCount(authorId: $authorId)
}
`;
위의 예시에서 local-only field currentAuthorId
를 postCount
의 $authorId
로 전달하고 있다.
postCount
가 local-only field라도 사용할 수 있다.
@export
사용할 때 주의사항
-
@export
를 사용하기 위해서,@client
를 사용하는 field가 있어야 한다. -
@export
의 값을 사용하기 위해선 이 값을 사용하기 전에 먼저 나타나야 한다. -
@export
로 동일한 이름의 variable에 할당될 경우 가장 나중의 값으로 사용된다. development mode에서는 warning message가 로그로 나타날 것이다. -
@export
를 먼저 불러야한다는 말을 보면 GraphQL 명세에서 operation의 순서가 결과에 영향을 주지 않아야 한다는 원칙을 위반한 것처럼 보이지만 실제로는@export
variable 값들은 모두 remote server에 전달되기 전에 모두 채워진다. 그리고 local-only field는 실제 서버로 전달될 때 생략된다.
🏡Home
- Apollo References
- Schema Directives
- Apollo Client - Local State
- GraphQL Execution
- Apollo Server Execution
- Apollo Client Cache
- Apollo Client Execution
- Mongoose-Populate