Apollo Client Cache

hwakyungChoi·2021년 9월 18일
2

Caching in Apollo Client

  • GraphQL 쿼리를 아래와 같은 곳에서 저장이 가능함
  1. 로컬
  2. 정규화
  3. in-memory cache
    위의 작업 과정을 통해 별도 network 요청을 보내지 않고. 캐시된 데이터에 대해 즉시 응답이 가능
  • Apollo Client는 높은 구성 관리를 가지며 스키마에 각각 타입과 필드에 대해 동작에 대해 커스터 마이징이 가능함

  • 더불어 서버로부터 fetch하지 않은 로컬 데이터와 함께 저장하고 상호작용을 할 수 있 음

어떻게 데이터 저장할까?

아폴로 클라이언트의 InMemoryCache는 서로 참조할 수 있는 객체의 flat look up 테이블을 유지함

- look UP 테이블이란 key-value 구조로 저장하는 형태를 가진 테이블을 뜻함
  • 이러한 객체는 GraphQL 쿼리에 의해 반환되는 객체로부터 필드 정보 저장

동일한 객체의 다른 필드를 쿼리가 fetch 한 경우 캐시된 단일 객체는 여러 쿼리에서 반환된 필드가 포함될 수 있음.

캐시는 무계층 → 쿼리로 반환되는 경우, 그렇지 ✗

  • In Memory Cache 는 계층 데이터를 어떻게 flat lookups 저장할까?
    -> 이는 데이터를 저장 전에 캐시를 정규화시켜야 함.

데이터 정규화

  • apollo client 가 쿼리의 response 데이터를 받아올 때 아래와 같이 과정을 진행함
  1. 객체 식별
    캐시는 쿼리 응답에 포함된 고유 객체를 식별함.
  2. 캐시 ID 생성
    캐시는 식별된 각 객체에 대한 캐시 ID 생성
    캐시 ID는 In Memory Cache 에 있는 특정 객체를 고유하게 식별

캐시 ID는 객체의_typename 과 ID 필드를 콜론으로 구별

특정 객체 유형에 대한 캐시 ID 형식을 사용자정의 할 수 있음.

특정 객체에 대해 캐시 ID를 생성할 수 없는 경우, 해당 객체는 상위 객체 내부에 직접 캐시되며 상위를 통해 참조되어야 함.

  1. 객체 필드를 참조로 바꿈.
    캐시는 객체가 포함된 각 필드를 가져와 해당 객체에 대한 참조도 값 바꿈.

  2. 정규환된 객체 저장
    결과 객체도 모두 캐시의 flat lookup 테이블에 저장됨.

기존 객체가 기존 캐시된 객체와 동일한 ID를 가질땐 해당 개체의 필드가 병합됩니다.
수신 객체와 기존 객체가 필드를 공유한 경우 수신 개체도 해당의 제시된 값을 덮어 씁니다.

캐시 시각화

데이터 구조를 이해하기

InMemoryCache Apollo Client 생성자에게 아래와 같이 제공

구성 요소 옵션

캐시의 기본 동작은 다양한 응용 프로그램에 적합하지만, 특정 사용 사례에 맞게 캐시의 동작을 구성할 수 있음.

  • 사용자 지정 기본 키 필드 지정
  • 개별 필드의 저장 및 검색 사용자 정의
  • 필드 인자 해석 사용자 정의
  • fragment 일치를 위한 Supertype, subtype 관계 정의
    - https://dataprofessional.tistory.com/86 (서브 타입, 슈퍼 타입 정의 참고)
    _ pagination에 대한 패턴 정의
  • 클라이언트 측 로컬 상태 관리

cache 동작을 사용자 정의 하기 위해 InMemory Cache 생성자에 옵션 객체를 제공

  • addTypeName
  • resultCaching
  • possible Types
  • type Policies

캐시 ID 사용자 정의

InMemoryCache가 스키마 개별유형에 대한 캐시 ID를 생성하는 방법 사용자 정의 가능

InMemoryCache가 스키마의 개별 유형에 대한 캐시 ID를 생성하는 방법을 사용자 정의할 수 있음
특히 유형이 id 또는 _id 이외의 필드 (또는 필드!)를 고유 식별자로 사용하는 경우에 유용

이렇게 하려면 사용자 지정할 각 유형에 대해 TypePolicy를 정의
InMemoryCache 생성자에 제공하는 옵션 개체에서 캐시의 모든 typePolicy를 지정

다음과 같이 관련 TypePolicy 개체에 keyFields 필드를 포함

