React에서 Apollo Client를 이용하여 GraphQL 다루기, Apollo Client Hooks

Janet·2023년 12월 29일
0

GraphQL

목록 보기
3/3
post-thumbnail

I. GraphQL Client 라이브러리


GraphQL Client 라이브러리가 필요한 이유?

GraphQL Client 라이브러리는 GraphQL을 통해 원격 및 로컬 데이터 관리를 단순화하는 상태 관리 라이브러리입니다. 데이터의 캐싱 처리 및 편리한 GraphQL Query 작성, UI 자동 업데이트 등을 통해 효율적인 코드 작성에 도움을 줍니다.

대표적인 GraphQL Client 라이브러리

GraphQL 클라이언트 라이브러리에는 대표적으로 Apollo Client 외에도 Relay, URQL 등이 있습니다. 이 외에도 https://graphql.org/code 에서 언어나 프레임워크, 클라이언트, 서버마다 사용할 수 있는 GraphQL 라이브러리를 찾아볼 수 있습니다.

  1. Relay: Facebook이 개발, 2015년 출시. 퍼포먼스 최적화 강점으로 네트워크 트래픽 감소. 높은 러닝커브, 상대적으로 대규모 애플리케이션에 적합합니다.
  2. URQL(Universal React Query Library): 2018년 출시, GraphQL 클라이언트의 단순화, 경량화에 중점을 두었습니다.
  3. Apollo Client: 2016년 출시. 잘 갖춰진 공식 문서와 자료, 거의 모든 프론트엔드 프레임워크 지원합니다.

II. Apollo Client


작은 규모의 웹 애플리케이션에 테스트 할겸, 자료도 많고 널리 사용되는 Apollo Client를 선택하게 되었습니다. 또한 Apollo는 서버와 클라이언트 모두 제공합니다.

Apollo Client의 캐시 처리

Apollo Client의 InMemoryCache는 캐시 및 상태 관리를 담당하는 핵심 기능으로 클라이언트에서 받은 GraphQL 데이터를 메모리에 저장하고 쿼리에 따라 캐시된 데이터를 효율적으로 관리합니다.

Apollo Client는 GraphQL 쿼리 결과를 정규화된 로컬 인메모리 캐시에 저장합니다. 이를 통해 이미 캐시된 데이터 즉, 동일한 요청에 대해서는 새로운 네트워크 요청을 보내지 않고 거의 즉시 응답할 수 있습니다.

위에서 말하는 정규화(Normalization)란? 데이터를 정규화하여 저장한다는 것인데, 이는 중첩된 데이터 구조를 펼쳐서 저장하고 관리하는 방식으로, 쿼리에 필요한 데이터를 효율적으로 찾을 수 있게 해줍니다.

Apollo Client 공개 API로 테스트하기

Countries GraphQL API라는 Public API를 이용하여 GraphQL 서버를 따로 만들지 않고, 테스트 할 수 있습니다.

GraphQL Playground란?
GraphQL Playground는 GraphQL API를 GUI로 보여주어 시각적으로 탐색할 수 있게 하는 GraphQL IDE입니다. 따라서, GraphQL 서버에 쿼리를 테스트해보거나 디버깅할 수 있습니다. GUI로 쉽게 Database의 구조를 파악하고, 실제로 데이터를 출력해 볼 수 있어 편리합니다.

III. React에서 Apollo Client 세팅


Apollo Client 설치

  • $ npm install @apollo/client graphql
    • @apollo/client: 아폴로 클라이언트를 설정하는 데 필요한 패키지
    • graphql: GraphQL 쿼리를 구문 분석하는 로직을 제공하는 패키지

ApolloClient 초기화 및 Provider로 감싸기

  1. index.js에서 Apollo Client 모듈을 import합니다.
  2. GraphQL 서버로와 정보를 주고받을 ApolloClient 객체를 초기화하고, uri와 cache 필드에 각각 GraphQL API 서버의 주소와 InMemoryCache의 새 인스턴스를 전달합니다.
  3. ApolloProvider로 App 컴포넌트를 감싸주고 client prop에 ApolloClient 객체를 전달합니다.
// index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client';

// GraphQL 서버로와 정보를 주고받을 ApolloClient 객체
const client = new ApolloClient({
  uri: 'https://countries.trevorblades.com/', // GraphQL 서버의 주소
  cache: new InMemoryCache(), // InMemoryCache를 통한 캐시 관리
});

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </React.StrictMode>,
);

