React Query basic

reference: https://jforj.tistory.com/search/React%20Query


useQuery

  • useQuery๋Š” React Query๋ฅผ ์ด์šฉํ•ด ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด์˜ฌ ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

    โ€ป ๋ฐ์ดํ„ฐ ์กฐํšŒ๊ฐ€ ์•„๋‹Œ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ž‘์—…์„ ํ•  ๋•Œ๋Š” **useMutation**์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ๋น„์œ ํ•˜์ž๋ฉด select๋ฅผ ํ•  ๋•Œ ์‚ฌ์šฉ

โ€ข queryKey: queryKey์˜ ์—ญํ• ์€ React-Query๊ฐ€ query ์บ์‹ฑ์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์คŒ

โ€ข queryFn

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

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

option๊ฐ’

// 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 // ์ œํ•œ ์—†์Œ
});

refetchOnWindowFocus ์ „์—ญ์„ค์ •

  • ๋ฐฉ๋ฒ• 1

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')
);
  • ๋ฐฉ๋ฒ• 2

// 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 ์ž๋™์‹คํ–‰ ์„ค์ •

ex)

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

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 // ์ฝ”๋“œ ์ž๋™ ์‹คํ–‰ ์„ค์ •
});


useMutation

useMutation์€ React Query๋ฅผ ์ด์šฉํ•ด ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ž‘์—…์„ ์š”์ฒญํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.โ€ป ๋ฐ์ดํ„ฐ ์กฐํšŒ๋ฅผ ํ•  ๋•Œ๋Š” useQuery๋ฅผ ์‚ฌ์šฉ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ๋น„์œ ํ•˜์ž๋ฉด insert, update, delete๊ฐ€ ๋ชจ๋‘ ํฌํ•จ

์‚ฌ์šฉํ˜•ํƒœ

// 1
const savePerson = useMutation(mutationFn);

// 2
const savePerson = useMutation({
    mutationFn: mutationFn
})

mutationFn์€ mutation Function์œผ๋กœ promise ์ฒ˜๋ฆฌ๊ฐ€ ์ด๋ฃจ์–ด์ง€๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

๋‹ค๋ฅธ ๋ง๋กœ๋Š” axios๋ฅผ ์ด์šฉํ•ด ์„œ๋ฒ„์— API๋ฅผ ์š”์ฒญํ•˜๋Š” ๋ถ€๋ถ„

// 1
const savePerson = useMutation((person: Iperson) => axios.post('/savePerson', person));

// 2
const savePerson = useMutation({
    mutationFn: (person: Iperson) => axios.post('/savePerson', person)
})

mutate

mutate๋Š” useMutation์„ ์ด์šฉํ•ด ์ž‘์„ฑํ•œ ๋‚ด์šฉ๋“ค์ด ์‹คํ–‰๋  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” trigger ์—ญํ• 

์ฆ‰, useMutation์„ ์ •์˜ ํ•ด๋‘” ๋’ค ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒ๋˜์—ˆ์„ ๋•Œ mutate๋ฅผ ์‚ฌ์šฉ

const savePerson = useMutation((person: Iperson) => axios.post('<http://localhost:8080/savePerson>', person)); // useMutate ์ •์˜

    const onSavePerson = () => {
        savePerson.mutate(person); // ๋ฐ์ดํ„ฐ ์ €์žฅ
    }

<Person.SaveButton onClick={onSavePerson}>์ €์žฅ</Person.SaveButton>

onSuccess, onError, onSettled

์ผ๋ฐ˜์ ์œผ๋กœ ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์š”์ฒญ์„ ํ•˜๊ฒŒ ๋˜๋ฉด ๋ณ€๊ฒฝ ์š”์ฒญ์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ์— ์ถ”๊ฐ€์ ์ธ ์•ก์…˜์„ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ.

์ด๋Ÿฐ ์ƒํ™ฉ์€ useMutation์„ ์‚ฌ์šฉํ•  ๋•Œ๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ.

async/await์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๋ณดํ†ต ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฒฐ๊ด๊ฐ’์ด ์žˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•œ ๋’ค ์ถ”๊ฐ€ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ

try {
    const res = await axios.post('<http://localhost:8080/savePerson>', person);

    if(res) {
        console.log('success');
    }
} catch(error) {
    console.log('error');
} finally {
    console.log('finally');
}

useMutation ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉ

// 1
const savePerson = useMutation((person: Iperson) => axios.post('<http://localhost:8080/savePerson>', person), {
    onSuccess: () => { // ์š”์ฒญ์ด ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ
        console.log('onSuccess');
    },
    onError: (error) => { // ์š”์ฒญ์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ๋œ ๊ฒฝ์šฐ
        console.log('onError');
    },
    onSettled: () => { // ์š”์ฒญ์ด ์„ฑ๊ณตํ•˜๋“ , ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ๋˜๋“  ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ
        console.log('onSettled');
    }
});

// 2
const savePerson = useMutation({
    mutationFn: (person: Iperson) => axios.post('/savePerson', person),
    onSuccess: () => { // ์š”์ฒญ์ด ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ
        console.log('onSuccess');
    },
    onError: (error) => { // ์š”์ฒญ์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ๋œ ๊ฒฝ์šฐ
        console.log('onError');
    },
    onSettled: () => { // ์š”์ฒญ์ด ์„ฑ๊ณตํ•˜๋“ , ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ๋˜๋“  ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ
        console.log('onSettled');
    }
})

onSettled๋Š” finally ๊ตฌ๋ฌธ์ฒ˜๋Ÿผ ์š”์ฒญ์ด ์„ฑ๊ณตํ•˜๋“  ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ๋˜๋“  ์ƒ๊ด€์—†์ด ๋งˆ์ง€๋ง‰์— ์‹คํ–‰๋˜๋Š” ๊ตฌ๊ฐ„

onSuccess, onError, onSettled๋Š” useMutation์„ ์ •์˜ํ•  ๋•Œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ mutate์—์„œ๋„ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

const onSavePerson = () => {
    savePerson.mutate(person, {
        onSuccess: () => { // ์š”์ฒญ์ด ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ
            console.log('onSuccess');
        },
        onError: (error) => { // ์š”์ฒญ์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ๋œ ๊ฒฝ์šฐ
            console.log('onError');
        },
        onSettled: () => { // ์š”์ฒญ์ด ์„ฑ๊ณตํ•˜๋“ , ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ๋˜๋“  ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ
            console.log('onSettled');
        }
    }); // ๋ฐ์ดํ„ฐ ์ €์žฅ
}

invalidateQueries

  • useQuery์—์„œ ์‚ฌ์šฉ๋˜๋Š” queryKey์˜ ์œ ํšจ์„ฑ์„ ์ œ๊ฑฐํ•ด์ฃผ๋Š” ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉ

  • queryKey์˜ ์œ ํšจ์„ฑ์„ ์ œ๊ฑฐํ•ด์ฃผ๋Š” ์ด์œ ๋Š” ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด์˜ค๊ธฐ ์œ„ํ•จ

  • ์ •ํ•ด์ง„ ์‹œ๊ฐ„์ด ๋„๋‹ฌํ•˜์ง€ ์•Š์œผ๋ฉด ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๊ฐ€ ์ ์žฌ๋˜์—ˆ๋”๋ผ๋„ useQuery๋Š” ๋ณ€๋™ ์—†์ด ๋™์ผํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํ™”๋ฉด์— ๋ณด์—ฌ์ค„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  • ๊ฒฐ๊ตญ ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ๋Š” ๋ฐ์ดํ„ฐ ์ƒ์„ฑ์ด ์ œ๋Œ€๋กœ ๋˜์—ˆ๋Š”์ง€์— ๋Œ€ํ•œ ํŒŒ์•…์ด ํž˜๋“ค๊ธฐ ๋•Œ๋ฌธ์— ํ˜ผ๋ž€์„ ๊ฒช์„ ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  • ํ•ด๋‹น ์ƒํ™ฉ์„ ํ•ด๊ฒฐํ•ด์ค„ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด ๋ฐ”๋กœ invalidateQueries์ž…๋‹ˆ๋‹ค.

  • ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ๋•Œ invalidateQueries๋ฅผ ์ด์šฉํ•ด useQuery๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๋˜ queryKey์˜ ์œ ํšจ์„ฑ์„ ์ œ๊ฑฐํ•ด์ฃผ๋ฉด ์บ์‹ฑ๋˜์–ด์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ํ™”๋ฉด์— ๋ณด์—ฌ์ฃผ์ง€ ์•Š๊ณ  ์„œ๋ฒ„์— ์ƒˆ๋กญ๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  • ๊ฒฐ๊ตญ ๋ฐ์ดํ„ฐ๊ฐ€ ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์„ ๋•Œ ๋‹ค์‹œ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฒŒ ๋˜๋ฉด์„œ ์ถ”๊ฐ€ํ•œ ๋ฐ์ดํ„ฐ๊นŒ์ง€ ํ™”๋ฉด์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