객체의 캐시 ID 계산

여러 필드를 사용하는 사용자 지정 캐시 ID를 정의한 경우 해당 ID를 계산하여 필요한 메서드에 제공하는 것이 어려울 수 있을

cache.identify 메소드를 사용하여 캐시에서 가져오는 표준화된 객체에 대한 캐시 ID를 계산 할 수 있음.

글로벌하게 식별자 생성 사용자 지정

특정_typename 에 한정되지 않은 단일 fallback keyfield 함수를 정의해야 할 경우, dataIdFrom Object 를 사용할 수 있음.

정규화를 사용하지 않음

InMemoryCache 에서 특정 유형의 객체에 대해 정규화하지 않도록 지시 가능. 유형에 대해 정규화 사용 시 해당 은행에 대한 TypePolicy 를 제한 정책의 Keyfield 를 false 로 설정

TypePolicy 필드

캐시가 스키마의 특정 유형과 상호 작용한 방법을 사용자지정하기 위해서
InMemoryCache 객체를 만들 때 TypePolicy 객체에 _typename.
문자열을 매핑한 객체를 제공

root 작업 유형 재정의

keyFields 외에도 typePolicy는 query,mutaion,subscription에 때해 queryType, mutationType, subscriptionType을 설정할 수 있음

캐시 읽고 쓰기

GraphQL 서버와 통신하지 않음 아폴로 클라이언트 캐시에 데이터를 직접 읽고 쓸 수 있음
이전에 서버에서 가져온 데이터 및 로컬에서만 사용할 수 있는 데이터와 상호 작용 할 수 있음.

  • GraphQL쿼리 사용(readQuery / writeQuery)
    - 원격 및 로컬 데이터를 모두 관리하기 위한 표준 GraphQL 쿼리 사용
  • GraphQL fragment 사용(readFragment / writeFragment)
    - 전체 쿼리를 구성하지 않고 캐시된 개체의 필드에 액세스하여 해당 개체에 연결할 수 있음
  • 캐시된 필드를 직접 수정 (cache.modify)
    - GraphQL를 전혀 사용하지 않고 캐시된 데이터 조작

위 전략 및 메서드를 사례에 따라 조합하여 사용할 수 있음

GraphQL 쿼리 사용

서버에서 실행하는 쿼리와 유사하거나 동일한 GraphQL 쿼리를 사용하여 캐시 데이터를 읽고 쓸 수 있음

  • readQuery
    readQuery 방법을 사용하면 다음과 같이 캐시에서 GraphQL 쿼리를 직접 실행할 수 있음
const READ_TODO = gql`
  query ReadTodo($id: ID!) {
    todo(id: $id) {
      id
      text
      completed
    }
  }
`;

// Fetch the cached to-do item with ID 5

const { todo } = client.readQuery({
  query: READ_TODO,
  variables: { // Provide any required variables here
    id: 5,
  },
});

캐시에 모든 쿼리 필드에 대한 데이터가 포함된 경우 readQuery는 쿼리 모양과 일치하는 개체를 반환

쿼리 문자열에 typename 필드를 포함하지 않더라도 Apollo Client는 모든 개체의 typename에 대해 자동으로 쿼리함

반환된 개체를 직접 수정하지 안됨. 동일한 개체가 여러 컴포넌트로 반환될 수 있으며 캐시에 데이터를 업데이트하기 위해서 대체 개체를 만들어 writeQuery 전달해야 함.

캐시에 쿼리 필드에 대한 데이터가 누락된 경우 readQuery는 null을 반환. GraphQL 서버에서 데이터를 가져오려고 시도하지 않음

readQuery를 제공하는 쿼리에는 GraphQL 서버의 스키마에서 정의되지 않은 필드(예: 로컬 전용 필드)가 포함될 수 있음

  • WriteQuery
    writeQuery 메서드를 사용하면 데이터를 GraphQL 쿼리와 일치하는 모양으로 캐시에 쓸 수 있음
    데이터 옵션이 필요하다는 점을 제외하면 readQuery와 유사
client.writeQuery({
  query: gql`
    query WriteTodo($id: Int!) {
      todo(id: $id) {
        id
        text
        completed
      }
    }`,
  data: { // Contains the data to write
    todo: {
      __typename: 'Todo',
      id: 5,
      text: 'Buy grapes 🍇',
      completed: false
    },
  },
  variables: {
    id: 5
  }
});

이 예에서는 ID 5로 캐시된 작업관리 오브젝트를 작성(편집)합니다.

