프로젝트를 진행하면서 가장 어려웠고 시간을 많이 할애 했던 부분은 cache
에 관한 작업이었다. 사전에 노마드코더에서 apollo client
관련 강의도 듣고 apollo client
를 사용해서 프로젝트를 진행했던 블로그들을 찾아 글을 읽어보면서 cache
관리만 잘하면 프로젝트를 진행하는데 무리가 없을거라고 생각했다.
하지만 결과적으로 cache
를 제대로 사용하지 못한채 프로젝트는 종료되었고 내가 무었때문에 힘들었고, 어떤 것이 잘 되지 않았나 찾아보기 위해서 InMemoryCache
에 대해 알아보기로 했고 지금까지 알아본 것들을 정리해보려고 한다.
일단 가장 어려웠고 힘든 것이 cache
가 update
가 되면 화면을 다시 그려주는 부분이었는데 내부에서 어떤 움직임이 일어나는지 알아봐야 할 것 같았다.
apollo
공식 문서를 보면 아래와 같이 쓰여져 있다.
apollo client
는gql
의 결과를InMemoryCache
에 저장하며, 이를 통해 클라이언트는 불필요한 네트워크 요청 없이 동일한 데이터에 대해 요청할 수 있다.
공식 문서를 좀 더 들여다 보면 InMemoryCache
는 쿼리 응답 개체를 내부 데이터 저장소에 저장하기 전에 이를 정규화(?) 하며 아래와 같은 단계를 가진다
- 캐시는 응답 받은 데이터에 포함된 모든 식별 가능한 객체에 대해 고유한 ID를 생성
- 캐시는 개체를 ID별로 플랫 룩업 테이블(DB 테이블 같이..?)에 저장한다.
- 들어오는 객체가 기존 객체와 동일한 ID로 저장될 때마다 해당 객체의 필드가 병합된다.
그리고 InMemoryCache
는 기본적으로 __typename
과 id
또는 _id
를 결합시켜서 고유한 식별자를 만든다.(이 기본 식별자를 id
또는 _id
이외의 다른 필드로 정의하고 싶을 경우 설정해 줄 수 있다.)
예시로 __typename
이 task
이며 id
가 14
일 경우에는 task:14
의 식별자가 생성되는 것이다.
In Memory Cache
에 데이터가 어떻게 저장되는지 알아보았다. 그렇다면 query
를 실행 후에 cache
를 업데이트하려면 어떻게 해야할까?
이 경우에는 여러 방법을 사용하여 해결할 수 있다.
mutation
이 실행 된 후 새로고침(F5) 실행mutation
이 실행 된 후 직접 cache
에 접근하여 데이터 변경(writeQuery)fetchPolicy
사용위 방식들을 활용하면 cache
를 업데이트 할 수 있으며 변경된 UI
를 볼 수 있다.
cache
를 업데이트 하는 방법들을 알고 있음에도 불구하고 내가 직접 코드를 작성하면서 겪은 에러와
어려웠던 점들을 알아보자. 아래 코드는 apollo
공식 문서 예시다.
// Query that fetches all existing to-do items
const query = gql`
query MyTodoAppQuery {
todos {
id
text
completed
}
}
`;
// Get the current to-do list
const data = client.readQuery({ query });
// Create a new to-do item
const myNewTodo = {
id: '6',
text: 'Start using Apollo Client.',
completed: false,
__typename: 'Todo',
};
// Write back to the to-do list, appending the new item
client.writeQuery({
query,
data: {
todos: [...data.todos, myNewTodo],
},
});
첫 번째로 cache
에 data
를 추가 또는 제거할 때는 immutable
규칙을 지켜야 한다. 생각보다 이 규칙은 정말 중요한데 cache
는 업데이트가 끝나기 전까지 변하지 않으므로 이 immutable
규칙을 적용해주어야 한다. 또한 immutable
을 지키지 않아 cache
가 업데이트 되지 않는다는 이슈에 대해서는 stackoverflow 에도 매우 많이 올라와 있다.(주로 immer
라이브러리를 사용하거나 Lodash Clone Deep
방식을 사용하는 것 같다.)
두 번째로 위 코드를 예로 들면 data.tods
의 Object 와 myNewTodo
의 Object 가 서로 같은 타입을 가지고 있어야 한다는 것이다. 위의 코드는 간단한 Object 형태로 id
를 가지고 있어 업데이트가 무난히 이루어지지만 nested Object
의 경우 cache
가 업데이트 되는 과정이 굉장히 복잡한데 아직도 이 부분에 대해서 정확히 이해한 바가 없어 더 공부해야 할 필요가 있다.
사실 immutable
로 인해 cache
가 업데이트 되지 않았던 것은 크게 문제가 되지 않았지만 writeQuery
를 실행할 때 마다 filed name
에 대해 undfined
에러가 발생할 때 마다 도저히 감을 잡지 못했었다. 그럴 때 마다 위에서 말한대로 업데이트 하려는 데이터와 writeQuery
안에 있는 data 내부의 Object
를 같게 맞춰줘서 에러를 해결 했지만 nested Object
의 경우는 아직도 해결하지 못했다. 이와 관련해서 몇 가지를 더 찾아보았는데 @Client
의 필드가 누락되었거나 여기서 설명하진 않았지만 id
또는 _id
로 구분되지 않는데 dataIdFromObject
를 정의하지 않아 발생할 수 있는 문제인 것 같았다. 그래도 이 기회로 조금이나마 cache
에 대해 알 수 있었지만 내가 제대로 이해하고 사용하지 못하고 있다는 점에서 매우 찝찝한 상황이다. 앞으로 더 많이 사용할 기회가 생기고 공부하다 보면 이해할 수 있을거라고 생각하며 apollo
를 사용해서 프로젝트를 해본 경험에 가치를 두자.
참고 자료
1. How to update the Apollo Client’s cache after a mutation
2. How to Rock the Party with Apollo GraphQL Cache
3. Apollo Docs
고민의 흔적이 보이는 멋진 글이네요
많이 배워갑니다 :)