아래와 같이 index.js에서 query를 작성해서 제대로 데이터를 받아오는지 간단한 테스트를 해보았습니다.

const client = new ApolloClient({
  uri: 'https://countries.trevorblades.com',
  cache: new InMemoryCache(),
});

client
  .query({
    query: gql`
      query GetCountry {
        country(code: "KR") {
          name
          native
          capital
          emoji
          currency
          languages {
            code
            name
          }
        }
      }
    `,
  })
  .then((result) => console.log(result));

콘솔창을 보니 아래와 같이 정상적으로 데이터를 받아오고 있습니다.

아래 예시 코드를 작성하기에 앞서, 위의 나라 데이터가 담긴 GraphQL Query를 별도 파일에서 관리하며 export/import하여 사용하겠습니다. (src/graphql/index.js)

import { gql } from '@apollo/client';

export const GET_COUNTRY_QUERY = gql`
  query GetCountry($code: ID!) {
    country(code: $code) {
      name
      native
      capital
      emoji
      currency
      languages {
        code
        name
      }
    }
  }
`;

IV. Apollo Hooks로 GraphQL 데이터 통신하기


Apollo Client는 React Hooks과 같이 GraphQL을 호출할 수 있도록 useQuery, useMutation와 같은 Apollo Hooks 을 제공하며 gql이라는 template literal tag를 사용해 자바스크립트를 GraphQL 구문으로 바꿔줍니다.

Apollo Client에는 세 가지 Operation Type이 있습니다.

  • Query: 데이터 조회(Read)
  • Muation: 데이터 변경(Create, Update, Delete)
  • Subscription: 데이터 실시간 구독

Operation Type #1: Query

useQuery

GraphQL 데이터를 fetch하여 UI에 결과를 반영할 수 있게 도와주는 hooks입니다. 또한, error를 추적하고 loading 상태를 수동으로 확인하여 추가적인 로딩 관리 코드를 다룰 수 있도록 도와줍니다.

import { useQuery } from '@apollo/client';
import { GET_COUNTRY_QUERY } from '../graphql';

function UseQuery() {
  const { loading, error, data } = useQuery(GET_COUNTRY_QUERY, {
    variables: { code: 'KR' },
  });

  if (loading) return 'Loading...';
  if (error) return `Error! ${error.message}`;

  const country = data.country;

  return (
    <div>
      <h1>{country.name} Information</h1>
      <ul>
        <li>Country name: {country.name}</li>
        <li>Native: {country.native}</li>
        <li>Capital: {country.capital}</li>
        <li>National flag: {country.emoji}</li>
        <li>Currency: {country.currency}</li>
        <li>Languages code: {country.languages[0].code}</li>
        <li>Languages name: {country.languages[0].name}</li>
      </ul>
    </div>
  );
}

export default UseQuery;

위와 같이 데이터를 가져와 화면에 출력된 것을 확인할 수 있습니다. 또한, useQuery에는 쿼리(데이터)를 최신 상태로 유지하기 위한 방법이 2가지가 있습니다. 바로 Polling과 Refetching을 통해서 가능합니다.

1. Polling (주기적인 데이터 업데이트)
Polling은 지정된 간격으로 Query를 주기적으로 다시 실행함으로써 데이터를 업데이트하여 서버와 동기화를 제공합니다. 실시간 혹은 주기적으로 데이터를 가져와야 하는 경우에 유용합니다.
쿼리에 대한 폴링을 활성화하려면 pollInterval 옵션을 밀리초 단위의 간격으로 useQuery hook에 전달합니다.

const { data } = useSuspenseQuery(GET_COUNTRY_QUERY, {
  variables: { code },
  pollInterval: 5000, // 5초마다 Query를 실행하여 데이터 업데이트
});

2. Refetching (수동으로 데이터 업데이트):
Refetching은 특정 이벤트 또는 사용자 상호 작용(특정 버튼을 클릭하는 등의 상호 작용)에 따라 최신의 데이터로 업데이트하려는 경우에 사용할 수 있습니다.
refetch 함수에 새로운 변수 객체를 전달할 수 있고, 변수를 전달하지 않으면 이전에 사용한 Query를 최신의 데이터로 가져와 업데이트합니다.

import { useQuery } from '@apollo/client';
import { GET_COUNTRY_QUERY } from '../graphql';

