[Apollo] 5장. Querying local state

Apollo 써보기

출처 : Apollo 공식 레퍼런스

목차


Querying local state


쿼리 예제

const GET_TODOS = gql`
  {
    todos @client { // @client를 붙여서 얘들이 로컬에서 끌고와야 하거나 실행되어야 한다는 걸 알랴줌!
      id
      completed
      text
    }
    visibilityFilter @client
  }
`;

function TodoList() {
  const { data: { todos, visibilityFilter } } = useQuery(GET_TODOS);
  return (
    <ul>
      {getVisibleTodos(todos, visibilityFilter).map(todo => (
        <Todo key={todo.id} {...todo} />
      ))}
    </ul>
  );
}

Initializing the cache

const cache = new InMemoryCache();
const client = new ApolloClient({
  cache,
  resolvers: { /* ... */ },
});

cache.writeData({
  data: {
    todos: [], // 이렇게 빈 칸이어도 되니까 먼저 모양만 잡아두자.
    visibilityFilter: 'SHOW_ALL',
    networkStatus: {
      __typename: 'NetworkStatus',
      isConnected: false,
    },
  },
});

Reset the store

const data = {
  todos: [],
  visibilityFilter: 'SHOW_ALL',
  networkStatus: {
    __typename: 'NetworkStatus',
    isConnected: false,
  },
};

cache.writeData({ data });
client.onResetStore(() => cache.writeData({ data }));

Local data query flow

  1. isInCart와 관련된 리졸버 함수가 있는지 찾는다

    • ApolloClient 생성자의 resolvers 인자라든가…
    • Apollo ClientsetResolvers, addResolvers 메소드라든가…
  2. 만약 맞는 리졸버 함수가 없으면, Apollo Client cache를 확인해서 isInCart 값이 있는지 찾는다.

  3. @client가 안 붙은 다른 값들은 쿼리하기 위해 서버로 요청하게 된다.

  4. 결과가 나온 뒤에는 두 값이 merge되어서 나간다.

이건 GraphQL의 Query, Mutation, Subscription에 모두 적용된다!


Handling @client fields with resolvers

const GET_CART_ITEMS = gql`
  query GetCartItems {
    cartItems @client // 이게 붙었으니까 로컬에서 뒤질 것
  }
`;

const cache = new InMemoryCache();
cache.writeData({
  data: {
    cartItems: [], // 깨지면 안되니까 미리 초기화 해두기
  },
});

const client = new ApolloClient({
  cache,
  link: new HttpLink({
    uri: 'http://localhost:4000/graphql',
  }),
  resolvers: { // 리졸버를 생성자에서 등록해줬음 > Apollo Client의 internal resolver map에 등록됨
    Launch: { // Launch 라는 이름의 GraphQL 오브젝트 타입으로 등록된다
      isInCart: (launch, _args, { cache }) => { // 로컬 리졸버 정의
        // 여기서 launch는 서버에서 반환된 결과값... 여기서 id를 캐올 수 있다
        // 두 번째 인자인 argument는 딱히 줄 게 없어서 저렇게 skip
        // context에는 지금 캐시 볼 거니까 캐시를 넣어준다 > 데이터 끌어올 때 캐시를 바로 보게 됨!

        const { cartItems } = cache.readQuery({ query: GET_CART_ITEMS }); // 캐시에서 가져온다음에
        return cartItems.includes(launch.id); // true, false로 반환하게 될 것 (id를 가진 값이 있는지)
      },
    },
  },
});

const GET_LAUNCH_DETAILS = gql`
  query LaunchDetails($launchId: ID!) {
    launch(id: $launchId) {
      isInCart @client
      site
      rocket {
        type
      }
    }
  }
`;

Async local resolvers

const client = new ApolloClient({
  cache: new InMemoryCache(),
  resolvers: {
    Query: {
      async cameraRoll(_, { assetType }) { // return Promise
        try {
          const media = await CameraRoll.getPhotos({
            first: 20,
            assetType,
          });

          return {
            ...media,
            id: assetType,
            __typename: 'CameraRoll',
          };
        } catch (e) {
          console.error(e);
          return null;
        }
      },
    },
  },
});

Handling @client field with cache

const cache = new InMemoryCache();
const client = new ApolloClient({
  cache,
  link: new HttpLink({ uri: "http://localhost:4000/graphql" }),
  resolvers: {}, // 미리 정의한 로컬 리졸버는 없다
});

cache.writeData({
  data: {
    isLoggedIn: !!localStorage.getItem("token"), // 초기값을 설정해준다
  },
});

const IS_LOGGED_IN = gql`
  query IsUserLoggedIn {
    isLoggedIn @client // @client가 붙었으니 로컬 상태에서 뒤진다 > 근데 리졸버는 없다...
  }
`;

function App() {
  const { data } = useQuery(IS_LOGGED_IN); // 여기서 data에는 캐시에서 바로 뽑은 값이 들어감
  return data.isLoggedIn ? <Pages /> : <Login />;
}

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById("root"),
);

[주의] local resolver 없이 캐시에서 바로 뽑고싶으면, ApolloClient 생성자에 빈 resolvers 옵션을 줘야한다 (위 예제처럼)


Working with fetch policies

export const GET_LAUNCH_DETAILS = gql`
  query LaunchDetails($launchId: ID!) {
    launch(id: $launchId) {
      isInCart @client // 로컬에서 뒤진다!
      site
      rocket {
        type
      }
    }
  }
`;

export default function Launch({ launchId }) {
  const { loading, error, data } = useQuery(
    GET_LAUNCH_DETAILS,
    { variables: { launchId } }
    // fetchPolicy 정의 X > cache-first로 ㄱㄱ
  );
 ...
}

...

import { GET_CART_ITEMS } from './pages/cart';

export const resolvers = {
  Launch: {
    isInCart: (launch, _, { cache }) => {
      const { cartItems } = cache.readQuery({ query: GET_CART_ITEMS });
      return cartItems.includes(launch.id);
    },
  },
};


Forcing resolvers with @client(always: true)

const client = new ApolloClient({
  cache: new InMemoryCache(),
  resolvers: {
    Query: {
      isLoggedIn() {
        return !!localStorage.getItem('token'); // 로컬스토리지를 체크한다.
        // 쿼리가 실행될 떄마다 이 부분이 실행되었으면 좋겠다
      },
    },
  },
});

const IS_LOGGED_IN = gql`
  query IsUserLoggedIn {
    isLoggedIn @client(always: true) // 이렇게 해주면 된다!
  }
`;

[주의] always:true를 줘도, fetchPolicy 자체가 캐시 먼저로 되어있다면 (cache-first, cache-and-network, cache-only) 여전히… 쿼리는 캐시부터 볼 것!


Using @client fields as variables

const query = gql`
  query currentAuthorPostCount($authorId: Int!) {
    currentAuthor @client {
      name
      authorId @export(as: "authorId") // 캐시에서 authorId라는 이름으로 끌고와서
    }
    postCount(authorId: $authorId) // 여기서 활용한다!
  }
`;

const cache = new InMemoryCache();
const client = new ApolloClient({
  cache,
  resolvers: {
    Query: {
      postCount(_, { authorId }) { // 로컬 리졸버로 정의된 부분에서도 활용 가능하다.
        return authorId === 12345 ? 100 : 0;
      },
    },
  },
});

[주의] @export@client가 꼭 있어야 사용 가능함!