writeQuery 대해 다음 사항을 참고:

writeQuery와 함께 캐시된 데이터에 대한 변경 사항쿼리는 GraphQL 서버에 푸시되지 않음 환경을 다시 로드하면 이러한 변경 사항이 사라짐.
쿼리 모양은 GraphQL 서버의 스키마에 의해 강제되지 않음

- 쿼리에는 스키마에 없는 필드가 포함될 수 있음
- 스키마에 따라 유효하지 않은 스키마 필드 값을 제공할 수 있음(일반적으로 제공해서는 안 됨).

GraphQL fragment 사용

정규화된 캐시 개체에서 GraphQL fragment을 사용하여 캐시 데이터를 읽고 쓸 수 있음 이렇게 하면 완전한 유효한 쿼리가 필요한 readQuery/writeQuery보다 캐시된 데이터에 더 많은 "랜덤 액세스"를 제공

  • readFragment
    이 예제는 readFragment를 사용하여 readQuery의 예제와 동일한 데이터를 가져옴
const todo = client.readFragment({
  id: 'Todo:5', // The value of the to-do item's cache ID
  fragment: gql`
    fragment MyTodo on Todo {
      id
      text
      completed
    }
  `,
});

readQuery와 달리 readFragment에는 ID 옵션이 필요
이 옵션은 캐시에 있는 개체의 캐시 ID를 지정. 기본적으로 캐시 ID의 형식은 <_typename>:임 이 ID를 사용자 정의할 수 있음

위의 예에서 readFragment는 ID가 5인 Todo 개체가 캐시에 존재하지 않거나 개체가 존재하지만 텍스트 또는 완료에 대한 값이 없는 경우 null을 반환.

  • writeFragment
    readFragment를 사용하여 Apollo 클라이언트 캐시에서 "random-access" 데이터를 읽을 뿐만 아니라 writeFragment 메서드로 캐시에 데이터를 쓸 수 있음.

writeFragment를 사용하여 캐시된 데이터를 변경한 내용은 GraphQL 서버에 푸시되지 않음 환경을 다시 로드하면 이러한 변경 사항이 사라짐

writeFragment 메서드는 추가 데이터 변수가 필요하다는 점을 제외하면 readFragment와 유. 예를 들어, writeFragment에 대한 다음 호출은 ID가 5인 Todo 객체에 대해 완료된 플래그를 업데이트

client.writeFragment({
id: 'Todo:5',
fragment: gql`
  fragment MyTodo on Todo {
    completed
  }
`,
data: {
  completed: true,
},
});

모든 활성 쿼리를 포함하여 Apollo Client 캐시에 대한 모든 subscribers는 이 변경 사항을 확인하고 그에 따라 응용 프로그램의 UI를 업데이트함

읽기 쓰기 결합

현재 캐시된 데이터를 가져와 선택적으로 수정하여 readQuery와 writeQuery 결합할 수 있음(또는 readFragment 및 writeFragment). 아예제는 새 작업 항목을 작성하여 캐시된 작업관리 목록에 추가. 이 추가는 원격 서버로 전송되지 않음

// 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.modify 사용

InMemoryCache의 modify 메서드는 개별 캐시된 필드의 값을 직접 수정하거나 필드를 완전히 삭제할 수 있음.

  • writeQuery 및 writeFragment와 같이 수정된 필드에 종속된 모든 활성 쿼리의 새로 고침을 트리거

  • writeQuery 및 writeFragment와 달리:
    - 수정은 정의한 merge 함수를 우회. 즉, 필드는 항상 지정한 값으로 덮어쓰여짐
    - 수정은 캐시에 없는 필드를 쓸 수 없음

    감시된 쿼리는 client.watchQuery 또는 useQuery hook에 fetchPolicy 및 nextFetchPolicy와 같은 옵션을 전달하여 캐시 업데이트로 무효화될 때 발생하는 작업을 제어할 수 있음.

매개변수

API 참조에 명시된 수정 방법은 다음 매개 변수를 사용

  • 수정할 캐시된 개체의 ID(cacehe.identify와 함께 캐시를 사용하여 가져오는 것을 추천).
  • 실행할 modifier 함수의 맵(수정할 각 필드에 하나씩)
  • 동작을 사용자 지정하기 위한 broadcast 및 optimistic 부울 값 선택

다음은 이름 필드를 수정하여 값을 대문자로 변환하는 예제

