SPA/React

[React] React Query의 useQuery에 대해 알아보기

J4J 2022. 3. 8. 19:58
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 React Query의 useQuery에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

들어가기에 앞서

 

react query의 기능 업데이트가 주기적으로 이루어졌고 최근 v5로 넘어오면서 여러 가지 변경점들이 발생되었습니다.

 

최근 변경점에 대해서도 확인하고 싶으신 분들은 아래 글들을 참고 부탁드립니다.

 

[React] react-query v5 변경점 알아보기 (1) - 공통 기능 변경

[React] react-query v5 변경점 알아보기 (2) - query 공통 기능 변경

 

 

 

useQuery란?

 

useQuery는 React Query를 이용해 서버로부터 데이터를 조회해올 때 사용합니다.

 ※ 데이터 조회가 아닌 데이터 변경 작업을 할 때는 useMutation을 사용합니다.

 

데이터베이스로 비유하자면 select를 할 때 사용된다고도 말씀드릴 수 있습니다.

 

 

 

useQuery를 코드로 작성하여 구현하기 위해서는 다음 2가지 개념을 알고 있어야 합니다.

 

  • queryKey
  • queryFn

 

 

 

또한 다음과 같은 형태로 사용됩니다.

 

// 1
const res = useQuery(queryKey, queryFn);

// 2
const res = useQuery({
    queryKey: queryKey,
    queryFn: queryFn
});

 

 

반응형

 

 

queryKey란?

 

먼저 queryKey는 useQuery마다 부여되는 고유 Key 값입니다.

 

해당 Key 값은 단순하게 문자열로 사용될 수도 있고 또한 배열의 형태로도 사용될 수 있습니다.

 

그렇기 때문에 실제로 사용될 때 다음과 같은 방식으로 코드가 작성됩니다.

 

// 문자열
const res = useQuery('persons', queryFn);

// 배열 - 1
const res = useQuery(['persons'], queryFn);

// 배열 - 2
const res = useQuery(['persons', 'add Id'], queryFn);

// 배열 - 3
const res = useQuery(['add Id', 'persons'], queryFn);

// 배열 - 4
const res = useQuery(['persons', {type: 'add', name: 'Id'}], queryFn);

 

 

 

"문자열"은 현재 'persons'라는 문자열이 queryKey로 사용되고 있습니다.

 

React Query는 이렇게 문자열로 작성된 경우에는 자동으로 길이가 1인 배열로 인식하기 때문에 결과적으로 "배열 - 1"과 동일한 queryKey로 작성되었다는 것을 알 수 있습니다.

 

 

 

또한 "배열 - 2"와 "배열 - 3"은 queryKey가 동일해 보이지만 React Query에게는 동일하지 않은 queryKey로 인식됩니다.

 

왜냐하면 queryKey가 할당될 때 배열에 입력되는 순서도 보장해주기 때문입니다.

 

 

 

 

queryKey 역할

 

queryKey의 역할은 React Query가 query 캐싱을 관리할 수 있도록 도와줍니다.

 

정말 간단하게 다음과 같은 예시가 있습니다.

 

import * as React from 'react';
import axios from 'axios';
import { useQuery } from 'react-query';

const Query = (): JSX.Element => {

    const getPersons1 = () => {
        const res1 = useQuery(['persons'], queryFn1);
    }
    
    const getPersons2 = () => {
        const res2 = useQuery(['persons'], queryFn2);
    }

    return (
        <div>
            {getPersons1()}
            {getPersons2()}
        </div>
    )
}

export default Query;

 

 

 

res1과 res2가 동일한 queryKey를 사용하며 서버에 있는 데이터를 조회해오려고 하고 있습니다.

 

일반적인 상황에서는 res1과 res2에 대한 모든 요청이 이루어지게 되므로 서버에 2개의 request가 전달될 것입니다.

 

하지만 해당 코드에서는 서버에 1개의 request만 전달이 됩니다.

 

왜냐하면 res1에서 request를 서버에 전달하게 되면 res2에서는 이미 동일한 queryKey에 대한 결괏값이 있기 때문에 추가 요청을 하지 않고 res1의 결과를 그대로 가져와 사용하기 때문입니다. 

 

 

 

또한 queryFn에 대해서는 아직 말씀을 드리지 않았지만 queryFn이 다르게 정의되어 있더라도 res2에서는 res1의 결과를 그대로 전달받기 때문에 queryFn1이 처리된 결과를 확인할 수 있습니다.

 

결국 위의 코드는 다음의 코드와 동일한 결과를 만든다고도 말씀드릴 수 있습니다.

 

import * as React from 'react';
import axios from 'axios';
import { useQuery } from 'react-query';

const Query = (): JSX.Element => {

    const getPersons1 = () => {
        const res1 = useQuery(['persons'], queryFn1);
    }
    
    const getPersons2 = () => {
        const res2 = useQuery(['persons']);
    }

    return (
        <div>
            {getPersons1()}
            {getPersons2()}
        </div>
    )
}

export default Query;

 

 

 

 

queryFn란?

 