const queryClient = useQueryClient();  // ๋“ฑ๋ก๋œ quieryClient ๊ฐ€์ ธ์˜ค๊ธฐ

const savePerson = useMutation((person: Iperson) => axios.post('<http://localhost:8080/savePerson>', person), {
    onSuccess: () => { // ์š”์ฒญ์ด ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ
        console.log('onSuccess');
        queryClient.invalidateQueries('persons'); // queryKey ์œ ํšจ์„ฑ ์ œ๊ฑฐ
    },
    onError: (error) => { // ์š”์ฒญ์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ๋œ ๊ฒฝ์šฐ
        console.log('onError');
    },
    onSettled: () => { // ์š”์ฒญ์ด ์„ฑ๊ณตํ•˜๋“ , ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ๋˜๋“  ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ
        console.log('onSettled');
    }
}); // useMutate ์ •์˜

setQueryData

  • invalidateQueries๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ ๋„ ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•ด์ค„ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์€ ์žˆ์Šต๋‹ˆ๋‹ค.

    setQueryData๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋˜๋Š”๋ฐ setQueryData๋Š” ๊ธฐ์กด์— queryKey์— ๋งคํ•‘๋˜์–ด ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ƒˆ๋กญ๊ฒŒ ์ •์˜ํ•ด์ค๋‹ˆ๋‹ค.

const queryClient = useQueryClient();  // ๋“ฑ๋ก๋œ quieryClient ๊ฐ€์ ธ์˜ค๊ธฐ

const savePerson = useMutation((person: Iperson) => axios.post('<http://localhost:8080/savePerson>', person), {
    onSuccess: () => { // ์š”์ฒญ์ด ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ
        console.log('onSuccess');
        queryClient.setQueryData('persons', (data) => {
            const curPersons = data as AxiosResponse<any, any>; // persons์˜ ํ˜„์žฌ ๋ฐ์ดํ„ฐ ํ™•์ธ
            curPersons.data.push(person); // ๋ฐ์ดํ„ฐ push

            return curPersons; // ๋ณ€๊ฒฝ๋œ ๋ฐ์ดํ„ฐ๋กœ set
        })
    },
    onError: (error) => { // ์š”์ฒญ์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ๋œ ๊ฒฝ์šฐ
        console.log('onError');
    },
    onSettled: () => { // ์š”์ฒญ์ด ์„ฑ๊ณตํ•˜๋“ , ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ๋˜๋“  ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ
        console.log('onSettled');
    }
}); // useMutate ์ •์˜

useQueries

useQueries๋Š” React Query์—์„œ useQuery์˜ ๋™์  ๋ณ‘๋ ฌ ์ฟผ๋ฆฌ ์ž‘์—…์„ ์œ„ํ•ด ์‚ฌ์šฉ

useQueries์˜ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์€ ๋‹จ์ˆœํ•˜๊ฒŒ useQuery๋ฅผ ๋ฐฐ์—ด๋กœ ๋„ฃ์–ด์ค€๋‹ค

const ress = useQueries([
    useQuery1,
    useQuery2,
    useQuery3,
    ...
]);
const res1 = useQuery(['persons'], () => axios.get('<http://localhost:8080/persons>'), {
});
const res2 = useQuery(['person'], () => axios.get('<http://localhost:8080/person>', {
    params: {
        id: 1
    }
}));
const res = useQueries([
    {
        queryKey: ['persons'],
        queryFn: () => axios.get('<http://localhost:8080/persons>'),
    },
    {
        queryKey: ['person'],
        queryFn: () => axios.get('<http://localhost:8080/person>', {
            params: {
                id: 1
            }
        }),
    }
]);

useQuery๋ณด๋‹ค useQueries๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์€ ๋™์ ์œผ๋กœ ๋ณ€ํ™”ํ•˜๋Š” ์ƒํ™ฉ

