Next.js와 Apollo Client 개선기

apollo-client

이 글은 2024-06-09에 작성되었어요.

프롤로그


사내 서비스에서는 GraphQL과 Apollo Client를 사용하고 있습니다.

1년 정도 된 일이지만 사내 서비스에 어느정도 적응한 이후 슬슬 몰랐던 문제점들이 보이더라고요.

그 중 하나가 Apollo Client를 서버 사이드에서 사용할 때 데이터를 불러온 뒤, props를 클라이언트로 넘기는 행동이었어요.

처음 입사하고, 배웠던 내용을 그대로 사용하고만 있다보니 개선점이 있는 코드인지 파악을 못했었는데, 그런 제가 어떻게 알게 되었는지, 어떻게 해결했는지 적어보도록 하겠습니다. 🙂

Props Driling이 불편해


제가 처음 개선이 필요한 코드인지 알게된건 ‘서버에서 요청한 데이터를 props로만 전달해서 하위 컴포넌트까지 다 전달했어야 하는 점이 좀 불편한데…’ 라는 생각 때문이었습니다.

분명 라이브러리라면 여러 예외상황이랑 불편한 점을 개선해서 만들어졌을 텐데, 저만 이런 불편함을 겪진 않을 것 같더라고요.

그래서 다른 방법이 있는지 찾아봤더니 조금 다른 방식으로 사용을 하더라고요.

아폴로 클라이언트는 ‘캐시’를 굉장히 잘 활용하기 때문에, 서버에서 요청한 ‘캐시’를 ‘클라이언트’로 넘길 수 있다면 괜찮을 것 같았습니다.

그리고 잘 정리된 많은 글들 덕분에 생각보다 쉽게 개선할 수 있었어요.

그럼 코드와 함께 볼까요?

코드


let apolloClient = null export function initializeApollo(initialState = null) { const _apolloClient = apolloClient ?? createApolloClient() if (initialState) { const existingCache = _apolloClient.extract() const mergedCache = mergeWith({}, initialState, existingCache, (objValue, srcValue) => { if (isEqual(objValue, srcValue)) return objValue }) _apolloClient.cache.restore(mergedCache) } if (typeof window === 'undefined') return _apolloClient if (!apolloClient) apolloClient = _apolloClient return _apolloClient } export function useApollo(initialState) { const store = useMemo(() => initializeApollo(initialState), [initialState]) return store }

클라이언트에서 아폴로 클라이언트의 client 객체를 얻기 위해서 다들 useApolloClient 훅을 많이들 활용하는데요. 서버에서는 이 훅이 동작하지 않겠죠. 그래서 이를 사용하기 위해선 아폴로 인스턴스를 서버 사이드에서 동작할 수 있게 기능을 만들어야 합니다.

위는 서버와 클라이언트에서 사용하는 아폴로 인스턴스를 각각 따로 생성하기 위한 코드입니다. 그런데 중간에 initialState가 있으면 캐시를 머지한 뒤 재저장하는 로직이 존재하는데요.

이게 바로 서버 사이드에서 요청한 데이터를 클라이언트 사이드에 선언한 아폴로 인스턴스의 캐시에 저장하기 위한 중요한 핵심 로직입니다.

아래는 서버 사이드에서의 페칭 예시입니다. (*Next.js pages router 기반)

export async function getServerSideProps(ctx) { const client = await initializeApollo(null) const { data, error } = await client.query({ query: exampleQuery, }) return { props: { initialState: client.cache.extract() 또는 custom data } } }

initialState에 요청한 데이터를 그대로 전달하지만 이번에는 각 라우팅 페이지의 클라이언트 마다 props로 데이터를 받지 않습니다.

아래와 같이 이 데이터는 app.jsx에서 전달된 props를 이용해 클라이언트 아폴로 인스턴스에 저장합니다.

function MyApp({ Component, pageProps }) { const client = useApollo(pageProps.initialState) }

여전히 서버 사이드에서 props로 데이터를 전달하지만, 이를 클라이언트 인스턴스 캐시에 담기 때문에 필요하다면 언제든 편하게 불러서 사용할 수 있겠네요.

마무리


이 글과 별개로 클라이언트에서 저장된 캐시를 다루는 건 정말 쉽게 친해지지 않는 것 같아요. 캐시를 합치는 과정, 업데이트 하는 과정, 캐시를 얼마나 유지할지, 얼마만에 갱신할지 등 신중한 작업과 고민이 들어가야 하거든요.

혹여나 아쉽거나 문제가 있다면 언제든 피드백 부탁드립니다! (타인의 피드백이 절실합니다 🙏)

읽어 주셔서 감사합니다.