queryFn은 query Function으로 promise 처리가 이루어지는 함수라고 생각하면 됩니다.

 

다른 말로는 axios를 이용해 서버에 API 요청하는 코드라고 생각할 수 있습니다.

 

그렇기 때문에 useQuery는 결과적으로 다음과 같은 형태로 코드가 작성됩니다.

 

// 1
const res = useQuery(['persons'], () => axios.get('http://localhost:8080/persons'));

// 2
const res = useQuery({
    queryKey: ['persons'],
    queryFn: () => axios.get('http://localhost:8080/persons')
});

 

 

 

 

staleTime과 cacheTime

 

useQuery에 대해 사용 방법을 알았으니 다음과 같이 간단하게 코드를 작성해보겠습니다.

 

import * as React from 'react';
import axios from 'axios';
import styled from 'styled-components';
import { useQuery } from 'react-query';

interface Iperson {
    id: number;
    name: string;
    phone: string;
    age: number;
}

const Query = (): JSX.Element => {

    const getPersons = () => {
        const res = useQuery(['persons'], () => axios.get('http://localhost:8080/persons')); // API 호출

        // 로딩 중일 경우
        if(res.isLoading) {
            return (
                <LoadingText>Loading...</LoadingText>
            )
        }

        // 결과값이 전달되었을 경우
        if(res.data) {
            const persons: Iperson[] = res.data.data;

            return (
                <Person.Container>
                    {persons.map((person) => {
                        return (
                            <Person.Box key={person.id}>
                                <Person.Title>{person.id}.</Person.Title>
                                <Person.Text>{person.name}</Person.Text>
                                <Person.Text>({person.age})</Person.Text>
                            </Person.Box>
                        )
                    })}
                </Person.Container>
            )
        }
    }

    return (
        <Wrapper>
            {getPersons()}
        </Wrapper>
    )
}

export default Query;

const Wrapper = styled.div`
    max-width: 728px;

    margin: 0 auto;
`;

const LoadingText = styled.h3`
    text-align: center;
`;

const Person = {
    Container: styled.div`
        padding: 8px;
    `,

    Box: styled.div`
        border-bottom: 2px solid olive;
    `,

    Title: styled.h2`
        display: inline-block;
        
        margin: 0 12px;

        line-height: 48px;
    `,

    Text: styled.span`
        margin: 0 6px;
    `
}

 

 

 

서버를 실행시킨 뒤 작성한 파일을 화면에 띄우면 다음과 같습니다.

 

코드 실행

 

 

 

여기서 확인해야 되는 부분은 Network탭에 persons를 호출하는 것입니다.

 

Network탭을 계속 확인해 보시면 단순히 페이지 전환만 했음에도 불구하고 persons를 지속적으로 호출하고 있습니다.

 

지속적인 서버 호출

 

 

 

이처럼 호출이 지속적으로 발생되는 이유는 자동으로 refetch가 이루어지고 있기 때문입니다.

 

그리고 refetch가 발생되는 이유는 해당 queryKey에 매핑되는 데이터가 fresh하지 않고 stale 해졌기 때문입니다.

 

 

 

 

stale은 다음과 같은 의미를 가집니다.

 

stale 의미

 

 

 

stale은 탁한, 신선하지 않은 이라는 의미를 가지는데 데이터가 stale 해졌다는 것은 결국 오래된 데이터라고 생각할 수 있습니다.

 

stale 한 데이터를 사용자에게 보여주는 것은 유의미하지 않다고 React Query는 판단하고 fresh 한 데이터를 요구하게 됩니다.

 

결국 서버로부터 fresh한 데이터를 전달받기 위해 refetch가 이루어집니다.

 

 

 

하지만 여기서 개발자는 데이터가 stale 하다고 생각하지 않을 수도 있습니다.

 

그럼에도 불구하고 React Query는 계속해서 refetch를 수행합니다.

 

왜냐하면 default값으로 staleTime은 0초이기 때문입니다.

 

한 번 데이터를 조회해오면 그 순간 바로 해당 데이터는 stale한 데이터이기 때문에 refetch가 계속해서 발생되고 있는 것입니다.

 

 

 

이런 staleTime과 유사한 역할을 수행하는 것에는 cacheTime이 있습니다.

 

cacheTime은 말 그대로 캐싱 처리가 이루어지는 시간을 의미합니다.

 

cacheTime은 default값으로 5분으로 설정되어 있습니다.

 

그렇기 때문에 queryKey에 매핑되는 데이터가 사용되지 않는 시점을 기준으로 5분이 지나지 않은 상태에서 해당 queryKey를 다시 호출할 경우 이전에 가져왔던 데이터를 다시 보여주게 됩니다.

 

하지만 5분이 지나게 되면 캐시 가비지 콜렉터 타이머가 실행되며 기존 데이터는 삭제 처리가 이루어지고 queryKey를 다시 호출하게 되면 서버에 다시 데이터를 요청하게 됩니다.

 

 

 

즉, useQuery에는 staleTime, cacheTime 두 개념이 모두 존재하기 때문에 둘 중 하나라도 만족되지 않으면 서버에 다시 데이터를 요청하게 됩니다.

 