const getPersons = (persons: Iperson[]) => {
  const res = useQueries(
    persons.map((person) => {
      return {
        queryKey: ['person', person.id],
        queryFn: () =>
          axios.get('<http://localhost:8080/person>', {
            params: {
              id: person.id,
            },
          }),
      };
    })
  );

  return (
    <Person.Container>
      {ress &&
        ress.map((res) => {
          const person: Iperson = res.data && res.data.data;
          return (
            person && (
              <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>
  );
};

Suspense

React Query์˜ suspense ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•˜๊ฒŒ ๋˜๋ฉด useQuery์˜ status, error ๋“ฑ์„ React.Suspense๋กœ ๋Œ€์ฒด

// App.tsx

import * as React from 'react';
import Queries from './pages/queries';

const App = (): JSX.Element => {
  return <Queries />;
};

export default App;
// queries.tsx

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

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

const Queries = (): JSX.Element => {
  const getPersons = () => {
    const res = useQuery(['persons'], () =>
      axios.get('<http://localhost:8080/persons>')
    );

    // ๋กœ๋”ฉ ์ค‘์ผ ๊ฒฝ์šฐ
    if (res.isLoading) {
      return <LoadingText>Queries Loading...</LoadingText>;
    }

    // ๊ฒฐ๊ณผ๊ฐ’์ด ์ „๋‹ฌ๋˜์—ˆ์„ ๊ฒฝ์šฐ
    const persons: Iperson[] = res.data && res.data.data;

    return (
      <Person.Container>
        {persons &&
          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 Queries;

// styled
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;
  `,
};

React.Suspense๋ฅผ ์ด์šฉํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝ

์ฒซ ๋ฒˆ์งธ๋Š” App.tsx์˜ Queries Component๊ฐ€ React.Suspense๋กœ ๋ฎ์—ฌ์žˆ๋‹ค๋Š” ๊ฒƒ

๋‘ ๋ฒˆ์งธ๋Š” queries.tsx์˜ useQuery์— suspense ๋ชจ๋“œ ์„ค์ •์ด ์ถ”๊ฐ€

์„ธ ๋ฒˆ์งธ๋Š” queries.tsx์—์„œ ๋กœ๋”ฉ ์ค‘์ผ ๊ฒฝ์šฐ์— ๋Œ€ํ•œ ์ฝ”๋“œ๊ฐ€ ์‚ญ์ œ

// App.tsx

import * as React from 'react';
import Queries from './pages/queries';

const App = (): JSX.Element => {
  return (
    <React.Suspense fallback={<div>App Loading...</div>}>
      <Queries />
    </React.Suspense>
  );
};

export default App;
// queries.tsx

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

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

const Queries = (): JSX.Element => {
  const getPersons = () => {
    const res = useQuery(
      ['persons'],
      () => axios.get('<http://localhost:8080/persons>'),
      {
        suspense: true, // suspense ๋ชจ๋“œ ์„ค์ •
      }
    );

    // ๊ฒฐ๊ณผ๊ฐ’์ด ์ „๋‹ฌ๋˜์—ˆ์„ ๊ฒฝ์šฐ
    const persons: Iperson[] = res.data && res.data.data;

    return (
      <Person.Container>
        {persons &&
          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 Queries;

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

  margin: 0 auto;
`;

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;
  `,
};

suspense ๋ชจ๋“œ์—์„œ useQueries๋ฅผ ์‚ฌ์šฉ

suspense ๋ชจ๋“œ์—์„œ useQueries๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ์ด์œ ๋Š” useQuery๋ฅผ ๋ณ‘๋ ฌ๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์„ ๋•Œ ๋งŒ์•ฝ ํ•˜๋‚˜์˜ useQuery๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘๋˜์ง€ ์•Š์„ ๊ฒฝ์šฐ ๊ทธ ์ดํ›„์— ์‹คํ–‰๋  useQuery์— ์˜ํ–ฅ ์„ ๋ฏธ์น˜๋ฉฐ ๊ฒฐ๊ณผ์ ์œผ๋กœ ์˜ฌ๋ฐ”๋ฅธ ํ™”๋ฉด์ด ๋ณด์ด์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ

์ฒซ ๋ฒˆ์งธ ์š”์†Œ๋Š” ๊ฑธ๋Ÿฌ์ง€๊ณ  ๋‘ ๋ฒˆ์งธ ์š”์†Œ์— ํ•ด๋‹นํ•˜๋Š” ๊ฒฐ๊ณผ๋งŒ ํ™”๋ฉด์— ๋…ธ์ถœ

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

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

const Queries = (): JSX.Element => {
  const getPersons = () => {
    const res = useQueries([
      {
        queryKey: ['persons'],
        queryFn: () => axios.get('<http://localhost:8080/personssss>'), // ์˜ค๋ฅ˜
        suspense: true,
      },
      {
        queryKey: ['person'],
        queryFn: () =>
          axios.get('<http://localhost:8080/person>', {
            params: {
              id: 1,
            },
          }),
        suspense: true,
      },
    ]);

    if (res) {
      const persons: Iperson[] = res[0].data && res[0].data.data;
      const person: Iperson = res[1].data && res[1].data.data;

      return (
        <Person.Container>
                  {persons && 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 && (
                    <Person.Box>
                        <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 Queries;

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

  margin: 0 auto;
`;

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;
  `,
};

useInfiniteQuery

useInfiniteQuery๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’๋งŒ ๋ณ€๊ฒฝํ•˜์—ฌ ๋™์ผํ•œ useQuery๋ฅผ ๋ฌดํ•œ์ • ํ˜ธ์ถœํ•  ๋•Œ ์‚ฌ์šฉ

const res = useInfiniteQuery(queryKey, queryFn);

  • pageParam

pageParam์€ useInfiniteQuery๊ฐ€ ํ˜„์žฌ ์–ด๋–ค ํŽ˜์ด์ง€์— ์žˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’

const res = useInfiniteQuery(['infinitePerson'], ({ pageParam = 5 }) =>
  axios.get('<http://localhost:8080/person>', {
    params: {
      id: pageParam,
    },
  })
);

๊ธฐ๋ณธ ๊ฐ’์€ undefined์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ’์ด ์—†์„ ๊ฒฝ์šฐ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ 5 ์„ค์ •

๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด์˜ฌ ๋•Œ pageParam๊ฐ’์„ api ์š”์ฒญํ•  ๋•Œ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์œผ๋กœ ๋„ฃ์–ด ์‚ฌ์šฉ

  • getNextPageParam๊ณผ fetchNextPage

getNextPageParam๊ณผ fetchNextPage์€ ๊ณตํ†ต์ ์œผ๋กœ ๋‹ค์Œ ํŽ˜์ด์ง€์— ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด์˜ฌ ๋•Œ ์‚ฌ์šฉ

  • getNextPageParam์€ ๋‹ค์Œ api๋ฅผ ์š”์ฒญํ•  ๋•Œ ์‚ฌ์šฉ๋  pageParam๊ฐ’์„ ์ •ํ•  ์ˆ˜ ์žˆ๋‹ค

const res = useInfiniteQuery(
  ['infinitePerson'],
  ({ pageParam = 5 }) =>
    axios.get('<http://localhost:8080/person>', {
      params: {
        id: pageParam,
      },
    }),
  {
    getNextPageParam: (lastPage, allPages) => {
      return lastPage.data.id + 1; // ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ์‚ฌ์šฉ ๋  pageParam
    },
  }
);

lastPage๋Š” useInfiniteQuery๋ฅผ ์ด์šฉํ•ด ํ˜ธ์ถœ๋œ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์— ์žˆ๋Š” ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

allPages๋Š” useInfiniteQuery๋ฅผ ์ด์šฉํ•ด ํ˜ธ์ถœ๋œ ๋ชจ๋“  ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ์˜๋ฏธ

return ๋˜๋Š” ๊ฐ’์ด ๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ pageParam ๊ฐ’

  • fetchNextPage๋Š” ๋‹ค์Œ ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ์‚ฌ์šฉ

useInfiniteQuery์˜ return ๊ฐ’์— ํฌํ•จ๋˜๋ฉฐ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฒ„ํŠผ์„ ํด๋ฆญํ•  ๋•Œ ์‹คํ–‰๋  ์ด๋ฒคํŠธ๋กœ ๋“ฑ๋กํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค

const getPersons = () => {
  const res = useInfiniteQuery(
    ['infinitePerson'],
    ({ pageParam = 5 }) =>
      axios.get('<http://localhost:8080/person>', {
        params: {
          id: pageParam,
        },
      }),
    {
      getNextPageParam: (lastPage, allPages) => {
        return lastPage.data.id + 1; // ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ์‚ฌ์šฉ ๋  pageParam
      },
    }
  );

  // ๋กœ๋”ฉ ์ค‘์ผ ๊ฒฝ์šฐ
  if (res.isLoading) {
    return <LoadingText>Loading...</LoadingText>;
  }

  // ๊ฒฐ๊ณผ๊ฐ’์ด ์ „๋‹ฌ๋˜์—ˆ์„ ๊ฒฝ์šฐ
  if (res.data) {
    return (
      <Person.Container>
        {res.data.pages.map((page) => {
          const person: Iperson = page.data;

          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.NextButton onClick={() => res.fetchNextPage()}>
          Next
        </Person.NextButton>{' '}
        {/* ํด๋ฆญ ์‹œ ๋‹ค์Œ ํŽ˜์ด์ง€ ํ˜ธ์ถœ */}
      </Person.Container>
    );
  }
};

Infinite Scroll

import axios from 'axios';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';

// info interface
interface Info {
  name: string;
  phone: string;
  age: number;
}

const App = (): JSX.Element => {
  // state
  const [infoArray, setInfoArray] = useState<Info[]>([]);

  // ref
  const observerRef = useRef<IntersectionObserver>();
  const boxRef = useRef<HTMLDivElement>(null);

  // useEffect
  useEffect(() => {
    getInfo();
  }, []);

  useEffect(() => {
    observerRef.current = new IntersectionObserver(intersectionObserver); // IntersectionObserver
    boxRef.current && observerRef.current.observe(boxRef.current);
  }, [infoArray]);

  // function
  const getInfo = async () => {
    const res = await axios.get('<http://localhost:8080/rest/getInfo>'); // ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    setInfoArray((curInfoArray) => [...curInfoArray, ...res.data]); // state์— ์ถ”๊ฐ€

    // console.log('info data add...');
  };

  // IntersectionObserver ์„ค์ •
  const intersectionObserver = (
    entries: IntersectionObserverEntry[],
    io: IntersectionObserver
  ) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        // ๊ด€์ฐฐํ•˜๊ณ  ์žˆ๋Š” entry๊ฐ€ ํ™”๋ฉด์— ๋ณด์—ฌ์ง€๋Š” ๊ฒฝ์šฐ
        io.unobserve(entry.target); // entry ๊ด€์ฐฐ ํ•ด์ œ
        getInfo(); // ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
      }
    });
  };

  // style
  const Wrapper = {
    width: '800px',
    margin: '0 auto',
  };

  const Box = {
    border: '1px solid olive',
    borderRadius: '8px',
    boxShadow: '1px 1px 2px olive',
    margin: '18px 0',
  };

  const BoxTable = {
    borderSpacing: '15px',
  };

  const Title = {
    fontWeight: 700,
  };

  return (
    <div style={Wrapper}>
      {infoArray.map((info, index) => {
        if (infoArray.length - 5 === index) {
          // ๊ด€์ฐฐ๋˜๋Š” ์š”์†Œ๊ฐ€ ์žˆ๋Š” html, ์•„๋ž˜์—์„œ 5๋ฒˆ์งธ์— ํ•ด๋‹นํ•˜๋Š” ๋ฐ•์Šค๋ฅผ ๊ด€์ฐฐ
          return (
            <div style={Box} ref={boxRef} key={index}>
              <table style={BoxTable}>
                <tbody>
                  <tr>
                    <td style={Title}>์ด๋ฆ„</td>
                    <td>{info.name}</td>
                  </tr>

                  <tr>
                    <td style={Title}>์ „ํ™”๋ฒˆํ˜ธ</td>
                    <td>{info.phone}</td>
                  </tr>

                  <tr>
                    <td style={Title}>๋‚˜์ด</td>
                    <td>{info.age}</td>
                  </tr>
                </tbody>
              </table>
            </div>
          );
        }
        // ๊ด€์ฐฐ๋˜๋Š” ์š”์†Œ๊ฐ€ ์—†๋Š” html
        return (
          <div style={Box} key={index}>
            <table style={BoxTable} key={index}>
              <tbody>
                <tr>
                  <td style={Title}>์ด๋ฆ„</td>
                  <td>{info.name}</td>
                </tr>

                <tr>
                  <td style={Title}>์ „ํ™”๋ฒˆํ˜ธ</td>
                  <td>{info.phone}</td>
                </tr>

                <tr>
                  <td style={Title}>๋‚˜์ด</td>
                  <td>{info.age}</td>
                </tr>
              </tbody>
            </table>
          </div>
        );
      })}
    </div>
  );
};

export default App;

Last updated