Skip to content

Apollo Client의 상태 관리 방법

Ahrim Yang edited this page Nov 30, 2020 · 3 revisions

작성자: J054 김진관

Local State

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 policiesreactive variables

Field policies와 local-only fields

Apollo Client 3.0 이상에서 지원

Field policies는 GraphQL server의 스키마에서 정의하지 않은 field에 대해 요청을 할 때 어떻게 동작할 지를 정의할 수 있도록 해준다. local-only fields라고 정의하면 localStoragereactive variables와 같이 데이터가 어디에 있든지 가져올 수 있다.

Local-only fields

local-only fields라고 정의된 fields의 value는 정의한 방식으로 local data를 읽어온다. 이렇게 정의하면 remote와 local fields 모두를 하나의 쿼리에 담을 수 있다.

Defining

우선 특정 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을 수행한다.

Querying

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
      }
    }
  }
`;

Storing

Apollo Client는 local state가 어떻게 저장되어 있는지에 상관 없이 local state에 대해 요청을 보낼 수 있다. Apollo Client에서는 local state를 나타내는(저장하는) 두가지 유용한 option을 제공한다.

  • Reactive variables
  • Apollo Client cache

Reactive variables

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() 함수를 직접 부른다는 얘기일 듯...?) 이 두가지 접근 방식은 서로 다른 상황에서 유용하다.

Cache

Apollo Client cache는 여러 이점이 있지만 대부분 reactive variables를 사용할 때보다 코드가 길 것이다.

  • cache를 사용하면 local-only fields에 대한 field policy를 정의할 필요가 없다. read 함수를 정의하지 않은 경우에는 기본적으로 Apollo Client가 cache에 있는 값을 직접적으로 가져오려고 시도한다.
  • writeQuerywriteFragment로 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 함수를 정의할 수 있다.

Modifying

값을 변경하는 것은 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의 이름을 제공할 수 있다.

Local-only fields를 GraphQL variables로 사용하기

@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 currentAuthorIdpostCount$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는 실제 서버로 전달될 때 생략된다.
Clone this wiki locally