글 작성자: 택시 운전사
반응형

Apollo Client

@apollo/client 라이브러리를 이용합니다. Apollo는 GraphQL 서버에서 필요한 정확한 데이터를 쉽게 query할 수 있는 GraplQL 클라이언트입니다. 데이터를 가져오고 변조하는 것 이외에도, Apollo는 query와 그 결과를 분석하여 클라이언트 사이드 캐시를 구성하며 이를 통해 추가 query와 mutation이 실행될 때 최신 상태로 유지되게 합니다.

Next.js에서 Apollo Client 사용하기

SSR을 할 수 있는 Next.js 프레임워크에서 Apollo Client를 사용하는 방법이 여러가지 있다. next-with-apollo라는 라이브러리가 유명하지만 아쉽게도 최신 스펙인 getServerSidePropsgetStaticProps를 정식으로 지원하고 있지 않다. 따라서 이번 글에서는 라이브러리 없이 구현한 예제들을 소개하고자한다.

/lib/apolloClient.ts

우리가 사용할 Apollo Client 객체를 생성하는 파일이다. Next.js에서는 SSR 과정과 CSR 과정이 나누어져있기 때문에 이 부분까지 고려해서 짜야한다.

import { useMemo } from 'react'
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'
import merge from 'deepmerge'

let apolloClient: ApolloClient<NormalizedCacheObject> = null

function createApolloClient() {
  return new ApolloClient({
    ssrMode: typeof window === 'undefined',
    link: new HttpLink({
      uri: 'https://nextjs-graphql-with-prisma-simple.vercel.app/api', // 서버 URL (상대 주소가 아닌 절대 주소를 써야한다.)
      credentials: 'same-origin', // `credentials`나 `headers`같은 추가적 fetch() 옵션
    }),
    cache: new InMemoryCache(),
  })
}

export function initializeApollo(initialState = null) {
  const _apolloClient = apolloClient ?? createApolloClient()

  // Next.js에서 Apollo Client를 이용해 데이터를 가져오는 함수가 있다면, 초기 상태값이 여기에서 합쳐진다.
  if (initialState) {
    // 클라이언트에서의 받은 데이터인 현재 캐시 데이터를 가져온다.
    const existingCache = _apolloClient.extract()

    // 현재 캐시와 SSR 메소드인 getStaticProps/getServerSideProps로 부터 받은 데이터를 합친다.
    const data = merge(initialState, existingCache)

    // 합쳐진 데이터를 저장한다.
    _apolloClient.cache.restore(data)
  }
  // SSG(Server Side Generation)와 SSR(Server Side Rendering)은 항상 새로운 Apollo Client를 생성한다.
  if (typeof window === 'undefined') return _apolloClient
  // 클라이언트의 Apollo Client는 한 번만 생성한다.
  if (!apolloClient) apolloClient = _apolloClient

  return _apolloClient
}

export function useApollo(initialState) {
  const store = useMemo(() => initializeApollo(initialState), [initialState])
  return store
}

/pages/_app.js

우리가 Apollo Client를 사용하고자 하는 최상위 페이지(/page/~)에 있는 SSR 메소드(getStaticProps/getServerSideProps) 안의 내용을 넣어줍니다. 해당 Page 컴포넌트가 로드될 때, 서버의 SSR 메소드에서 본인 혹은 자식 컴포넌트들에서 필요한 qurey를 가져옵니다. qurey의 값을 받았을 때, Apollo Client의 store는 완전히 초기화됩니다. 결과적으로 브라우져에서 가져온 데이터hydrate Apollo를 가진 초기 HTML를 제공할 수 있게 됩니다.

import { ApolloProvider } from '@apollo/client'
import { useApollo } from '../lib/apolloClient'

export default function App({ Component, pageProps }) {
  const apolloClient = useApollo(pageProps.initialApolloState)

  return (
    <ApolloProvider client={apolloClient}>
      <Component {...pageProps} />
    </ApolloProvider>
  )
}

/pages/index.tsx

페이지 컴포넌트의 예시로 컴포넌트 내의 Query 메소드와 SSR 메소드를 주의깊게 확인해야합니다.

import {
  ALL_POSTS_QUERY,
} from '@/~'
import {
  allPostsQueryVars,
} from '@/~'
import { initializeApollo } from '../lib/apolloClient'

const Home = () => (
   ...
  // SSR에서 사용한 query와 같은 query(ALL_POSTS_QUERY)를 사용
  const { loading, error, data } = useQuery(ALL_POSTS_QUERY, {
    variables: allPostsQueryVars,
  })

)

export const getServerSideProps: GetServerSideProps<{}, {}> = async (ctx) => {
  /*
    1. 투표 등 실시간성 데이터가 계속 업데이트 되어야하는 경우 getStaticProps와 revalidate를 이용
    2. 일반적인 경우 getServerSideProps를 이용
  */

  // browser단의 context(headers)를 SSR에 넘기는 과정
  const apolloClient = initializeApollo(null, ctx)

  await apolloClient.query({
    query: ALL_POSTS_QUERY,
    variables: allPostsQueryVars,
  })

  return {
    props: {
      initialApolloState: apolloClient.cache.extract(),
    },
  }
}

export default Home
반응형