cache.modify({
id: cache.identify(myObject),
fields: {
  name(cachedName) {
    return cachedName.toUpperCase();
  },
},
/* broadcast: false // Include this to prevent automatic query refresh */
});

Values vs. references

스칼라, 열거형 또는 이러한 기본 유형의 목록을 포함하는 필드에 대한 한정자 함수를 정의하면 한정자 함수가 필드에 대한 기존 값과 정확히 일치하도록 전달됩니다. 예를 들어 현재 값이 5인 개체의 수량 필드에 한정자 함수를 정의하면 한정자 함수가 값 5를 통과합니다.

그러나 개체 유형 또는 개체 목록을 포함하는 필드에 한정자 함수를 정의하면 해당 개체가 참조로 표시됩니다. 각 참조는 캐시 ID로 캐시의 해당 개체를 가리킵니다. 한정자 함수에 다른 참조를 반환하면 이 필드에 포함된 다른 캐시된 개체를 변경할 수 있습니다. 원래 캐시된 개체의 데이터는 수정하지 않습니다.

수정자 함수 유틸리티

수정자 함수는 선택적으로 여러 유용한 유틸리티를 포함하는 개체인 두 번째 매개 변수를 사용할 수 있음.

이러한 유틸리티(readField 함수와 DELETE Sentinel 개체) 중 두 가지가 아래 예제에 사용됨
사용 가능한 모든 유틸리티에 대한 설명은 API 참조를 참조.

예제: 목록에서 항목 제거
각 포스트에 다양한 의견이 있는 블로그 애플리케이션이 있다고 가정해 보았을 때,
페이지화된 Post.comments 배열에서 특정 주석을 제거하는 방법은 다음과 같음

const idToRemove = 'abc123';

cache.modify({
id: cache.identify(myPost),
fields: {
  comments(existingCommentRefs, { readField }) {
    return existingCommentRefs.filter(
      commentRef => idToRemove !== readField('id', commentRef)
    );
  },
},
});

comments 필드 modifier 함수가 호출되면 먼저 writeFragment를 호출하여 캐시에 newComment 데이터를 저장, writeFragment 함수는 새로 캐시된 주석을 가리키는 참조(newCommentRef)를 반환

그런 다음 기존 주석 참조(existingCommentRefs)의 배열을 검사하여 새 주석이 목록에 없는지 확인합
그렇지 않으면 참조 목록에 새 주석 참조를 추가하여 캐시에 저장할 전체 목록을 반환

mutation 후 캐시 업데이트

캐시가 식별할 수 있는 options.data 객체를 사용하여 writeFragment를 호출하는 경우(__typename 및 캐시 ID 필드를 기반으로) options.id를 writeFragment로 전달하지 않도록 할 수 있음

options.id을 명시적으로 제공하든 writeFragment가 options.data를 사용하여 해결하도록 하든 writeFragment는 식별된 객체에 대한 참조를 반환

이러한 동작은 writeFragment를 캐시 내 기존 객체에 대한 참조를 얻는 데 좋은 도구로 만들며, 이는 useMutation에 대한 업데이트 함수를 작성할 때 유용

const [addComment] = useMutation(ADD_COMMENT, {
  update(cache, { data: { addComment } }) {
    cache.modify({
      id: cache.identify(myPost),
      fields: {
        comments(existingCommentRefs = [], { readField }) {
          const newCommentRef = cache.writeFragment({
            data: addComment,
            fragment: gql`
              fragment NewComment on Comment {
                id
                text
              }
            `
          });
          return [...existingCommentRefs, newCommentRef];
        }
      }
    });
  }
});

이 예에서 useMutation은 자동으로 주석을 만들어 캐시에 추가하지만 해당 주석을 해당 포스트의 주석 목록에 추가하는 방법은 자동으로 알지 못 함
즉, 게시물의 주석 목록을 감시하는 쿼리는 업데이트되지 않음

이 문제를 해결하기 위해 useMutation의 업데이트 콜백을 사용하여 cache.modify를 호출
이전 예와 달리 useMutation에 의해 이미 캐시에 설명이 추가됨
결과적으로 cache.writeFragment는 기존 개체에 대한 참조를 반환

캐시된 객체에서 필드 삭제

보조자 함수의 선택적 두 번째 매개 변수는 canRead 및 isReference 함수와 같은 몇 가지 유용한 유틸리티를 포함하는 개체임.
또한 DELETE라는 감시 개체도 포함

특정 캐시된 개체에서 필드를 삭제하려면 다음과 같이 필드의 한정자 함수에서 DELETE Sentinel 개체를 반환

