React-Query 의 initialData와 PlaceholderData 사용하기
문제정의
서버에서 데이터가 아직 도착하지 않았을 때 우리는 보통 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 옵션을 사용 할 수 있을 것 같고, 검증 로직은 더 나아가서 중간에 또 다른 훅을 두거나 다른 라이브러리를 사용 할 수 있을 것 같다.!!