나의 아폴로 cache-first가 캐시 사용을 보장하지 않는 이유

김민국·2023년 6월 11일
0

미세먼지 팁

목록 보기
3/3

개요

CLASS101의 구독 관리 페이지는 유저의 상태에 따라 보여지는 화면이 다릅니다.
ex) 미구독인 사람, 정기 구독중인 사람, 구독 이용권(쿠폰)으로 이용중인 사람...

각각의 유저들의 상황에 따라 메뉴의 노출 여부가 바뀌기 때문에 유저의 구독 상태 정보를 거의 모든 메뉴에서 알고 있어야 합니다.

이 과정에서 한 페이지지만 여러 컴포넌트에서 각각 구독 정보를 부르는 쿼리를 호출하고 있었고 이는 캐시되지 않고 각각 로딩되어 화면이 덜컹덜컹 로딩되고 있었습니다..

월간 정기구독중인 유저 화면

구독 이용권으로 구독중인 유저 화면

유저에 따라 보여지는 화면이 다르다.

문제

기존의 컴포넌트가 각각 부르고 있던 쿼리들. 비슷하지만 조금씩 다른 정보를 요청하고 있다.

Apollo query는 따로 명시하지 않는다면 기본적으로 cache-first fetch-policy를 가집니다. 이는 요청한 데이터가 Apollo cache에 있으면 우선적으로 cache에서 가져오기 때문에 불필요한 네트워크 요청을 줄일 수 있습니다.

그러나 위 예시에서처럼 한 페이지에서 여러 컴포넌트에서 같은 쿼리를 호출하는 경우, 첫 번째 쿼리 응답이 오기 전에 같은 쿼리를 또 요청하게 됩니다.

캐시에 없는 상황에서 이러한 요청을 반복하다 보면, 모든 곳에서 cache-first로 캐시를 이용하기를 바랬지만 어느 곳도 캐시를 사용하고 있지 않고 각각의 컴포넌트가 같은 정보를 받기 위해 여러개의 쿼리를 하게 되는 문제가 생겼습니다.

게다가 CLASS101의 GraphQL은 JAVA 백엔드의 REST API를 wrapping한 것이기 때문에 매번의 네트워크 요청은 필요한 것만 부르는 형태가 아니라 데이터 전부를 받은 뒤에 GraphQL로 정제되어 내리는 것이기 때문에 성능에 더 나쁘게 다가왔습니다.

결과적으로 페이지를 로딩할 때 한번에 로딩되지 않고 각각의 메뉴들이 제각각 로딩되어 화면에 나타나게 되었습니다.

해결

개선된 페이지에서는 더 이상 로딩이 산발적으로 일어나지 않고 한번의 로딩으로 전체 페이지가 로딩됩니다.

문제를 요약하자면 같은 정보를 요구하는데도 불구하고 여러번의 네트워크 요청이 일어나는 것이었습니다.

여러번의 네트워크 요청을 막고 한번의 쿼리로 모든 페이지가 로딩되도록 해야겠습니다.

개선 순서는 다음과 같았습니다.

  1. Section에서 호출하는 query문을 Fragment로 만들어 FragmentDoc을 생성한다.
  2. MyPage에서 생성된 Fragment를 모두 import하여 한번에 쿼리한다.
  3. 하위 Section 컴포넌트에서는 MyPage에서 쿼리한 결과를 prop으로 받아 페이지를 렌더링한다.
// SectionCurrentSubscription.tsx
gql`
	fragment SubscriptionInfoOnSectionCurrentSubscription on SubscriptionInfo {
		체험정보 { ... }
		구독상태
		결제수단 { ... }
	}
`

// SectionNextubscription.tsx
gql`
	fragment SubscriptionInfoOnSectionNextubscription on SubscriptionInfo {
		다음구독상품 { ... }
		다음결제일
		체험정보 { ... }
		구독상태
		결제수단 { ... }
	}
`

// SectionNotice.tsx
gql`
	fragment SubscriptionInfoOnSectionNotice on SubscriptionInfo {
		다음구독상품 { ... }
		다음결제일
		체험정보 { ... }
		구독상태
		결제수단 { ... }
	}
`

위와 같이 각각 Section에서 필요로 하는 Fragment를 미리 선언해 둔 다음,

//MyPage.tsx
gql`
  ${SubscriptionInfoOnSectionCurrentSubscriptionFragmentDoc}
  ${SubscriptionInfoOnSectionNextubscriptionFragmentDoc}
  ${SubscriptionInfoOnSectionNoticeFragmentDoc}
  query SubscriptionDetailInfoOnPlusSubscriptionContent($planToken: String!) {
    subscription: getSubscriptionDetailInfo(planToken: $planToken) {
      _id
      ...SubscriptionInfoOnSectionCurrentSubscriptionFragmentDoc
      ...SubscriptionInfoOnSectionNextubscriptionFragmentDoc
      ...SubscriptionInfoOnSectionNoticeFragmentDoc
      구독 상태 { ... }
    }
  }
`

위와 같이 페이지에서 한번에 Fragment를 모아 쿼리합니다.

이제 하위 컴포넌트에서는 MyPage에서 쿼리한 subscriptionData를 받기만 하면 됩니다.

//MyPage.tsx

export const MyPage = () => {
	// query를 요청해 subscriptionData를 불러옵니다.
	const { subscriptionData } = useSubscriptionDetailInfoOnPlusSubscriptionContentQuery()
	
	return (
		<div>
			<SectionCurrentSubscription subscriptionData={subscriptionData} />
			<SectionNextubscription subscriptionData={subscriptionData} />
			<SectionNotice subscriptionData={subscriptionData} />
		</div>
	)
}

결과적으로는 쿼리 데이터를 props로 변경해준 작업이었습니다.

기존에는 cache-first를 쓰면 무조건 캐시를 쓸거라 생각하고 작업을 했었는데 이부분 작업을 하면서 그렇지 않다는 것을 알게 되었습니다.

요약:

  • cache-first라고 항상 쿼리 요청을 안하는게 아니다. 당연히 cache에 데이터가 없다면 네트워크 요청을 한다.
  • 페이지를 한번의 로딩으로 렌더하고자 한다면 페이지 단에서 쿼리한 결과를 prop으로 내려주면 한번의 쿼리로 해결할 수 있다.
  • 다만 하위 컴포넌트에서 쓰일 필드는 하위 컴포넌트에서만 알 수 있기 때문에 이를 Fragment로 만들고 Page에서는 이를 import하여 사용한다.

0개의 댓글