cache.modify({
id: cache.identify(myPost),
fields: {
  comments(existingCommentRefs, { DELETE }) {
    return DELETE;
  },
},
});

####캐시된 개체 내의 필드를 무효화하는 중
일반적으로 필드 값을 변경하거나 삭제하면 필드가 무효화되므로 이전에 필드를 사용한 경우 감시된 쿼리를 다시 읽을 수 있습니다.

cash.modify를 사용하면 INVALIDATE sentinel을 반환하여 값을 변경하거나 삭제하지 않고 필드를 무효화할 수도 있음

cache.modify({
  id: cache.identify(myPost),
  fields: {
    comments(existingCommentRefs, { INVALIDATE }) {
      return INVALIDATE;
    },
  },
});

지정된 개체 내의 모든 필드를 무효화해야 하는 경우 필드 옵션의 값으로 한정자 함수를 전달할 수 있음

cache.modify({
id: cache.identify(myPost),
fields(fieldValue, details) {
  return details.INVALIDATE;
},
});

cache.modify 형식을 사용할 때, detail.fieldName을 사용하여 개별 필드 이름을 결정할 수 있음
INVALIDATE를 반환하는 함수뿐만 아니라 모든 한정자 함수에도 사용할 수 있음

객체의 cache ID 얻기

캐시의 유형이 사용자 지정 캐시 ID를 사용하는 경우(또는 사용하지 않는 경우에도) cache.identify을 사용하여 해당 유형의 개체에 대한 캐시 ID를 얻을 수 있음 이 메서드는 개체를 가져와서 __typename 및 식별자 필드를 기준으로 ID를 계산.
즉, 각 유형의 캐시 ID를 구성하는 필드를 추적할 필요가 없음.


캐시된 GraphQL 개체의 자바스크립트 표현이 있다고 가정

const invisibleManBook = {
__typename: 'Book',

isbn: '9780679601395', // The key field for this type's cache ID
title: 'Invisible Man',
author: {
  __typename: 'Author',
  name: 'Ralph Ellison',
},
};

writeFragment 또는 cash.modify와 같은 방법으로 캐시에 있는 이 개체와 상호 작용하려면 개체의 캐시 ID가 필요
.ID 필드가 없으므로 북 유형의 캐시 ID는 사용자 정의인 것으로 나타남.

우리의 Book 타입이 그것의 캐시 ID에 isbn 필드를 사용한다는 것을 조회할 필요 없이, 우리는 다음과 같은 cache.identify 메소드를 사용할 수 있음

const bookYearFragment = gql`
  fragment BookYear on Book {
    publicationYear
  }
`;

const fragmentResult = cache.writeFragment({

  id: cache.identify(invisibleManBook),
  fragment: bookYearFragment,
  data: {
    publicationYear: '1952'
  }
});

캐시는 Book유형이 캐시 ID에 isbn 필드를 사용한다는 것을 알고 있으므로 cache.identify는 위의 ID 필드를 올바르게 채울 수 있음.

캐시 ID는 단일 필드(isbn)를 사용하기 때문에 이 예는 간단
그러나 사용자 지정 캐시 ID는 isbn 및 title과 같은 여러 필드로 구성될 수 있음
따라서 캐쉬 ID를 사용하지 않고 개체의 캐시 ID를 지정하는 것이 훨씬 더 어렵고 반복적.

Garbage 컬렉션 and cache 제거

Apollo 클라이언트 3을 사용하면 더 이상 유용하지 않은 캐시된 데이터를 선택적으로 제거할 수 있음
gc 메소드의 기본 가비지 수집 전략은 대부분의 응용 프로그램에 적합하지만, 제거 방법은 이를 필요로 하는 응용 프로그램에 대해 보다 세분화된 제어를 제공

cache.gc()

gc method는 연결할 수 없는 모든 개체를 정규화된 캐시에서 제거

cash.gc();

개체에 연결할 수 있는지 여부를 확인하기 위해 캐시는 알려진 모든 루트 개체에서 시작하고 추적 전략을 사용하여 사용 가능한 모든 하위 참조를 반복적으로 방문
이 프로세스 중에 방문하지 않은 정규화된 개체는 제거됩니다.
cash.gc() 메서드는 제거된 개체의 ID 목록을 반환

가비지 수집 구성

retain 방법을 사용하여 개체에 연결할 수 없는 경우에도 개체와 하위 개체에 가비지가 수집되지 않도록 할 수 있음

cache.retain('my-object-id');
나중에 보존 개체를 가비지 수집하려면 릴리스 방법을 사용

