Placeholder and Initial Data in React Query

content-logo

TkDodoPlaceholder and Initial Data in React Query를 번역한 문서입니다.


오늘의 아티클은 React Query를 사용할 때 사용자 경험을 향상시키는 방법에 관한 것입니다. 대부분의 경우, 우리와 (우리의 사용자들은) 성가신 로딩 스피너를 싫어합니다. 로딩 스피너는 때때로 필요하지만, 가능하다면 피하고 싶습니다.

React Query는 이미 많은 상황에서 로딩 스피너를 사용하지 않을 수 있도록 도구를 제공하고 있습니다. 백그라운드에서 데이터를 다시 불러올 때 캐시에서 오래된 데이터를 갖고올 수 있고, 나중에 필요하다는 것을 안다면 데이터를 미리 불러올 수 있습니다. 그리고 쿼리 키가 변경될 때 이전의 데이터를 계속 유지하는 방식으로 로딩 스피너를 사용하지 않을 수도 있습니다.

또 다른 방법으로는 사용할 가능성이 있는 데이터를 캐시에 동기적으로 미리 채워 놓은 방법이 있습니다. 이를 위해 React Query는 2개의 서로 다르면서도 비슷한 방법을 제공합니다. Placeholder DataInitial Data입니다.

두 방법의 차이점을 살펴보기 전에 먼저 공통점을 살펴보겠습니다. 그리고 각각의 방법이 어떤 상황에서 적합한지를 알아보겠습니다.

공통점 (Similarities)

앞에서 힌트를 드렸듯이, 두 방법 모두 동기적으로 사용 가능한 데이터를 캐시에 미리 채워 넣는 방법입니다. 이는 더 나아가서 두 방법 중 하나라도 제공되면, 쿼리는 loading상태를 건너뛰고 바로 success 상태로 들어설 것입니다. 또한, 각각의 방법은 값이 될 수도 있고, 값의 연산 비용이 높다면 해당 값을 반환하는 함수가 될 수도 있습니다.

// success-queries

function Component() {
  // ✅ 데이터를 불러오지 않았지만 status는 success가 될 것입니다
  const { data, status } = useQuery({
    queryKey: ['number'],
    queryFn: fetchNumber,
    placeholderData: 23,
  });

  // ✅ initialData도 마찬가지 입니다
  const { data, status } = useQuery({
    queryKey: ['number'],
    queryFn: fetchNumber,
    initialData: () => 42,
  });
}

마지막으로, 두 방법은 캐시에 이미 데이터가 있다면 효과가 없습니다. 그렇다면 두 방법은 어떤 차이점이 있을까요? 이를 이해하려면, 먼저 React Query의 옵션이 어떻게 동작하는지 (그리고 어느 “레벨”에서 동작하는지)를 간단하게 살펴보아야 합니다.

캐시 레벨 (cache level)

각각의 쿼리 키에 대해서, 캐시 엔트리는 오직 한 개만 존재합니다. 이는 꽤 명확합니다. 왜냐하면 React Query를 훌륭하게 만드는 이유 중 하나가 어플리케이션에서 데이터를 “전역적으로” 공유할 수 있는 가능성이기 때문입니다.

useQuery에 제공하는 일부 옵션은 이 캐시 엔트리에 영향을 줍니다. 주목할만한 예시로는 staleTimegcTime이 있습니다. 캐시 엔트리는 하나만 있기 때문에, 해당 옵션들은 엔트리가 오래되는 시점 또는 가비지 컬렉션이 될 수 있는 시점을 특정합니다.

옵저버 레벨 (On observer level)

React Query의 옵저버는, 넓게 말해, 하나의 캐시 엔트리를 향해 생성된 구독입니다. 옵저버는 캐시 엔트리에서의 변화를 계속 주시하며 변화가 있을 때마다 알림을 받습니다.

옵저버를 생성하는 기본적인 방법은 useQuery를 호출하는 것입니다. 매번 호출할 때마다, 옵저버를 생성하며, 데이터가 변할 때마다 컴포넌트는 리렌더링 할 것입니다. 이는 당연하게도 동일한 캐시 엔트리를 주시하는 다수의 옵저버를 만들 수도 있음을 의미합니다.

여기서 잠깐, React Query 데브툴에서는 쿼리가 몇 개의 옵저버를 갖고 있는지를 좌측에 있는 숫자로 확인할 수 있습니다. (해당 예시에서는 3개 입니다.)

PR recently i made

옵저버 레벨에서 작동하는 또 다른 옵션은 select 또는 keepPreviousData가 있을 것입니다. 사실, select데이터 변환 측면에서 훌륭하다고 하는 이유는, 동일한 캐시 엔트리를 주시하지만 컴포넌트마다 그 데이터의 다른 슬라이스를 구독할 수 있기 때문입니다.

차이점 (Differences)

InitialData는 캐시 레벨에서 작동하는 반면, placeholderData는 옵저버 레벨에서 작동합니다. 여기에는 몇 가지의 구현 사항이 있습니다.

지속성 (Persistence)

