리액트

[React] Reate Query와 Suspense

코딩하는둥이 2025. 4. 15. 14:01

Query란? 

Query는 비동기 데이터 소스에 대한 선언적 의존성을 의미하며, React Query에서 데이터를 가져오고 관리하기 위해 사용됩니다. 각 Query는 고유한 키(queryKey)와 데이터를 가져오는 함수(queryFn)를 필요로 합니다.

 

핵심 역할

 데이터를 서버에서 가져오고(fetching)

캐싱하여 재사용하며

데이터의 상태를 관리합니다(예: 로딩, 성공, 실패)

 

useQuery 사용법

queryKey: 쿼리를 고유하게 식별하는 키 (문자열 또는 배열 형태). 

queryFn: 데이터를 가져오는 비동기 함수.

 

api.js

export const getUser = () => {
  return axios.get("/user").then((res) => res.data);
};

 

Edit.js

리액트 쿼리와 유저 정보를 가져오는 api인 getUser를 가져옵니다.

import {useQuery} from'react-query'
import {getUser} from "../mocks/api

 

리액트쿼리를 사용하여 유저의 정보를 가져옵니다.

export default function Edit() {
  const [inputValue, setInputValue] = useState("");
  const result = useQuery('@getUser', getUser);

  const handleChange = (e) => {
    setInputValue(e.target.value);
  };
  const handleSubmit = (e) => {
    e.preventDefault();
  };

}

 

콘솔로그를 확인하면 데이터가 잘 나오는 걸 확인할 수 있습니다.

 

 

잘가져오는 걸 확인했으니 데이터를 가져올 동안은 Loing이 보이고 데이터를 잘 가져오면 해당 닉네임이 보이도록 했습니다.

export default function Edit() {
  const [inputValue, setInputValue] = useState("");
  const {data, isLoading} = useQuery("@getUser");

const handleChange = (e) => {
    setInputValue(e.target.value);
  };
  const handleSubmit = (e) => {
    e.preventDefault();
  };
  
  if (isLoading) return <span>Loing...</span>
  return (
    <>
      <h1>Edit</h1>
      <h3>현재 닉네임: {data.nickName}</h3>
      <form onSubmit={handleSubmit}>
        <label>
          변경할 닉네임:
          <input type="text" value={inputValue} onChange={handleChange} />
        </label>
      </form>
    </>
  );

}

 

 

Mutation이란?

Mutation은 서버와의 상호작용을 통해 데이터를 생성, 수정, 삭제하거나 서버 상태를 변경하는 작업을 처리합니다. React Query의 useMutation 훅은 이러한 작업을 쉽게 수행할 수 있도록 도와줍니다.

 

api.js

export const updateNickname = (nickname) => {
  return axios.put(`/update-nickname?nickname=${nickname}`);
};

 

handlers.js

url에서 닉네임이라고 하는 파람스를 가져와 DB를 업데이트를 해줍니다.