cache.release('my-object-id');
개체에 연결할 수 없는 경우 gc에 대한 다음 호출 중에 가비지가 수집

cache.evict()

evict 메서드를 사용하여 캐시에서 정규화된 개체를 제거할 수 음

cache.evict({ id: 'my-object-id' })
제거할 필드 이름을 제공하여 캐시된 오브젝트에서 단일 필드를 제거할 수도 있음

cache.evict({ id: 'my-object-id', fieldName: 'yearOfFounding' });
개체를 제거하면 다른 캐시된 개체에 연결할 수 없게 되는 경우가 많음. 따라서 캐시에서 하나 이상의 개체를 제거한 후 cash.gc를 호출

캐시된 필드의 동작 커스터마이징

-아폴로 클라이언트 캐시의 특정 필드를 읽고 쓰는 방법을 사용자 정의함 필드에 대한 필드 정책을 정의함
필드 정책에는 다음이 포함될 수 있음

  • 필드의 캐시된 값을 읽을 때 발생하는 작업을 지정하는 read 함수

  • 필드의 캐시된 값이 기록될 때 수행할 작업을 지정하는 merge 함수

  • 캐시가 불필요한 중복 데이터를 저장하지 않도록 하는 데 도움이 되는 key argument 배열

    InMemoryCache 생성자에게 필드 정책을 제공합니다.
    각 필드 정책은 필드가 포함된 유형에 해당하는 TypePolicy 개체 내부에서 정의다음 예에서는 사용자 유형의 이름 필드에 대한 필드 정책을 정의함

const cache = new InMemoryCache({
  typePolicies: {
    Person: {
      fields: {

        name: {
          read(name) {
            // Return the cached name, transformed to upper case
            return name.toUpperCase();
          }
        }
      },
    },
  },
});
  

 

read 함수

필드에 대한 read 함수를 정의하는 경우 캐시는 클라이언트가 필드에 대해 쿼리할 때마다 해당 함수를 호출합니다. 질의 응답에서 필드는 필드의 캐시된 값 대신 read 함수의 반환 값으로 채워집니다.

read 함수의 첫 번째 매개 변수는 필드의 현재 캐시된 값(있는 경우)을 제공합니다.
이 옵션을 사용하여 함수의 반환 값을 결정할 수 있습니다.

두 번째 매개 변수는 FieldPolicy API 참조에서 설명하는 여러 속성과 도우미 기능에 대한 액세스를 제공하는 개체입니다.

필드가 인수를 허용하는 경우 두 번째 매개 변수에는 해당 인수의 값이 포함

읽기 기능의 다른 사용 사례는 다음과 같습니다.

  • 부동 소수점 값을 가장 가까운 정수로 반올림하는 것과 같이 캐시된 데이터를 클라이언트의 필요에 맞게 변환

  • 동일한 개체의 하나 이상의 스키마 필드에서 로컬 전용 필드 도출(예: 생년월일 필드에서 연령 필드 도출)

  • 여러 개체에 걸쳐 하나 이상의 스키마 필드에서 로컬 전용 필드 도출

    merge 함수

    필드에 대한 병합 함수를 정의하는 경우 캐시는 필드가 들어오는 값(예: GraphQL 서버)으로 작성될 때마다 해당 함수를 호출합니다. 쓰기가 발생하면 필드의 새 값은 원래 들어오는 값 대신 병합 함수의 반환 값으로 설정

    배열 병합

    병합 함수의 일반적인 사용 사례는 배열을 포함하는 필드에 쓰는 방법을 정의하는 것입니다. 기본적으로 필드의 기존 배열은 들어오는 배열로 완전히 대체됩니다. 대신 다음과 같이 두 어레이를 연결하는 것이 좋습니다.

    정규화되지 않은 개체 병합

    • 사용자 지정 필드 병합 함수의 또 다른 일반적인 사용 사례는 ID가 없지만 알려진 중첩된 객체를 결합하여 부모 객체가 동일하다고 가정할 때 동일한 논리적 객체를 나타냄

    Book 유형에 작성자의 이름, 언어 및 생년월일과 같은 정보가 들어 있는 오브젝트인 작성자 필드가 있다고 가정합니다. Book 개체는 __typename: "Book" 및 고유한 isbn 필드를 가지고 있으므로 캐시는 두 개의 Book 결과 개체가 동일한 논리적 개체를 나타낼 때 알 수 있습니다. 그러나 어떤 이유에서든지 이 책을 검색한 질의는 책에 대한 충분한 정보를 요구하지 않았다.저자 목적 작성자 유형에 대해 keyFields가 지정되지 않았을 수 있으며 기본 ID 필드는 없습니다.

