apollo client cache 딥다이브

비고
Tags
deep dive
Select
recommand
 
 
참고문서

기본 동작 원리

notion image
첫번째 호출 시에는 캐시에 데이터가 없다. 따라서 GraphQL Server로 실제 쿼리를 요청해서 캐시 데이터를 채운 후, 쿼리를 호출한 곳으로 결과 값을 반환한다.
 
notion image
두번째 호출 부터는 캐시에 데이터가 이미 있는 상태이므로, 쿼리를 요청하면 실제 서버로 요청하지 않고 캐시 데이터에 있는 내용을 그대로 반환한다.
불필요한 API 호출을 줄이고, 속도 관점에서의 이점도 가져올 수 있다. (데이터 처리 속도, 렌더링 속도 등)

정규화

링크 같이 보기
 
💡
reference로 관리된다는 점을 상기 해야함. reference로 관리되기에 객체 하나만 변경되면 다 같이 변경된 최신 데이터 상태를 유지할 수 있고, 데이터 공간등 최적화도 가능하다. 또 reference로 관리되므로, 캐시 데이터를 get 한 것에서 직접 수정하면 안된다.

캐시 데이터 관리

__typenameid 값이 중요하다.

한번도 쿼리가 호출되지 않은 경우, 즉 캐시가 아예 비워져 있는 경우에는 캐시데이터에 write 할 수 없다. (검증 다시 해보기)

동일한 쿼리도 variables별로 따로 캐시 관리!

client.writeQuery({ query: GetLatestFirmwareDocument, data: { latestFirmware: { __typename: 'FirmwareType', deviceLineUp: { __typename: 'DeviceLineUpType', modelId: 'Nutrition Engine Pro 1-B', }, file: 'https://application.algocare.link/Firmware/DEV/NE_PRO1_REV.B_APP_V1.4.1.bin', fileSize: 27870, id: '71', majorVersion: 1, minorVersion: 6, patchVersion: 1, }, }, broadcast: false, variables: { tabletImei: '1' }, }); client.writeQuery({ query: GetLatestFirmwareDocument, data: { latestFirmware: { __typename: 'FirmwareType', deviceLineUp: { __typename: 'DeviceLineUpType', modelId: 'Nutrition Engine Pro 1-B', }, file: 'https://application.algocare.link/Firmware/DEV/NE_PRO1_REV.B_APP_V1.4.1.bin', fileSize: 27870, id: '72', majorVersion: 1, minorVersion: 7, patchVersion: 1, }, }, broadcast: false, variables: { tabletImei: '2' }, });
💡
동일한 쿼리에 대해서도 여러개의 값들이 각각 캐시되어 관리된다.
const cacheData = client.readQuery({ query: GetLatestFirmwareDocument, variables: { tabletImei: '1' }, }); console.log('cacheData', cacheData); const cacheData2 = client.readQuery({ query: GetLatestFirmwareDocument, variables: { tabletImei: '2' }, }); console.log('cacheData2', cacheData2);
notion image
variables에 맞게 writeQuery 했던 값으로 잘 조회되는 모습이다.

variables가 다른데 덮어쓰이는 경우

이번에는 writeQuery 시 dataid 값을 동일하게 설정해서 테스트를 진행한다.
client.writeQuery({ query: GetLatestFirmwareDocument, data: { latestFirmware: { __typename: 'FirmwareType', deviceLineUp: { __typename: 'DeviceLineUpType', modelId: 'Nutrition Engine Pro 1-B', }, file: 'https://application.algocare.link/Firmware/DEV/NE_PRO1_REV.B_APP_V1.4.1.bin', fileSize: 27870, id: '72', majorVersion: 1, minorVersion: 6, patchVersion: 1, }, }, broadcast: false, variables: { tabletImei: '1' }, }); client.writeQuery({ query: GetLatestFirmwareDocument, data: { latestFirmware: { __typename: 'FirmwareType', deviceLineUp: { __typename: 'DeviceLineUpType', modelId: 'Nutrition Engine Pro 1-B', }, file: 'https://application.algocare.link/Firmware/DEV/NE_PRO1_REV.B_APP_V1.4.1.bin', fileSize: 27870, id: '72', majorVersion: 1, minorVersion: 7, patchVersion: 1, }, }, broadcast: false, variables: { tabletImei: '2' }, });
💡
readQuery로 variables: {tabletImei: ‘1’}로 호출하면 minorVersion이 몇으로 나올까? 6으로 나올 거라고 생각할 수 있으나, 실제로는 7로 조회된다. 왜냐하면 id72로 동일하기 때문에 variables: {tabletImei: ‘2’}로 writeQuery한 결과로 덮어쓰여진 상태이기 때문이다.
const cacheData = client.readQuery({ query: GetLatestFirmwareDocument, variables: { tabletImei: '1' }, }); console.log('cacheData', cacheData); const cacheData2 = client.readQuery({ query: GetLatestFirmwareDocument, variables: { tabletImei: '2' }, }); console.log('cacheData2', cacheData2);
notion image
 
