문제정의

서버에서 데이터가 아직 도착하지 않았을 때 우리는 보통 Loading UI 를 보여주곤 한다. 그런데 Loading UI 를 보여주고 싶지 않을때는 어떡할까? 그 때는 보통 아래와 같은 nullish 병합 연산으로 아직 오지 않은 데이터에 대한 기본값을 처리할 것이다.


<p>
	{data?.quantity ?? 0 }
<p>

하지만 생각해보면 과연 데이터가 도착하지 않았을 때, 디폴트 값 인 ‘0’으로 표시해주는 것이 UI 렌더링 계층의 역할인지 생각해보자.

UI를 그려주는 영역에서, 데이터가 없을 때 디폴트 데이터는 이런 형태이다까지 신경써줘야할까? 그게 과연 UI 렌더링에 속하는 역할인지 생각해보면 아닌 것 같다.

UI 를 그려주는 영역의 역할은 단지 받아온 데이터를 요구사항대로 화면에 그려주기일 뿐이다.

간단한 데이터 스키마가 아니라 더욱더 복잡해질 경우.. 저런 nullish 병합 연산자 코드와 같이 데이터가 도착하지 않았을 때 특정 형태의 디폴트 데이터로 대체하는 코드가 가득해지면 아래와 같이 UI를 그려주는 영역의 역할이 더욱더 모호해지지 않을까생각한다.


<p>{data?.quantity ?? 0 }</p>
<p>{data?.user?.name ?? ''}</p>
<ProductList products={data?.productList ?? []}/>

왜냐하면, 첫번째로, 계속해서 서버에서 보내주는 데이터 스키마를 UI 영역에서 파악하고 있어야 하고, 두번째로, 디폴트 데이터 값이 여기저기 산발되기 때문이다.

즉 UI 영역에서는 ‘이 데이터가 도착하지 않았을 때 특정 형태의 디폴트 데이터로 대체한다’라는 책임을 가지는 것은 좋지 못하다고 생각한다. 이 ‘디폴트 데이터’는 서버로부터 데이터를 가져오는 곳에서 처리해줘야하지 않을까? 그래야 나중에 서버 데이터 스키마가 변경되었을 때/디폴트 데이터를 변경해야 할 때, UI 코드까지 건들이지 않고 유연하게 변경에 대응할 수 있지않을까?


<p>{data.quantity }</p>
<p>{data.user.name}</p>
<ProductList products={data.productList}/>

즉, 데이터가 존재하든 말든 UI 영역에서는 신경쓰지 않고 그저 데이터를 그려주는 역할만하는..위와 같은 형태가 되어야 한다.

그러면 이를 어떻게 해결 할 수 있을까? 만약 별다른 data fetching 라이브러리를 사용하지 않는다면 api 요청 과정에서 디폴트 데이터를 반환하게 해주면 될 것 같다. 그런데 내가 주로 사용하는 라이브러리인 리액트 쿼리에서는 이를 쉽게 옵션으로 설정할 수 있게 해준다.

리액트쿼리에서 어떻게 설정할 수 있을지 한번 알아보자.


리액트 쿼리의 initialData 와 PlaceholderData

두 옵션은 매우 비슷하게 작동한다. 둘 다 역할이

  • 캐시를 미리 채워넣기

이다. 위에서 정의했던 대로 데이터의 디폴트 값을 채워넣는데 사용할 수 있을 것 같다.

사용법

function SomeComponent() {
  const { data, status } = useQuery(['products'], fetchProducts, {
    initialData: {
      quantity: 0,
      productList: [],
    },
  });

  const { data, status } = useQuery(['products'], fetchProducts, {
    placeholderData: {
      quantity: 0,
      productList: [],
    },
  });
}

이런식으로 사용 된다. 그럼 대체 두가지의 공통점은 뭐고 차이점은 뭘까? 언제 무엇을 사용해야할까?


공통점

  • 둘 중 하나가 제공되면 쿼리가 로드 상태가 아니라 성공 상태가 된다.
  • 캐시에 이미 데이터가 있는 경우 placeholderData, initialData 는 사용되지 않는다.

차이점

1. placeholderData는 Observer Level 이고 initialData는 Cache Level 이다.

  • Observer Level 이란 뜻은, 해당 useQuery를 호출한 캐시 항목(entry)를 구독하고 있는 컴포넌트 내부에서만 공유된다는 것이다. 즉 쿼리에서 설정한 placeholderData는 전역적으로 공유되지 않고 해당 컴포넌트 내에서만 유효하다.

  • Cache Level 은 반대로 마치 캐시처럼 전역적으로 공유된다는 것이다. 따라서 쿼리에 설정한 initialData는 전역적으로 공유가 된다.

  • 이러한 특징에 따라서 placeholderData 를 사용하면 처음으로 관찰자 컴포넌트가 등록될 때 항상 Background refetch 가 실행된다. 왜냐? Placeholder Data 는 가짜 데이터 임이 명백하기 때문이다.

  • 반면 initialData 는 staleTime을 준수한다.

    • 물론 initialDataUpdatedAt 이라는 옵션을 사용 할수도 있다.

2. 쿼리 실패 시

  • placeholderData 를 설정 한 경우 해당 쿼리는 error 상태가 되고 데이터는 undefined 가 된다.

  • initialData 를 설정 한 경우 해당 쿼리는 error 상태이지만, 데이터는 initialData 로 여전히 존재한다. (캐시에 저장되므로)


그래서 언제 무엇을 사용해야 할까? (내 생각)

initialData 사용

  • 실제로 쿼리에 유효한 데이터를 채워넣을 경우. 가령 다른 쿼리의 데이터라든지..

PlaceholderData 사용

  • 쿼리에 디폴트 데이터를 채워넣어야 할 경우.
    • 왜 initialData가 아니냐면..일단 디폴트 데이터는 ‘가짜 데이터’임이 명백해야하고, 사용처별로 디폴트 데이터가 다를 수 있기 때문이다.

마무리

결국 핵심은 서버에서 받아온 데이터 가공/검증 로직을 UI 로부터 분리하자는 것이었다. 가공하는 것 역시 리액트 쿼리의 select 옵션을 사용 할 수 있을 것 같고, 검증 로직은 더 나아가서 중간에 또 다른 훅을 두거나 다른 라이브러리를 사용 할 수 있을 것 같다.!!


참고 자료

Placeholder and Initial Data in React Query | TkDodo’s blog