이렇게 식별 정보가 부족하면 캐시가 두 Author 결과 개체가 동일한지 여부를 자동으로 확인할 수 없기 때문에 캐시에 문제가 발생합니다. 여러 쿼리가 이 책의 작성자에 대한 다른 정보를 요청할 경우 쿼리의 순서가 중요합니다. 왜냐하면 첫번째 쿼리의 favoriteBook.author 객체가 2번째 쿼리의 favoriteBook.author 객체와 함께 안전하게 병합되지 못하기 때문임

이러한 상황에서 캐시는 기본적으로 이름과 언어 필드를 함께 병합하지 않은채로 기존 favoriteBook.author 데이터를 들어오는 데이터로 변경합니다. 왜냐하면 author 수용되지 못하여 일관성 없는 이름과 언어 필드를 병합하는데 위험이 있기 때문입니다.

favoriteBook.author에 대한 ID 필드를 요청하도록 쿼리를 수정하거나 작성자 개체 또는 ["name", "dateOfBirth"]와 같은 작성자 유형 정책에서 사용자 지정 keyFields를 지정함으로 문제를 해결.
캐시에 이 정보를 제공하면 두 작성자 개체가 동일한 논리적 엔터티를 나타내는 시기를 알 수 있으므로 필드를 안전하게 병합할 수 있습니다. 가능한 경우 이 솔루션을 사용하는 것이 좋습니다.

그러나 데이터 그래프가 작성자 개체에 대해 고유 식별 필드를 제공하지 않는 상황이 발생할 수 있으며 이러한 드문 시나리오에서, 주어진 책이 한 명의 기본 작성자만 있고 작성자는 변경되지 않는다고 가정하는 것이 안전할 수 있습니다. 즉, 저자의 정체성은 책의 정체성에 의해 암시될 수 있다는 말입니다. 이 상식적인 지식은 인간으로서 여러분이 마음대로 할 수 있는 것이지만, 그것은 인간도 아니고 텔레파시 능력도 없는 은신처로 전달되어야 합니다.

다행히 병합 함수에 전달된 옵션에서 options.mergeObjects라는 도우미 함수를 찾을 수 있습니다. 이 함수는 수신 필드에 사용자 지정 병합 함수가 있는 경우를 제외하고 일반적으로 {...기존, ...수신 }과 동일하게 작동합니다. options.mergeObjects가 두 번째 인수(수신)의 필드에 대해 사용자 정의 병합 함수를 발견하면 다음과 같이 중첩된 병합 함수가 호출되어 기존 및 수신 필드를 결합합니다.

const cache = new InMemoryCache({
  typePolicies: {
    Book: {
      fields: {
        author: {
          merge(existing, incoming, { mergeObjects }) {
            // Correct, thanks to invoking nested merge functions.
            return mergeObjects(existing, incoming);
          },
        },
      },
    },
  },
});

정규화되지 않은 객체의 배열 병합

  • 섹션의 아이디어와 권장 사항에 익숙해지면 한 책에 여러 명의 저자가 있을 경우에는??
const cache = new InMemoryCache({
  typePolicies: {
    Book: {
      fields: {
        authors: {
          merge(existing: any[], incoming: any[], { readField, mergeObjects }) {
            const merged: any[] = existing ? existing.slice(0) : [];
            const authorNameToIndex: Record<string, number> = Object.create(null);
            if (existing) {
              existing.forEach((author, index) => {
                authorNameToIndex[readField<string>("name", author)] = index;
              });
            }
            incoming.forEach(author => {
              const name = readField<string>("name", author);
              const index = authorNameToIndex[name];
              if (typeof index === "number") {
                // Merge the new author data with the existing author data.
                merged[index] = mergeObjects(merged[index], author);
              } else {
                // First time we've seen this author in this array.
                authorNameToIndex[name] = merged.length;
                merged.push(author);
              }
            });
            return merged;
          },
        },
      },
    },
  },
});

기존 작성자 배열을 들어오는 배열로 맹목적으로 대체하는 대신, 이 코드는 배열을 함께 연결하는 동시에 중복 작성자 이름을 확인하여 반복되는 작성자 개체의 필드를 병합합니다.