function UseQuery() {
  const { loading, error, data, refetch } = useQuery(GET_COUNTRY_QUERY, {
    variables: { code: 'KR' },
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  const country = data.country;

  return (
    <div>
      <h1>{country.name} Information</h1>
      <ul>
        <li>Native: {country.native}</li>
        <li>Capital: {country.capital}</li>
        <li>National flag: {country.emoji}</li>
        <li>Currency: {country.currency}</li>
        <li>Languages code: {country.languages[0].code}</li>
        <li>Languages name: {country.languages[0].name}</li>
      </ul>
      <button onClick={() => refetch({ code: 'US' })}>Refetch with USA Code</button>
      <button onClick={() => refetch({ code: 'KR' })}>Refetch with Korea Code</button>
      <button onClick={() => refetch()}>Refetch without Changing Country Code</button>
    </div>
  );
}

export default UseQuery;

useLazyQuery

useLazyQuery는 GraphQL 쿼리를 수동으로 실행하고 싶을 때 사용됩니다. 컴포넌트가 마운트될 때 자동으로 실행되는 useQuery와 달리, useLazyQuery는 나중에 사용자 조작(버튼 클릭 등)과 같은 이벤트에 응답하여 쿼리를 원하는 시점에 실행하고 싶을 때 유용합니다.

import { useLazyQuery } from '@apollo/client';
import { GET_COUNTRY_QUERY } from '../graphql';

function UseLazyQuery() {
  const [fetchCountryData, { loading, data, error }] = useLazyQuery(GET_COUNTRY_QUERY);

  const handleButtonClick = () => {
    // 쿼리를 수동으로 실행
    fetchCountryData({ variables: { code: 'KR' } });
  };

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  const country = data?.country;

  return (
    <div style={{ marginTop: '30px' }}>
      <button onClick={handleButtonClick}>Fetch Country Data</button>
      {data && (
        <>
          <h1>{country.name} Information</h1>
          <ul>
            <li>Native: {country.native}</li>
            <li>Capital: {country.capital}</li>
            <li>National flag: {country.emoji}</li>
            <li>Currency: {country.currency}</li>
            <li>Languages code: {country.languages[0].code}</li>
            <li>Languages name: {country.languages[0].name}</li>
          </ul>
        </>
      )}
    </div>
  );
}
export default UseLazyQuery;

useSuspenseQuery

Apollo Client에서 useSuspenseQuery를 이용하여 React 18에 도입된 Suspense 기능을 활용한 데이터 가져오기가 가능합니다.
React의 Suspense란, 동시 렌더링 엔진을 사용하여 자식(children) 요소들이 로딩을 마칠 때까지 fallback을 표시할 수 있는 기능입니다.

<Suspense fallback={<Loading />}>
  <SomeComponent />
</Suspense>

위 코드와 같이 fallback에는 UI 로딩이 끝날 때까지 보여줄 로딩관련 컴포넌트(스피너 등)를 전달합니다.
아래 코드는 React Suspense와 Apollo Client의 useSuspenseQuery를 사용하여 구현한 예시 코드입니다. 로딩 중에 자동으로 Suspense를 활성화하고 로딩 상태를 처리합니다.

import { Suspense } from 'react';
import { useSuspenseQuery } from '@apollo/client';
import { GET_COUNTRY_QUERY } from '../graphql';

function SuspenseComponent({ code }) {
  const { data } = useSuspenseQuery(GET_COUNTRY_QUERY, {
    variables: { code },
  });
  return <p>Country name: {data.country.name}</p>;
}

function UseSuspense() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <SuspenseComponent code="KR" />
    </Suspense>
  );
}

export default UseSuspense;

Operation Type #2: Mutation

useMutation

useMutation은 GraphQL 서버에 데이터의 업데이트를 전송하기 위한 Hook입니다.
Mutation을 사용하기 위해서는 먼저 서버 단에서 Schema와 Resolver 작성이 필요합니다. (참고로 Apollo는 GraphQL 서버 라이브러리로 Apollo Server를 제공합니다.)

const [convertToUppercase, { loading, error, data }] = useMutation(
  CONVERT_TO_UPPERCASE_MUTATION
);
  • convertToUppercase: 뮤테이션을 수행하는 함수로 호출 시 뮤테이션이 서버로 전송되고 실행됩니다.
  • { data }: 뮤테이션의 실행 결과로부터 얻은 데이터입니다. 뮤테이션이 성공적으로 실행되면 data 객체에 결과 데이터가 저장됩니다.