그렇기 때문에 두 설정을 모두 고려하며 코드를 구현해야 될 것이라고 여겨집니다.

 

 

 

지금까지 말한 staleTime, cacheTime을 설정하는 방법은 useQuery를 작성할 때 함께 해줄 수 있습니다.

 

다음과 같이 시간을 설정해줄 수 있고 작성되는 시간은 ms 단위이기 때문에 1000 = 1초라고 생각하시면 됩니다.

 

// 1
const res = useQuery(['persons'], () => axios.get('http://localhost:8080/persons'), {
    staleTime: 5000, // 5초
    cacheTime: Infinity, // 제한 없음
});

// 2
const res = useQuery({
    queryKey: ['persons'],
    queryFn: () => axios.get('http://localhost:8080/persons'),
    staleTime: 5000, // 5초
    cacheTime: Infinity // 제한 없음
});

 

 

 

 

refetch window focus 설정

 

staleTime에 대한 개념을 알게 되었더라도 조금 낯선 부분은 단순 페이지 전환만으로 refetch가 이루어진다는 것입니다.

 

이런 동작이 수행되는 이유는 default로 window focus 설정이 true로 되어 있기 때문입니다.

 

 

 

어떤 상황에서는 이런 기능이 더 효율적으로 사용될 수는 있지만 또 다른 상황에서는 필요 없는 기능일 수도 있습니다.

 

그럴 때는 window focus 설정을 false로 변경하여 staleTime이 지났더라도 focus가 다시 되는 것만으로 refetch가 발생되지 않게 설정해줄 수 있습니다.

 

참고적으로 window focus 설정이 false로 변경된 후 staleTime이 지났을 때 refetch 되는 경우로는 다른 화면으로 이동되었다가 다시 현재 화면으로 되돌아오는 케이스가 있습니다.

 

 

 

설정하는 방법은 2가지가 있습니다.

 

첫 번째는 전역적으로 설정하는 방법입니다.

 

개발된 App에 QueryClient를 제공하기 위해 다음과 같이 설정된 코드가 있을 겁니다.

 

import * as React from 'react';
import ReactDom from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';

const queryClient = new QueryClient(); // queryClient 생성

ReactDom.render(
    // App에 QueryClient 제공
    <QueryClientProvider client={queryClient}>
        <App />
    </QueryClientProvider>, 
    document.querySelector('#root')
);

 

 

 

여기서 QueryClient를 생성할 때 다음과 같이 window focus 설정을 false로 해주면 전역적으로 적용이 될 수 있습니다.

 

import * as React from 'react';
import ReactDom from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';

const queryClient = new QueryClient(
    {
        defaultOptions: {
            queries: {
                refetchOnWindowFocus: false, // window focus 설정
            }
        }
    }
); // queryClient 생성

ReactDom.render(
    // App에 QueryClient 제공
    <QueryClientProvider client={queryClient}>
        <App />
    </QueryClientProvider>, 
    document.querySelector('#root')
);

 

 

 

두 번째는 useQuery마다 설정하는 방법입니다.

 

useQuery를 생성할 때 다음과 같이 각각 window focus 설정을 변경해줄 수 있습니다.

 

// 1
const res = useQuery(['persons'], () => axios.get('http://localhost:8080/persons'), {
    refetchOnWindowFocus: false // window focus 설정
});

// 2
const res = useQuery({
    queryKey: ['persons'],
    queryFn: () => axios.get('http://localhost:8080/persons'),
    refetchOnWindowFocus: false // window focus 설정
});

 

 

 

 

query 자동 실행 설정

 

코드를 구현하다 보면 다음과 같이 조건이 맞을 때만 서버에 데이터 요청을 하고 조건이 맞지 않으면 코드 실행을 막는 경우가 있습니다.

 

if(id) {
    const res = axios.get('http://localhost:8080/person', {
        params: {
            id: id,
        }
    })
}

 

 

 

useQuery를 사용할 때도 당연히 동일한 상황이 생길 수가 있습니다.

 

하지만 useQuery에서는 if문을 사용하지 않고 useQuery에서 제공해주는 query 자동 실행 설정을 통해 동일한 결과를 만들어 줄 수 있습니다.

 

위와 동일한 결과가 나오는 코드를 다음과 같이 작성해줄 수 있습니다.

 

// 1
const res = useQuery(['person', id], () => axios.get('http://localhost:8080/person', {
    params: {
        id: id,
    }
}), {
    enabled: !!id // 코드 자동 실행 설정
});

// 2
const res1 = useQuery({
    queryKey: ['person', id],
    queryFn: () => axios.get('http://localhost:8080/person', {
        params: {
            id: id,
        }
    }),
    enabled: !!id // 코드 자동 실행 설정
});

 

 

 

enabled의 default 값은 true로 되어 있습니다.

 

하지만 위와 같이 id값이 존재하지 않을 경우 false를 변경해줌으로 써 자동 실행을 막을 수 있게 도와줍니다.

 

 

 

 

 

 

 

 

이상으로 React Query의 useQuery에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형