readField 도우미 함수는 author.name을 사용하는 것보다 더 강력합니다. 왜냐하면 작성자가 캐시의 다른 곳에 있는 데이터를 참조하는 참조 개체일 가능성도 허용하기 때문입니다. 사용자(또는 사용자 팀의 다른 사람)가 최종적으로 작성자 유형에 대한 keyFields를 지정할 때 발생할 수 있습니다.

이 예에서 알 수 있듯이 병합 기능은 상당히 정교해질 수 있습니다. 이 경우 재사용 가능한 도우미 함수에 일반 논리를 추출할 수 있습니다.

페이지네이션 처리

필드가 배열을 보유할 경우 전체 결과 집합이 임의로 클 수 있으므로 해당 배열의 결과를 페이지화하는 것이 유용할 수 있습니다.

일반적으로 쿼리에는 다음을 지정하는 페이지 지정 인수가 포함됩니다.

  • 숫자 offset 또는 시작 ID를 사용하여 배열에서 시작할 위치
  • 단일 "페이지"로 반환할 최대 요소 수
    필드에 대한 페이지 배분을 구현하는 경우 해당 필드에 대한 읽기 및 병합 함수를 구현하는 경우 페이지 배율 인수를 염두에 두는 것이 중요합니다.
  const cache = new InMemoryCache({
  typePolicies: {
    Agenda: {
      fields: {
        tasks: {
          merge(existing: any[], incoming: any[], { args }) {
            const merged = existing ? existing.slice(0) : [];
            // Insert the incoming elements in the right places, according to args.
            const end = args.offset + Math.min(args.limit, incoming.length);
            for (let i = args.offset; i < end; ++i) {
              merged[i] = incoming[i - args.offset];
            }
            return merged;
          },

          read(existing: any[], { args }) {
            // If we read the field before any data has been written to the
            // cache, this function will return undefined, which correctly
            // indicates that the field is missing.
            const page = existing && existing.slice(
              args.offset,
              args.offset + args.limit,
            );
            // If we ask for a page outside the bounds of the existing array,
            // page.length will be 0, and we should return undefined instead of
            // the empty array.
            if (page && page.length > 0) {
              return page;
            }
          },
        },
      },
    },
  },
});

이 예에서 알 수 있듯이 읽기 함수는 종종 같은 인수를 역방향으로 처리하여 병합 함수와 협력해야 합니다.

args.offset에서 시작하는 대신 특정 엔티티 ID 다음에 "페이지"를 시작하려면 readField 도우미 함수를 사용하여 다음과 같이 병합 및 읽기 함수를 구현하여 기존 작업 ID를 검사하십시오.

  const cache = new InMemoryCache({
  typePolicies: {
    Agenda: {
      fields: {
        tasks: {
          merge(existing: any[], incoming: any[], { args, readField }) {
            const merged = existing ? existing.slice(0) : [];
            // Obtain a Set of all existing task IDs.
            const existingIdSet = new Set(
              merged.map(task => readField("id", task)));
            // Remove incoming tasks already present in the existing data.
            incoming = incoming.filter(
              task => !existingIdSet.has(readField("id", task)));
            // Find the index of the task just before the incoming page of tasks.
            const afterIndex = merged.findIndex(
              task => args.afterId === readField("id", task));
            if (afterIndex >= 0) {
              // If we found afterIndex, insert incoming after that index.
              merged.splice(afterIndex + 1, 0, ...incoming);
            } else {
              // Otherwise insert incoming at the end of the existing data.
              merged.push(...incoming);
            }
            return merged;
          },

          read(existing: any[], { args, readField }) {
            if (existing) {
              const afterIndex = existing.findIndex(
                task => args.afterId === readField("id", task));
              if (afterIndex >= 0) {
                const page = existing.slice(
                  afterIndex + 1,
                  afterIndex + 1 + args.limit,
                );
                if (page && page.length > 0) {
                  return page;
                }
              }
            }
          },
        },
      },
    },
  },
});

readField(fieldName)를 호출하면 현재 개체에서 지정된 필드 값이 반환됩니다. 개체를 readField(예: readField("id", 작업))에 두 번째 인수로 전달하면 readField는 대신 지정된 개체에서 지정된 필드를 읽습니다. 위의 예에서는 기존 작업 개체에서 ID 필드를 읽으면 들어오는 작업 데이터를 중복 제거할 수 있습니다.

위의 페이지 지정 코드는 복잡하지만 선호하는 페이지 지정 전략을 구현한 후에는 필드 유형에 관계없이 해당 전략을 사용하는 모든 필드에 다시 사용할 수 있습니다.

0개의 댓글