아래는 useMutation 예시 코드입니다. loading, error, data를 통해 뮤테이션의 실행 상태와 결과를 처리하며, 버튼 클릭 시 비동기적으로 뮤테이션을 실행하는 예시입니다.

import { gql, useMutation } from '@apollo/client';

// GraphQL Mutation 정의
const CONVERT_TO_UPPERCASE_MUTATION = gql`
  mutation ConvertToUppercase($countryCode: ID!) {
    convertToUppercase(countryCode: $countryCode) {
      name
      native
      capital
      emoji
      currency
      languages {
        code
        name
      }
    }
  }
`;

function UseMutation() {
  const [convertToUppercase, { loading, error, data }] = useMutation(
    CONVERT_TO_UPPERCASE_MUTATION
  );

  // Mutation 실행 함수
  const handleConvertToUppercase = async () => {
    try {
      // country 코드를 원하는 값으로 설정
      const countryCode = 'KR';

      // useMutation 훅을 통해 Mutation 실행
      await convertToUppercase({
        variables: { countryCode },
      });

      // Mutation 실행 후의 로직
      // 예: 성공 메시지 표시 또는 필요한 업데이트 수행
      console.log('Mutation 실행 완료:', data);
    } catch (error) {
      // Mutation 실행 중 에러 처리
      console.error('Mutation 에러:', error.message);
    }
  };

  return (
    <div>
      <h1>useMutation Components</h1>
      <button onClick={handleConvertToUppercase} disabled={loading}>
        {loading ? 'Converting...' : 'Convert Languages to Uppercase'}
      </button>
      {error && <p>Error: {error.message}</p>}
    </div>
  );
}

export default UseMutation;

Operation Type #3: Subscription

useSubscription

Subscription은 Query처럼 데이터 조회를 위해 사용되지만, 주로 실시간(real-time) 애플리케이션 구현을 위해 사용됩니다. 서버에서 이벤트가 발생할 때마다 클라이언트에게 데이터를 전송합니다.
GraphQL 스키마에서 사용 가능한 구독을 Subscription 유형의 필드로 정의해야 합니다. 마찬가지로 클라이언트 단에서도 query와 같이 subscription 구문을 작성해야 합니다.
GraphQL 서버를 구현할 때, Apollo Server와 같은 웹소켓을 지원하는 라이브러리를 사용하여 GraphQL Subscription을 처리합니다.

아래는 Subscription을 사용하기 위한 순서/과정들을 간략하게 정리한 것입니다.

  1. 서버 단에서 subscription 스키마를 정의합니다.
  2. 클라이언트 단에서 subscription에 대한 GraphQL 구문을 작성합니다.
  3. 클라이언트에서 웹소켓 관련 패키지 설치: $ npm install graphql-ws
  4. index.js에서 Apollo client에서 제공하는 GraphQLWsLink, createClient 모듈을 import하고 초기화합니다.
// index.js

import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';

const wsLink = new GraphQLWsLink(createClient({
  url: 'ws://localhost:4000/subscriptions',
}));
  1. 기존 Query와 Mutation관련 GraphQL 서버의 HttpLink와 Subscription이 사용할 WebSocketLink를 분할(split)하는 코드를 작성합니다. (apollo client에서 지원하는 split 함수 이용)
// index.js

import { split, HttpLink } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';

// query, mutation 관련 graphQL 서버(http) Link
const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql'
});

// subscription 관련 웹소켓 Link
const wsLink = new GraphQLWsLink(createClient({
  url: 'ws://localhost:4000/subscriptions',
}));

// split 함수는 3개의 매개변수를 받습니다.
// 1. 각 작업이 실행될 때마다 호출되는 함수
// 2. 함수가 true를 반환할 때 사용할 링크
// 3. 함수가 false를 반환할 때 사용할 링크
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink, // true 값을 반환할 때 사용할 링크
  httpLink, // false 값을 반환할 때 사용할 링크
);
  1. ApolloClient에 위에서 작성한 splitLink 연결하고 Provide로 감싸줍니다.
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client';

// ...위의 httpLink, wsLink, splitLink 코드들

const client = new ApolloClient({
  link: splitLink, // splitLink 전달
  cache: new InMemoryCache()
});

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </React.StrictMode>,
);

Reference.

profile
😸

0개의 댓글