rest.put("/update-nickname", (req, res, ctx) => {
const nickname = req.url.searchParams.get("nickname");
const updated = db.user.update({
  where: { id: { equals: 1 } },
  data: { nickName: nickname },
});

 

Edit.js

useMutation과 유저 정보를 업데이트하는 api인 updateNickname를 가져옵니다.

import {useQuery, useMutation} from'react-query'
import {getUser, updateNickname} from "../mocks/api

 

리액트쿼리를 사용하여 유저의 정보를 가져옵니다.

export default function Edit() {
  const [inputValue, setInputValue] = useState("");
  const result = useQuery('@getUser', getUser);
  const mutation = useMutaion(updateNickname);

  const handleChange = (e) => {
    setInputValue(e.target.value);
  };
  const handleSubmit = (e) => {
    e.preventDefault();
    mutation.mutate(inputValue) // 닉네임 업데이트
  };

}

 

Query Invalidation이란?

쿼리 무효화는 특정 쿼리를 "오래된 상태(stale)"로 표시하여 React Query가 해당 데이터를 다시 가져오도록 합니다. 

queryClient.invalidateQueries 메서드를 사용하여 쿼리를 무효화할 수 있습니다. 무효화된 쿼리는 다시 렌더링되며, 백그라운드에서 데이터를 재패치(fetch)합니다.

 

Edit.js

useMutation과 유저 정보를 업데이트하는 api인 updateNickname를 가져옵니다.

import {useQuery, useMutation, useQueryClient} from'react-query'
import {getUser, updateNickname} from "../mocks/api

 

 

api가 성공을 했을 때 업데이트된 데이터를 가져오게 합니다.

export default function Edit() {
  const [inputValue, setInputValue] = useState("");
  const queryClient = useQueryClient();
  const result = useQuery('@getUser', getUser);
  const mutation = useMutaion(updateNickname,{
  	onSuccess:() => {
    	queryClient.invalidateQueries("@getUser") //데이터 갱신
    }
  }));

  const handleChange = (e) => {
    setInputValue(e.target.value);
  };
  const handleSubmit = (e) => {
    e.preventDefault();
    mutation.mutate(inputValue) 
  };

}

 

Cache staleTime과 cacheTime

A컴포넌트 데이터를 패치를 하고 그 결과를 로컬스토리지에 저장한 후 B컴포넌트에 props로 전달해주지 못하기 때문에 전역상태가 필요한 상태입니다. 

A컴포넌트에서 api를 호출을 해서 그결과를 전역 상태에 저장을 했으면 B컴포넌트는 스토어 내가 찾는 데이터가 있으면 스토어에서 뽑아서 랜더링을 하고 없으면 dispatch를 하면 됩니다.

 

staleTime

데이터를 신선한 상태로 간주하는 기간을 설정합니다. 이 기간 동안 React Query는 네트워크 요청 없이 캐싱된 데이터를 사용합니다. staleTime이 지나기 전까지는 데이터를 다시 패치하지 않습니다. 컴포넌트가 언마운트 후 다시 마운트되어도 네트워크 요청이 발생하지 않습니다.

const { data } = useQuery(['user'], fetchUser, {
  staleTime: 10000, // 데이터가 10초 동안 신선한 상태로 유지
});

 

cacheTime 

 비활성화된 쿼리(사용되지 않는 쿼리)가 캐시에서 제거되기까지의 기간을 설정합니다. 쿼리를 사용하는 컴포넌트가 언마운트되면 해당 쿼리는 비활성 상태(inactive)가 됩니다. 비활성 쿼리는 cacheTime 동안 캐시에 남아 있다가 제거됩니다.

const { data } = useQuery(['user'], fetchUser, {
  cacheTime: 60000, // 비활성화된 데이터가 캐시에 1분 동안 유지
});

 

Home.js

Edit.js에서 사용하고 있는 useQuery가 있기 때문에 redispatch가 되고 있습니다.

import React from "react";
import { useNavigate } from "react-router-dom";
import {useQuery} from'react-query'
import {getUser} from "../mocks/api"

export default function Home() {
  const navigate = useNavigate();
  const {data, isLoading} = useQuery("@getUser");
  
  return (
    <div>
      <h1>Home {"hwqrqi"}</h1>
      <button onClick={() => navigate("/edit")}>Go Edit Page</button>
    </div>
  );
}

 

staleTime를 추가하면 re dipatch가 안되도록 할 수 있습니다.

import React from "react";
import { useNavigate } from "react-router-dom";
import {useQuery} from'react-query'
import {getUser} from "../mocks/api"

export default function Home() {
  const navigate = useNavigate();
  const {data, isLoading} = useQuery("@getUser", getUser,{
  	staleTime:Infinity,
  });
  
  return (
    <div>
      <h1>Home {"hwqrqi"}</h1>
      <button onClick={() => navigate("/edit")}>Go Edit Page</button>
    </div>
  );
}

 

Edit.js

export default function Edit() {
  const [inputValue, setInputValue] = useState("");
  const queryClient = useQueryClient();
  const {data, isLoading} = useQuery("@getUser", getUser,{
  	staleTime:Infinity,
  });
  
  const mutation = useMutaion(updateNickname,{
  	onSuccess:() => {
    	queryClient.invalidateQueries("@getUser")
    }
  }));

  const handleChange = (e) => {
    setInputValue(e.target.value);
  };
  const handleSubmit = (e) => {
    e.preventDefault();
    mutation.mutate(inputValue) 
  };

}

 

