GraphQL의 서버 통신 캐싱은 REST API를 사용할 때와 차이가 있습니다. REST API는 HTTP 캐시를 통해 데이터를 저장하고 관리하고, 클라이언트는 HTTP 헤더를 사용하여 캐시 동작을 제어할 수 있습니다.
이에 반해 GraphQL에서는 단일 엔드 포인트로 통신합니다. 그렇기 때문에 엔드 포인트 단위가 아닌 Client Side에서 쿼리들에 해당하는 캐시를 관리합니다. 그 중에서 Relay의 내용을 기반으로 GraphQL 캐싱에 대해 알아보고자 합니다.
목차
1️⃣ About Relay Cache
2️⃣ Cache Update
3️⃣ Cache Update with Mutation Payload
🔗캐싱이란, 더 느린 기본 스토리지 계층에 액세스해야 하는 필요를 줄임으로써 데이터 검색 성능을 높이는 것입니다. 클라이언트에서 🔗소요시간이 비교적 오래 걸리는 서버 통신을 캐싱을 통해 최소화한다면, 어플리케이션의 성능을 향상시킬 수 있습니다.
Relay에서 캐시는 어떻게 동작할까요?
Relay Environment의 설정(Fetch policy, GC, …)에 따라 Cache data를 담는 Store가 구성됩니다. 해당 스토어에 “A graph of records” 구조로써 Cache data가 저장됩니다. 각 Record는 고유한 ID로 식별되고, 각 쿼리에 해당하는 scalar field, 혹은 또다른 Record의 연결 정보로 구성됩니다.
쿼리를 부르게 되면, store.get('id12345') 와 같이 다이렉트로 해당 캐시 정보를 얻거나 store.get(‘id12345’).getLinkedRecord(‘user’)[1] 와 같이 link 되어 있는 레코드의 정보를 얻어 탐색합니다.
더 나아가서, 한 쿼리에 대해 부분적인 데이터라도 재사용함으로써 빠르게 렌더링 할 수 있습니다.
한 query에 대하여 캐싱이 되어있는 필드들과, 캐싱이 안 되어 있는 fragment를 호출하는 경우를 가정해보겠습니다. 이 때 fragment는 패칭이 될 때까지 suspend 상태를 유지하지만, query의 상태는 이미 캐시 히트가 되어 최대한 빠르게 렌더링이 가능하게끔 동작합니다. 즉, Suspense를 중첩하여 fragemnt와 query를 나누어 조합하고 🔗Partially cached data 에 따라 로딩 상태를 분리할 수 있습니다.
다른 쿼리에 대한 데이터를 가져온 후에도 시간이 지나면 너무 커지고 “Invalid 한 데이터”가 될 수 있으므로 가져온 모든 데이터를 메모리에 무한정 보관할 수는 없습니다. 이를 해결하기 위해 Relay는 기본적으로 가비지 컬렉션(GC)이라는 프로세스를 실행하여 더 이상 사용하지 않는 데이터를 삭제합니다. 또는, fetch-policy에 대한 환경을 설정함으로써 캐시 히트를 시도 않고 서버 통신을 할 수 있습니다.
별도로 클라이언트는 명시적으로 데이터가 더이상 “not fresh” 함을 표시할 수 있습니다.
store.invalidateStore() updater 함수는 전역적으로 캐시 스토어를 무효화 합니다. 혹은 store.get(’id12345’).invalidateRecord() updater 함수로써 특정 캐시를 세분화하여 무효화 할 수 있습니다.
만약 이미 렌더링 된 쿼리라면 어떻게 해야 할까요? 마운트 된 이후기 때문에 캐시가 업데이트 되었더라도 이를 알고 불러왔던 쿼리를 업데이트하여 렌더링하는 것은 또 다른 문제입니다. Relay는 이 문제를 해결하기 위해 캐시 업데이트를 감지하는 🔗useSubscribeToInvalidationState 훅을 제공하였습니다. 문서를 첨부해드리니, 흥미가 있으신 분은 더 자세히 살펴보시면 좋을 것 같습니다.
위와 같이 우리가 직접 하나하나 subscribe까지 하며 invalidate를 하기에는 공수가 많이 들고, 유지보수하기에도 쉽지 않습니다. 이를 위하여 Relay에서는 Mutation Payload를 통해 캐시를 자동으로 업데이트를 해줍니다.
데이터의 not fresh 함은 끊임없이 변화하는 값이 아니라면 주로 사용자의 액션에 의해 야기됩니다. 따라서, mutation의 payload로 캐시를 업데이트 할 수 있다면, 선언적으로 데이터의 생성/업데이트/삭제와 함께 기존 캐시의 업데이트를 수행할 수 있습니다.
FYI, connection query와 같이 업데이트된 node와 별도의 쿼리 패칭은 payload로 조작할 수 없습니다. 이는 별도로 refetch나 cache의 invalid 로직을 명시해주어야 하기도 합니다.
예시를 살펴보겠습니다.
아래와 같이 Schema를 구성합니다. 클라이언트는 Post와 viewer의 특정 필드들을 payload로 선언하여, 해당 부분에 대한 캐시가 자동으로 업데이트 할 수 있습니다.
위와 같은 mutation 선언으로, 업데이트 된 포스팅의 경된 제목과 내용에 대해 캐시 업데이트가 동작합니다. Post 타입의 title, content 이외의 타입에 대해서는 별도의 쿼리 호출 전에, 업데이트가 일어나지 않습니다.
더 나아가, payload로는 복잡한 데이터 업데이트에 대한 니즈가 충족되지 않을 수 있습니다. 이 같은 경우에는 mutation function의 updater 옵션을 사용할 수 있습니다. 해당 옵션에서 RecordProxy(store)를 인자를 받아 명령형으로 캐시를 무효화 하거나, 특정 필드에 대한 값을 조작할 수 있습니다.
마지막으로 Delete에 대한 mutation은, @deleteRecord Directive는 삭제하는 객체에 대한 Record를 선언적으로 삭제할 수 있게끔 해줍니다.
이렇게 Relay의 캐시 스토어는 데이터와 같이 Graph 자료 구조로 구성되어 있고, mutation payload에 따른 reponse 필드를 명시함으로써 캐시 업데이트도 가능하다는 것까지 살펴보았습니다.
다양한 Relay의 활용법이 공유되며 GraphQL이 더 널리 사용성을 인정받기를 소망합니다.
잘못된 부분이 있다면 언제든지 피드백 주시길 바랍니다! 글 읽어주셔서 감사합니다.
Ref