본문으로 건너뛰기

React-Query로 초기 값 빌려쓰기

· 약 10분

이미 호출했던 데이터의 일부를 호출할 데이터로 사용할 수 있다면? React-Query의 initialData로 지혜롭게 초기 값을 할당하는 방법을 알아봤습니다.

initialdata 이미지

React Query는 서버 상태를 관리해주는 라이브러리로, API를 호출하고 에러처리나 로딩처리를 사용하기 좋게 지원해줍니다. GET 메소드 위주의 useQuery, useQueries와 GET 메소드 외의 메소드에 사용되는 useMutation가 있습니다. 그 외에도 useInfiniteQuery, useQueryClient등 페이지네이션과 캐싱 관련한 유용한 기능을 제공하기도 합니다.

react-query에서 잠시 벗어나 우리는 데이터를 호출하고 사용할 때, 항상 부딪히는 문제가 있습니다.
바로 초기값인데요. 개발하다보면 마주하는 Cannot read properties of undefined...의 문제는 대부분 값이 정의되어 있지 않아서 혹은 데이터가 들어오기 전에 화면을 그려 에러가 발생하는 문제로 이어집니다.

해당 에러를 해결하는 방법은 사용할 데이터의 타입을 미리 알려주거나 옵셔널 체이닝으로 값이 undefined 상태일 때를 오류로 체크하지 않게 해줍니다.

저는 typeScript로 타입을 미리 선언하여 자동완성이나 초기값이 없을 때에 에러를 방지하는 방법이 좋다고 생각하는데, 사실 저는 별로 사용하지를 않았습니다. 서버에서 내려주는 데이터가 너무 많았고 타입을 미리 선언해도 타입이 종종 바뀌는 상황이 존재했기에 오히려 불편했습니다. (.. 물론 제가 잘못쓰고 있었지만요.)


속죄합니다.

고백합니다. 저는 물음표 살인마입니다.


Cannot read properties of undefined...에러가 발생하면 ?(옵셔널 체이닝)으로 해결하곤 합니다. 물론 ?를 사용했을 때의 파급효과를 잘 알고 있어서 없어도 되는 값에만 사용하곤 있습니다. 참 다행이죠..?

제 반성을 하느라 딴길로 돌아왔습니다...ㅎ 그래서 저는 해야할 것을 하기로 했습니다.

타입을 미리 정의하고, 에러를 방지하기 위해 초기 값을 미리 넣어주자

당연한 것이지만, 당연하지 못했습니다. 제가 작업하던 환경은 typeScript로 타입을 정의하고 시작하고 useQuery의 result값인 data에 초기값을 넣어주고 시작합니다.

const initialTestData: initialTestDataType = {
list: [],
...
}

const { data: testData = initialTestData } = useQuery<T>({
queryKey: ['getData', 'id'],
queryFn: () => getData(id),
})

이런식으로 사용하면 화면을 그리는데 사용할 testData에 이미 initialTestData를 넣어놨고, useQuery에 타입또한 같이 전달하니, 초기 값 에러에 대한 우려는 낮아지게 됩니다.

이런식으로 사용하면 될 줄 알았지만, react-query의 공식 문서를 읽어 보니 무언가 눈에 들어옵니다.

useQuery 공식문서

initialData

initialData? 초기 데이터? 음.. 초기 값 이미 넣어놨는데 사용해야하나 싶은 마음도 있었지만...
그래도 한 번 보기로 했습니다.

공식문서의 설명에 따르면 다음과 같습니다.

  • 선택적이고

  • 설정된 경우 이 값은 쿼리 캐시의 초기 데이터로 사용됩니다(쿼리가 아직 생성되거나 캐시되지 않은 경우).

  • 함수로 설정된 경우 공유/루트 쿼리 초기화 중에 함수가 한 번 호출되며 동기적으로 초기 데이터를 반환할 것으로 예상됩니다.

  • staleTime이 설정되지 않은 한 초기 데이터는 기본적으로 오래된 것으로 간주됩니다.

  • initialData는 캐시에 유지됩니다.


    (고맙다.. 구글 번역기야..)

    initialData 공식문서


