GraphQL Client 라이브러리는 GraphQL을 통해 원격 및 로컬 데이터 관리를 단순화하는 상태 관리 라이브러리입니다. 데이터의 캐싱 처리 및 편리한 GraphQL Query 작성, UI 자동 업데이트 등을 통해 효율적인 코드 작성에 도움을 줍니다.
GraphQL 클라이언트 라이브러리에는 대표적으로 Apollo Client 외에도 Relay, URQL 등이 있습니다. 이 외에도 https://graphql.org/code 에서 언어나 프레임워크, 클라이언트, 서버마다 사용할 수 있는 GraphQL 라이브러리를 찾아볼 수 있습니다.
Relay
: Facebook이 개발, 2015년 출시. 퍼포먼스 최적화 강점으로 네트워크 트래픽 감소. 높은 러닝커브, 상대적으로 대규모 애플리케이션에 적합합니다.URQL(Universal React Query Library)
: 2018년 출시, GraphQL 클라이언트의 단순화, 경량화에 중점을 두었습니다.Apollo Client
: 2016년 출시. 잘 갖춰진 공식 문서와 자료, 거의 모든 프론트엔드 프레임워크 지원합니다.작은 규모의 웹 애플리케이션에 테스트 할겸, 자료도 많고 널리 사용되는 Apollo Client를 선택하게 되었습니다. 또한 Apollo는 서버와 클라이언트 모두 제공합니다.
Apollo Client의 InMemoryCache
는 캐시 및 상태 관리를 담당하는 핵심 기능으로 클라이언트에서 받은 GraphQL 데이터를 메모리에 저장하고 쿼리에 따라 캐시된 데이터를 효율적으로 관리합니다.
Apollo Client는 GraphQL 쿼리 결과를 정규화된 로컬 인메모리 캐시에 저장합니다. 이를 통해 이미 캐시된 데이터 즉, 동일한 요청에 대해서는 새로운 네트워크 요청을 보내지 않고 거의 즉시 응답할 수 있습니다.
위에서 말하는 정규화(Normalization)란? 데이터를 정규화하여 저장한다는 것인데, 이는 중첩된 데이터 구조를 펼쳐서 저장하고 관리하는 방식으로, 쿼리에 필요한 데이터를 효율적으로 찾을 수 있게 해줍니다.
Countries GraphQL API라는 Public API를 이용하여 GraphQL 서버를 따로 만들지 않고, 테스트 할 수 있습니다.
GraphQL Playground란?
GraphQL Playground는 GraphQL API를 GUI로 보여주어 시각적으로 탐색할 수 있게 하는 GraphQL IDE입니다. 따라서, GraphQL 서버에 쿼리를 테스트해보거나 디버깅할 수 있습니다. GUI로 쉽게 Database의 구조를 파악하고, 실제로 데이터를 출력해 볼 수 있어 편리합니다.
$ npm install @apollo/client graphql
@apollo/client
: 아폴로 클라이언트를 설정하는 데 필요한 패키지graphql
: GraphQL 쿼리를 구문 분석하는 로직을 제공하는 패키지// 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
}
}
}
`;
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
: 데이터 실시간 구독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는 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;
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;
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;
Subscription은 Query처럼 데이터 조회를 위해 사용되지만, 주로 실시간(real-time) 애플리케이션 구현을 위해 사용됩니다. 서버에서 이벤트가 발생할 때마다 클라이언트에게 데이터를 전송합니다.
GraphQL 스키마에서 사용 가능한 구독을 Subscription 유형의 필드로 정의해야 합니다. 마찬가지로 클라이언트 단에서도 query와 같이 subscription 구문을 작성해야 합니다.
GraphQL 서버를 구현할 때, Apollo Server와 같은 웹소켓을 지원하는 라이브러리를 사용하여 GraphQL Subscription을 처리합니다.
아래는 Subscription을 사용하기 위한 순서/과정들을 간략하게 정리한 것입니다.
$ npm install graphql-ws
// index.js
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
const wsLink = new GraphQLWsLink(createClient({
url: 'ws://localhost:4000/subscriptions',
}));
// 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 값을 반환할 때 사용할 링크
);
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.