cacheTime을 추가하여 데이터를 캐실하고 비활성화된 데이터가 일정 시간 동안 캐시에 남아있도록 설정하겠습니다.

 

Home.js

import React from "react";
import { useNavigate } from "react-router-dom";
import { useQuery } from "react-query";
import { getUser } from "../mocks/api";

export default function Home() {
  const navigate = useNavigate();
  const { data, isLoading } = useQuery("@getUser", getUser, {
    staleTime: Infinity, // 데이터를 항상 신선한 상태로 유지
    cacheTime: 600000, // 비활성화된 데이터가 10분 동안 캐시에 유지
  });

  if (isLoading) return <span>Loading...</span>;

  return (
    <div>
      <h1>Home {"hwqrqi"}</h1>
      <button onClick={() => navigate("/edit")}>Go Edit Page</button>
    </div>
  );
}

 

Edit.js

import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "react-query";
import { getUser, updateNickname } from "../mocks/api";

export default function Edit() {
  const [inputValue, setInputValue] = useState("");
  const queryClient = useQueryClient();

  const { data, isLoading } = useQuery("@getUser", getUser, {
    staleTime: Infinity, // 데이터를 항상 신선한 상태로 유지
    cacheTime: 600000, // 비활성화된 데이터가 10분 동안 캐시에 유지
  });

  const mutation = useMutation(updateNickname, {
    onSuccess: () => {
      queryClient.invalidateQueries("@getUser"); // 업데이트 후 쿼리 무효화
    },
  });

  const handleChange = (e) => {
    setInputValue(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    mutation.mutate(inputValue); // 닉네임 업데이트 요청
  };

  if (isLoading) return <span>Loading...</span>;

  return (
    <>
      <h1>Edit</h1>
      <h3>현재 닉네임: {data.nickName}</h3>
      <form onSubmit={handleSubmit}>
        <label>
          변경할 닉네임:
          <input type="text" value={inputValue} onChange={handleChange} />
        </label>
        <button type="submit">변경</button>
      </form>
    </>
  );
}

 

 

Suspense이란?

React의 Suspense는 컴포넌트가 비동기 작업(예: 데이터 패칭)을 수행하는 동안 UI를 잠시 대기 상태로 유지하고, 로딩 중인 상태를 처리할 수 있도록 돕는 기능입니다. React Query는 이 기능을 지원하여 데이터 로딩과 에러 처리를 간소화합니다.

 

기존 방식

  Fetch-on-Render (렌더 후 데이터 가져오기) 컴포넌트가 렌더링된 후 useEffect 또는 componentDidMount에서 데이터를 가져옵니다.

 단점: "워터폴(waterfall)" 문제가 발생하여 네트워크 요청이 순차적으로 처리됩니다.

useEffect(() => {
  fetchUser().then(setUser);
}, []);

 

Fetch-Then-Render (데이터 가져온 후 렌더링)

 모든 데이터를 미리 가져온 후 화면을 렌더링합니다.

 단점: 데이터를 모두 가져올 때까지 기다려야 하므로 초기 렌더링이 지연됩니다

function fetchProfileData() {
  return Promise.all([fetchUser(), fetchPosts()]);
}

 

Render-as-You-Fetch (데이터 가져오면서 렌더링)

 데이터를 가져오는 동시에 화면을 렌더링합니다. 필요한 데이터가 준비되지 않은 경우 해당 컴포넌트를 중단(suspend)하고 fallback을 표시합니다.

 1) 데이터를 미리 가져오기 시작 (fetchProfileData()). 

 2) 컴포넌트를 렌더링 시도. 

 3) 필요한 데이터가 없으면 Suspense의 fallback을 표시. 

 4) 데이터가 준비되면 React가 다시 렌더링 시도.

const resource = fetchProfileData();