이 부분을 참고하여, react-query가 제공하는 initialData를 그대로 사용해봤습니다.

initialData를 사용했을 때, 이점이 있을 만한 부분은 목록화면에서 목록API를 호출하고 상세 글을 클릭했을 때, 상세 API를 호출하지 않고 목록API에 속해있는 상세 데이터를 가져와 사용하는 방식이 좋다고 생각했습니다.

목록API의 데이터 일부를 재사용한다는 가정

initialData 사용법

공식문서에 따르면, 모든 곳에서 initialData를 실행하는게 아니라면 함수형으로 전달하여 쿼리가 초기화될때만 실행되게하여 메모리와 CPU자원을 절약할 수 있다고 합니다.

유저 목록
const { data: userList } = useQuery<User[]>({
queryKey: ['userList'],
queryFn: () => axios.get('/users').then((response) => response.data),
})
유저 상세
const userDetail = useQuery<any>({
queryKey: ['userdetail', userId],
queryFn: () =>
axios.get(`/users/${userId}`).then((response) => response.data),
initialData: () => {
const users = queryClient
.getQueryData<User[]>(['userList'])
?.find((user) => user.id === userId)
},
})

react-quert를 사용하면 컴포넌트 최상단에 <QueryClientProvider>로 감싸주게 됩니다. 그럼 그 아래의 컴포넌트에서는 queryKey를 통해 사용했던 데이터를 서로 주고 받거나 간섭할 수 있게 됩니다.

initialData도 마찬가지로 목록 API호출 시에 사용했던 queryKey를 사용하여, 데이터를 가져오는 방식입니다.

유저목록에서 사용했던 queryKey인 "userList"를 유저 상세의 initialData 부분에서 사용합니다. 문법을 풀이하자면.

initialData 동작과정

  1. useQueryClient의 queryClient의 getQueryData를 사용해서 쿼리키 userList로 호출했던 데이터를 찾는다.
  2. 유저 상세에서 데이터를 호출할 때, 사용하는 userId를 목록 API에 존재하지는 비교한다.
  3. userList에 userId와 일치하는 상세 데이터가 있다면 데이터를 새로 호출하지 않고 캐싱된 데이터를 사용한다.

initialData 사용결과

간단히 initialData를 사용한 화면입니다. staleTime은(캐싱시간) 3초로 설정했습니다.

결과는 다음과 같습니다.

  • 처음 상세 데이터를 클릭했을 때는 데이터를 호출하지 않고, 캐싱된 데이터를 사용한다.
  • 클릭하고 3초 후에, 캐싱시간이 끝났기에 react-query는 불러온지 오래된 데이터라고 판단하고 다시 호출한다.

내가 생각한 대략적인 사용법

사용방법을 설명하기 전에 필요한 준비물이 좀 있습니다.

준비물

  • Paste JSON as Code 설치(VS CODE, 브라우저) => JSON 파일을 자동으로 타입변환해준다.
  • react-query

Paste JSON as Code은 데이터의 초기 값을 설정해줄 때, 타입도 같이 선언하는데 이때, 용이합니다.


  1. 타입 선언 => Paste JSON as Code
  • 호출한 데이터의 값을 복사한 후, 편한 방법으로 Paste JSON as Code 복사한다.
  • (VS기준) command + shift + p후에 데이터를 넣어주면 데이터의 타입에 맞게 간단히 타입을 생성해준다.
  1. useQuery설정 , initialData 설정
  • 자주 사용하거나 공통으로 사용할 부분을 설정해준다. (에러처리, 로딩처리)
  • 개별적으로 적용해줘야 하는 기능을 설정한다. (initialData...)
  • 생성한 타입을 넣어준다.
  1. 초기값으로 defaultData 넣어준다.

추가적으로 initialData를 사용할 때, initialDataUpdatedAt, staleTime과 cacheTime(v5기준 사라짐) 등 캐싱 시간에 관련있는 부분이 있는데 이건 나중에 새로운 글에서 합쳐서 정리할려고 합니다.