2021. 2. 26. 16:49ㆍ카테고리 없음
참고자료: 공식문서 및 아래 영상
www.youtube.com/watch?v=ou0fEW1eRjc&t=7081s
1. Apollo Client의 캐시와 Nomalization
아폴로 클라이언트를 상태관리에 사용하면 캐싱 처리에 대한 지원을 해준다.
graphQL은 기본적으로 엔드포인트가 1개이기때문에 캐시 처리가 까다롭지만,
아폴로 클라이언트는 이러한 데이터의 redundancy들을 고려하여 예쁜 캐싱처리를 해준다.
캐싱된 데이터의 상세 내용은 아폴로 dev tool에서 확인할 수 있다.
graphQL은 nested한 데이터를 가져오지만, 아폴로 클라이언트가 관리하는 캐시의 형태는 언제나 flat하다.
아폴로 클라이언트의 캐시를 사용하면, 서버에서 받아온 데이터를 flat한 형태로 바꾸어주는 normalization 작업이 수반되기 때문이다.
이 과정에서 각 Entity에 유니크한 Identifier를 assign 한다.
특정 Entity에 접근하고 싶다면, 아폴로 클라이언트의 캐시 관련 API를 통해 직접 Entity를 선택하면 된다.
예컨데 이렇게 복잡한 형태의 스키마도
모두 아폴로 클라이언트의 normalization에 의한 flat한 형태로 캐시에 저장된다.
2. Apollo client의 fetch policy
아폴로가 캐시를 사용하는 방법은 아폴로 클라이언트의 객체를 생성할때 fetch policy를 통해 결정되며
아폴로 캐시는 Inmemory cache를 통해 만들어낼 수 있다.
fetchPolicy에 대한 설명은 이전 포스팅에 있으니 생략.
무튼 대부분은 캐싱 처리는 Policy만 적절하게 지정해주면 된다.
이 설정은 캐시를 사용하는 시점, 빈도 등을 결정하게 된다.
아폴로 클라이언트는 타입이름 그대로 캐시에 데이터를 저장하기 때문에, 서버로부터 새롭게 데이터를 받으면 자동으로 이를 merge 해준다.
따라서 데이터를 새로 fetch하는 경우에는 policy 말고 별다른 설정이 필요하지 않다.
아폴로 클라이언트가 현재 가지고 있는 캐시와 새로 가져온 데이터를 적절히 비교해서 알맞게 캐시 업데이트를 해줄 것 이기 때문이다.
3. 아폴로 클라이언트의 캐시 로직이 필요한 상황과 그 예시
하지만 자주 맞딱드림에도 불구하고 policy로 커버하지 못하는 상황들이 있다.
대표적으로 데이터에 대한 CUD, 즉 Mutation이 수행되는 경우이다.
이 경우에는 Mutation 수행 후 바뀐 graphQL 스키마를 통째로 다시 fetch해오는 방법이 보편적이다.
따라서 useMutation내의 refetchQueries를 이용해 데이터를 통째로 업데이트 하게 된다.
그러나 데이터가 많은 사람들에 의해 바뀌지 않는다는 가정하에, 한개의 Entity가 변했는데 모든 데이터를 업데이트 하는 것은 효율적이지 못하다.
(설령 많은 사람들에 의해 바뀌는 데이터라고 할지라도, 이는 적절한 subscription이나 polling을 통해 풀어나가는 것이 적절할듯)
따라서 이러한 상황에는 CUD가 적용된 특정 Document만 다시 서버에서 가져와 캐시에 반영해주고, 이를 화면에 반영하는 것이 효율적일 것이다.
예시에서는 다음 쿼리를 사용한다.
해당 쿼리를 통해 Activity의 List를 가져온다.
가져온 데이터들은 화면 테이블에 display된다.
이때 내가 '작업완료'버튼을 통해, 첫번째 Row의 Entity에 Mutation을 날렸다.
Mutation은 특정 activity를 찾아 그 status를 변경한다.
하지만 문제는 변경된 status는 하나의 Entity뿐인데, 변경내역을 화면에 반영하기 위해서는 모든 데이터를 다시 refetch 해야된다는 것이다.
이러한 상황에서 useMutation에 refectchQueries를 따로 설정하지 않고, update된 데이터만 읽어와 캐시에 반영한 후, 캐시를 이용해 데이터를 업데이트 하는 것이다.
useMutation에 update 커맨드를 이용하면 캐시에 대한 로직을 적용할 수 있다.
이 과정에서는 cache객체와 data 객체가 사용된다.
data객체는 Mutation을 통해 받아온 결과 데이터를 가리킨다. 단, API에서 업데이트 된 데이터를 리턴하도록 로직을 짜야 한다.
cache 객체는 readQuery 메소드를 통해 캐시에 있는 데이터를 읽어낼 수 있다.
아폴로 클라이언트는 캐시에 graphQL과 같은 타입이름으로 데이터를 저장하기 때문에, 서버에 보냈던 변수들을 똑같이 보내주도록한다.
캐시의 데이터는 graphQL이 리턴한 데이터처럼 readOnly로 고정되어 있다.
따라서 immer를 통해 객체에 대한 깊은 복사를 수행하였다.
이때 data에 담겨진 _id와 동일한 캐시내의 Entity를 찾고, 해당 Entity의 statusId를 Mutation으로 가져온 statusId로 업데이트해주었다.
이후 writeQuery 메소드를 통해 동일한 형태의 쿼리를 날려주되, 데이터는 우리가 변경한 데이터로 바꾸어 날려준다.
이 과정까지 수행하면 캐시는 Mutation을 통해 업데이트 된 사항만 변경하고 바뀌지 않은 나머지 데이터는 고스란히 가지고 있게 된다.
따라서 화면의 테이블 데이터들은 바뀐 부분만 변경되고 나머지 Entity는 캐시와 동일하게 display된다.
create나 delete의 경우도, 해당하는 Document를 받아와 동일한 방법으로 캐시에 업데이트해주면 된다.
4. InMemoryCache 설정이 별도로 필요한 경우(Field 설정)
하지만 3의 케이스로 커버하지 못하는 여러가지 상황이 존재한다.
이 경우에는 cache객체를 생성하는 InMemoryCache에 대한 로직 처리가 필요하다.
요기 포스팅에서는 세가지 경우만 생각해보았다.
InMemoryCache에선 타입에 따라 read와 merge를 통해 캐시 처리를 고도화 할 수 있다.
아래 예시들은 merge는 제외하고 read를 사용하는 케이스만 다뤄보겠다.
(왜냐하면 merge는 내가 아직 제대로 적용해 본 적이 없다...)
1) 캐시에 새로운 타입을 설정하여 읽어오고 싶은 경우
상기 예시와 동일한 쿼리에서, 캐시가 name과 statusId를 합쳐 nickName이라는 이름으로 보내줬으면 좋겠다.
이 경우 gql에 클라이언트에서만 사용하는 타입이라는 플래그를 달아 새로운 타입을 정의한다.
서버에서 받아온 데이터들은 모두 WBS라는 타입으로 캐시에 저장되어 있다.
따라서 typePolicies를 만들고, WBS타입에 대해 객체를 만들어 field에 내가 원하는 nickName이라는 객체를 선언한다.
이 과정에서 read 함수를 사용한다.
첫번째 args로 받아오는 인자는 현재 존재하는 WBS타입의 캐시 데이터들이고, 두번째인자는 상세 로직에 사용될 메소드들이다.
read함수는 캐시에 저장된 데이터들을 가공한다.
read함수에서 특정값을 리턴하게 되면 nickName이라는 객체는 리턴값으로 표기된다.
이는 캐시에 직접적으로 저장되지는 않는다.
단, readQuery에서 캐시 데이터를 읽어오면, 클라이언트에서만 사용되는 추가적인 필드가 생긴다.
해당 필드에는 read함수에 정의된 로직을 통해 리턴된 결과값들이 표기된다.
이를 응용하여 많은 일들을 할 수 있다.
데이터들을 가져와 1차적인 전처리도 가능하고, 여러가지 플래그 삽입도 가능하다
2) Query는 다르지만 캐싱되어있는 데이터를 사용하고 싶은 경우
서로 다른 Query임에도 불구하고, 가져오는 데이터가 중복되는 경우가 있다.
이러한 경우에는 먼저 불려진 Query가 가져온 데이터들이 캐시에 있으므로, 굳이 서버에 한 번 더 갈 필요 없이 캐시데이터를 사용하는 것이 효율적일 것이다.
이 경우엔 toReference 메소드를 활용해, 원하는 데이터의 id를 맞춰주면 된다.
위의 예시에서는 이미 캐시데이터에 존재하는 Zone 타입데이터들에 대해, args의 id로 캐시 내에서 찾아내는 예시이다.
이것 역시 실제 캐시데이터에 저장되진 않으므로, readQuery 메소드를 통해 사용한다.