function ProfileDetails() {
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

 

 

현재 작성된 코드에서 Home.js와 Edit.js는 React Query와 Suspense를 사용하여 데이터를 패칭하고 있습니다. 하지만 병렬로 데이터를 가져오지 않고 순차적으로 실행되는 문제가 있습니다. 이를 해결하기 위해 React Query와 Suspense의 특성을 활용하여 병렬로 데이터를 가져오는 방법을 설명하겠습니다.

 

Home.js

suspense추가합니다.

import React from "react";
import { useNavigate } from "react-router-dom";
import { useQuery } from "react-query";
import { getUser } from "../mocks/api";

export default function Home() {
  const navigate = useNavigate();
  const { data, isLoading } = useQuery("@getUser", getUser, {
    staleTime: Infinity,
    cacheTime: 600000, 
    suspense: true,
  });


  return (
    <div>
      <h1>Home {data?.nickName}</h1>
      <button onClick={() => navigate("/edit")}>Go Edit Page</button>
    </div>
  );
}

 

app.js

각 데이터 유형별 로딩 상태를 개별적으로 처리합니다.

쿼리 키 관리

에러 처리 

import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Edit from "./pages/Edit";
import { Suspense } from "react";
import { QueryClientProvider, QueryClient } from "react-query";

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Suspense fallback={<span>Loading...</span>}>
        <BrowserRouter>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/edit" element={<Edit />} />
          </Routes>
        </BrowserRouter>
      </Suspense>
    </QueryClientProvider>
  );
}

export default App;

 

api.js

import axios from "axios";

export const getUser = () => {
  return axios.get("/user").then((res) => res.data);
};

export const updateNickname = (nickname) => {
  return axios.put(`/update-nickname?nickname=${nickname}`);
};

export const getPosts = () => {
  return axios.get("/posts").then((res) => res.data);
};

 

 

handlers.js

posts GET 요청을 별도의 핸들러로 분리합니다.

import { rest } from "msw";
import { db } from "./db";

export const handlers = [
  rest.get("/user", (req, res, ctx) => {
    return new Promise((resolve) =>
      setTimeout(() => {
        return resolve(res(ctx.status(200), ctx.json(db.user.getAll()[0])));
      }, 1000)
    );
  }),

  rest.put("/update-nickname", (req, res, ctx) => {
    const nickname = req.url.searchParams.get("nickname");
    const updated = db.user.update({
      where: { id: { equals: 1 } },
      data: { nickName: nickname },
    });
    
   rest.get("/posts", (req, res, ctx) => {
    return new Promise((resolve) =>
      setTimeout(() => {
        return resolve(res(ctx.status(200), ctx.json(db.user.getAll()[0])));
      }, 1000)
    );
  }),

    return res(ctx.json(updated));
  }),
];

 

Edit.js

useQueries로 병렬 데이터 패칭

Suspense를 통한 로딩 관리

import React, { useState } from "react";
import { useQueries, useMutation, useQueryClient } from "react-query";
import { getUser, updateNickname, getPosts } from "../mocks/api";

export default function Edit() {
  const [inputValue, setInputValue] = useState("");
  const queryClient = useQueryClient();

  // 병렬 데이터 패칭
  const results = useQueries([
    {
      queryKey: "@getUser",
      queryFn: getUser,
      staleTime: Infinity,
      cacheTime: 600000,
    },
    {
      queryKey: "@getPosts",
      queryFn: getPosts,
      staleTime: Infinity,
      cacheTime: 600000,
    },
  ]);

  const user = results[0].data;
  const posts = results[1].data;

  const mutation = useMutation(updateNickname, {
    onSuccess: () => {
      queryClient.invalidateQueries("@getUser"); // 닉네임 업데이트 후 데이터 갱신
    },
  });

  const handleChange = (e) => {
    setInputValue(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    mutation.mutate(inputValue); // 닉네임 업데이트 요청
  };

  return (
    <>
      <h1>Edit</h1>
      <h3>현재 닉네임: {user?.nickName}</h3>
      <form onSubmit={handleSubmit}>
        <label>
          변경할 닉네임:
          <input type="text" value={inputValue} onChange={handleChange} />
        </label>
        <button type="submit">변경</button>
      </form>
      <ul>
        {posts?.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </>
  );
}

 

개발자도구를 performance를 하면 프로파일링 한 결과를 확인할 수 있습니다. user가 실행된 후 posts가 실행된 걸 보니 병렬적으로  실행되지 않았는 걸 알 수 있습니다.