💡
typenameid가 같으니까 동일한 cacheId 라서 동일한 reference를 참조하게 된다.
 

apollo client cache와 상호작용하기 (읽기, 쓰기)

 

Query 관련 variables

variables에 따라서도 캐시가 관리된다.
 

Fragment 관련 고려사항

cacheId에 fragment 상위의 객체가 이미 존재해야 한다. 거기서 fragment에 명시해둔 필드들만 추출해서 반환되는 것이기 때문이다.
 

cache.modify로 직접 필드를 수정하는 경우

writeQuery, writeFragment 처럼 modify로 캐시 필드를 수정하는 경우에도, 해당 필드를 사용하고 있는 모든 쿼리들에 전파되어 refresh가 이루어진다.
마찬가지로 전파를 원치 않는 경우에는 broadcast: false로 지정해주면 캐시 내용만 업데이트하고 전파는 되지 않는다.
 

cache와 broadcast 전파

소스코드

inMemoryCache.js 파일의 watch 멤버변수와 broadcastWatch 멤버함수 쪽을 보면 된다.

query

  • cache 데이터의 변화에 따른 broadcast를 별다른 조치 없이 바로 수신할 수 있다.
    • (선언과 동시에 호출이 진행되므로)

lazyQuery

  • 쿼리를 한번 호출한 이후여야, 이후 cache 데이터의 변화에 따른 broadcast를 수신할 수 있다.

refetch

  • refetch를 한 경우에도 cache를 업데이트 하므로, 전파는 이루어진다.

lazyQuery 함수의 await 한 결과물을 쓰는 경우에는 broadcast 해당되지 않음

  • 함수는 명시적으로 호출하고 싶은 순간에 호출해서 결과값을 받아오는 것이므로, 전파 대상이 아님.

standby는? (뭐지?)

clearStore와 resetStore 사용 시 broadcast 차이

notion image
  • clearStore는 캐시 삭제가 전파가 되지 않음.
    • 전파되어 캐시를 사용하는 모든 query들의 dataundefined로 찍힐 거라 생각했는데, 그렇지 않고 기존의 내용을 그대로 들고 있다.
    • cache에선 데이터가 없어진 상태이지만, watch가 다시 실행되지 않으므로 마지막 상태에서 그냥 멈춰있는 상태라고 보면 됨.
  • resetStore는 캐시 전체를 비운 후 다시 refetch까지 수행함
    • refetch를 하기 때문에 undefined가 뜨는 것부터, 호출 후 실제 데이터가 차는 것까지 수행된다.
    • 💡
      cache를 사용하고 있는 곳에 한정해서 refetch가 수행된다. - network-only, no-cache를 쓰고 있는 쿼리는 refetch 대상에서 제외된다.
      notion image

broadcast: boolean

Allow silencing broadcast for cache update methods.
notion image
broadcast: false로 설정하여 캐시를 업데이트 할 경우, 해당 캐시를 사용하고 있는 쿼리들에도 전파가 이루어지지 않고, 따라서 로직도 다시 동작하지 않는다.

이슈와 대안

겪은 이슈

  • 새로운 쿼리를 반영했고, 갱신을 위한 작업을 했는데 그대로에요. 안바뀌어요
  • cache-only인데 캐시가 다 날아갔어요

대안

  • default fetchPolicy를 network-only로 변경한다.
  • 캐시를 사용하겠다고 마음먹은 부분에 한해 명시적으로 cache를 활용한다.
    • cache-only를 사용하지 않으며, 캐시 사용을 의도한 부분도 cache-first로 정책을 사용한다.
  • cache.clear는 탈퇴,로그아웃 외에는 사용하지 않는다.
💡
위 링크처럼 cache-and-network가 아닌 network-only로 default 옵션을 설정할 것이다.
 
 
추가로 읽을만한 것
  • Configuration options
    • errorPolicy ‘all’을 하면 error 객체로 에러가 오지 않고, data에 성공과 실패 모두 담아서 준다. 에러가 발생한 경우지만 onError 콜백도 호출되지 않음
    • errorPolicy
    • notion image
  • Optimistic mutation results ← 읽어야함
    • optimistic UI란 먼저 서버로 요청을 보내고 응답될 것으로 예상되는 임시데이터를 사용하여 미리 UI를 업데이트 합니다. 그 후, 서버로부터 실제결과가 응답되면 다시 한번 실제 데이터로 UI를 업데이트 하는 것을 말합니다.