먼저 initialData는 캐시에 지속적으로 남아있습니다. 이는 React Query에 다음과 같이 말하는 것과 같습니다. 나는 내 필요에 딱 맞는 “좋은” 데이터를 이미 갖고 있어. 이 데이터는 백엔드에서 불러온 것과 비슷한 수준의 데이터야. initialData는 캐시 레벨에서 작동하기 때문에 오직 하나만 존재할 수 있습니다. 그리고 해당 데이터는 캐시 엔트리가 생성되자마자 (첫 번째 옵저버가 마운트 되자마자) 캐시에 들어갈 것입니다. 또 다른 initialData와 함께 두 번째 옵저버를 마운트 하려고 한다면 아무 일도 일어나지 않을 것입니다.

반면에 placeholderData는 캐시에 절대로 지속적으로 남아있지 않습니다. 저는 해당 데이터를 “실제 데이터를 만들어내기 전까지 속이는” 용도로 여깁니다. 해당 데이터는 “실제”가 아닙니다. React Query는 실제 데이터가 불러와지는 동안에 해당 데이터를 출력합니다. 해당 데이터는 옵저버 레벨에서 작동하기 때문에 이론적으로 컴포넌트 별로 서로 다른 placeholderData를 갖고 있을 수 있습니다.

백그라운드에서 데이터 다시 불러오기 (Background refetches)

placeholderData를 사용하면 옵저버가 처음 마운트 될 때 항상 백그라운드에서 데이터가 다시 불러와질 것입니다. 해당 데이터는 “실제 데이터”가 아니기 때문에 React Query는 실제 데이터를 불러올 것입니다. 이 과정 동안 useQueryplaceholderData값을 반환합니다. 이 플래그를 통해 사용자에게 지금 보고있는 데이터는 placeholderData라는 것을 시각적으로 알려줄 수 있습니다. 해당 값은 실제 데이터가 불러와지면 false로 전환될 것입니다.

반면 initialData는 실제 캐시에 들어갈 만큼 좋은 수준의 데이터이고 또 유효한 데이터이기 때문에, staleTime을 따릅니다. 만약 staleTime이 (기본값인) 0이라면 백그라운드에서 데이터를 다시 불러올 것입니다. 하지만 쿼리의 staleTime을 그 이상으로 (예를 들면 30초) 설정했다면 React Query는 initialData를 보면서 다음과 같이 생각할 것입니다.

오, 여기서 동기적으로 신선하고 새로운 데이터를 받고 있네요. 매우 감사해요. 이제 이 데이터가 있기 때문에 30초 동안은 백엔드에 갈 필요가 없겠네요.

— React Query가 initialDatastaleTime을 봤을 때

만약 이게 원하는 바가 아니라면 쿼리에 initialDataUpdatedAt을 제공할 수 있습니다. 이는 React Query에게 initialData가 생성된 시간을 알려주며, 백그라운드에서 데이터 다시 불러오기가 이를 고려하여 트리거 될 것입니다. 이는 이미 존재하는 캐시 엔트리에서 dataUpdatedAt 타임 스탬프를 사용해서 initialData를 사용할 때 매우 유용합니다.

// initialDataUpdatedAt

const useTodo = (id) => {
  const queryClient = useQueryClient();

  return useQuery({
    queryKey: ['todo', id],
    queryFn: () => fetchTodo(id),
    staleTime: 30 * 1000,
    initialData: () => queryClient.getQueryData(['todo', 'list'])?.find((todo) => todo.id === id),
    initialDataUpdatedAt: () =>
      // ✅ 목록 쿼리 데이터가 주어진 staleTime (30초) 보다 오래되면
      // 백그라운드에서 데이터를 다시 불러올 것입니다.
      queryClient.getQueryState(['todo', 'list'])?.dataUpdatedAt,
  });
};

에러 전환 (Error transitions)

initialData 또는 placeholderData를 제공하고, 백그라운드에서 데이터 다시 불러오기가 트리거된 후, 실패했다고 가정해보세요. 각각의 상황에서 어떤 일이 발생할 것 같나요? 정답을 숨겨놓았으니 원하신다면 펼쳐보기 전에 스스로 생각해보실 수 있습니다.

  • InitialData
    • initialData는 캐시 상에 남아있기 때문에 다시 불러오기 에러는 여느 백그라운드 에러와 동일하게 여겨집니다. 쿼리는 error 상태가 될 것이지만 data는 남아있을 것입니다.
  • PlaceholderData
    • placeholderData는 “실제 데이터를 만들어내기 전까지 속이는” 용도이고, 이 실제 데이터는 아직 만들어지지 않았기 때문에, 해당 데이터는 더이상 볼 수 없을 것입니다. 쿼리는 error 상태가 될 것이며, 데이터undefined가 될 것입니다.

언제 사용해야 하는지 (When to use what)

항상 그렇듯이, 이는 전적으로 여러분께 달렸습니다. 저는 개인적으로 다른 쿼리로부터 어떤 쿼리를 미리 채워놓을 때 initialData를 사용하고 그 외에는 placeholderData를 사용하는 것을 선호합니다.