본문 바로가기

React.js

Apollo Client Cache로 전역 상태 관리하기

사내 솔루션으로 UI CMS를 개발하면서, 관리해야 할 전역 상태가 많아지고 컴포넌트 간 의존성이 복잡해졌습니다. 더 복잡해지기 전에 코드를 정리하고자, 과도하게 사용되는 상태를 찾아 정리했습니다.

그중의 하나가 서버의 응답 데이터를 Recoil을 활용해 전역 상태로 사용하는 경우였습니다. “Source of Truth” 개념으로 생각했을 때, 이미 그 역할을 하는 Apollo Client Cache를 사용하는 게 상태 관리 측면에서 더 직관적이라 생각했습니다.

 

 

오픈소스 분석


캐시 동작을 분석하기 위해, Apollo Client 오픈소스 코드를 분석했습니다.

Apollo Client 통신 흐름

Fetch

Apollo Client는 데이터 요청 시, fetch 정책에 따라 캐시 데이터를 사용할지 말지, 또는 응답 데이터를 캐싱할지 말지 달라집니다. 캐시를 사용하는 정책은 응답 결과를 자동으로 캐싱합니다.

예를 들어 “cache-first” 정책으로 데이터를 요청하는 경우, 우선 캐시에 데이터가 있는지 확인합니다. 있다면 캐시를 반환하고, 없다면 서버로 요청합니다. 응답 결과가 오면 데이터를 캐싱하고 클라이언트로 반환됩니다.

소스 코드를 살펴보면, 응답 결과가 오면 inMemoryCache의 write 함수를 호출하고, GraphQL의 쿼리 형태로 새로운 데이터가 캐싱 됩니다. 데이터가 저장되면, Apollo Client는 다음 데이터 요청마다 캐싱된 데이터를 반환합니다.

 

캐시 데이터 직접 수정

Mutation의 경우, Fetch와 달리 데이터를 직접 캐싱해야 합니다.

캐시를 업데이트하면 Apollo Client는 inMemoryCache의 modify 메서드를 호출하고, 수정된 데이터를 파라미터로 전달합니다. 계속해서 entityStore의 ID로 데이터 관련 store에 변경 필드가 있는지 찾습니다. 모두 찾으면 수정 대상에 변경 내용을 적용하고, 캐시를 업데이트합니다.

 

코드 수정 방향

Apollo Client 통신 흐름

 

Apollo Client는 위 그림과 같이 Fetch, Mutation 과정에서 캐시를 활용합니다. 이를 전역 상태 관리에 쓰기 위해서, 빨간색 화살표처럼 관계를 이어주면 됩니다. 이미 정의가 되어 있으니, 캐시를 가져다 쓰고 수정만 하면 되는 간단한 작업입니다.

 

 

 

 

구현


Fetch

const { loading, data, error } = useCmsReportQuery({
  variables: { reportId },
});

fetch 정책에 따라 Apollo Client가 응답 데이터를 자동으로 캐싱합니다. 이미 useQuery를 통해 데이터를 요청했으므로, fetch 정책 설정 외에 요청 시 필요한 코드는 없습니다.

전역 상태 관리 코드는 기존 Recoil 상태 관리 코드를 제거하고, 그 역할을 대체할 캐시 호출 코드가 추가합니다.

 

Mutation

const response = await mutate({
  variables: { input: values },
});
const { report } = response?.data?.cmsCreateOrUpdateReport || {};
if (report?.name) {
  setReport(report);
  Modal.info({
    centered: true,
    title: '확인',
    content: `리포트 [${report.name}]를 ${
      data?.id ? '수정' : '생성'
    }했습니다.`,
  });
}

기존에는 수정된 데이터를 다른 컴포넌트에서 사용하기 위해, setReport와 같은 Recoil setter 함수를 호출했습니다.

await mutate({
  variables: { input: values as TReportInput },
  update(cache, result) {
    if (result?.data?.cmsCreateOrUpdateReport?.report) {
      const { report } = result?.data?.cmsCreateOrUpdateReport;
      cache.modify({
        fields: {
          cmsReportList(
	          list: TReportInfoFragment[] = [], 
		        { readField }
		      ) {
            const newReportRef = cache.writeFragment({
              data: report,
              fragment: ReportInfoFragmentDoc,
            });
            if (list.some((ref) => 
	            readField('id', ref) === report.id)
	          ) {
              return list;
            }
            return [...list, newReportRef];
          },
        },
      });
      if (!values.id) {
        history.push(`/cms/report/${report.id as string}`);
      } else {
        Modal.info({
          centered: true,
          title: '확인',
          content: `리포트 [${report.name}]를 수정 했습니다.`,
        });
      }
    }
  },
});

mutation 내용을 캐시에 반영하기 위해, 우선 setReport 함수와 Recoil 상태 관리 코드를 모두 제거합니다. 그리고 mutation이 잘 작동하면, cache.modify 함수를 호출해서 변경 내용을 캐시에 반영합니다. 이렇게 수정된 캐시 데이터는 전역 상태처럼 사용할 수 있습니다.

 

 

 

마지막으로


캐시를 사용하면서, 동일한 데이터를 반복해서 요청하는 것을 방지했습니다. 이를 통해 네트워크 요청 횟수를 줄이고 응답 시간이 단축되었습니다. 그리고 사용자 작업 시 데이터 로딩 시간도 줄어들면서 사용자 경험이 향상되는 효과를 볼 수 있었습니다.