[React] Axios에서 useQuery 사용해서 코드 구조 바꾸기

kjy0124·2025년 4월 18일
post-thumbnail

🎯 main.jsx에서 useQuery사용하기 위해 구조 변경

✅ 변경 전

main.jsx

import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(<App />);

✅ 변경 후

import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.jsx";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

createRoot(document.getElementById("root")).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

1️⃣ QueryClientQueryClientProvider은 React Query의 핵심 요소

  • QueryClient : 요청, 응답, 캐시 등을 관리하는 중앙 관리자
  • QueryClientProvider : 앱 전체에서 React Query를 사용할 수 있게 해주는 Provider 컴포넌트

2️⃣ queryClientQueryClient()의 인스턴스
3️⃣ App.jsxQueryClientProvider로 감싸주기

createRoot(document.getElementById("root")).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

🎯 App.jsx에서 useQuery사용하여 코드 변경

✅ 변경 전

import "./App.css";
import Header from "./components/Header";
import LikeCardList from "./components/LikeCardList";
import List from "./components/List";
import SkeletonList from "./components/SkeletonList";
import {
  fetchAllRestaurants,
  getUserLikes,
  postUserLike,
  deleteUserLike,
} from "./api/restaurantAPI";
import { sortPlacesByDistance } from "./utils/loc";

import { useEffect, useState } from "react";

function App() {
  //맛집 데이터를 담을 상태 변수 선언
  const [allRestaurants, setAllRestaurants] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [likedRestaurants, setLikedRestaurants] = useState([]);

  //에러처리 상태 변수 선언
  const [errorMessage, setErrorMessage] = useState(null);

  // console.log("allRestaurants : ", allRestaurants);
  useEffect(() => {
    // 브라우저에서 현재 사용자 위치 요청
    navigator.geolocation.getCurrentPosition(
      (position) => {
        // 위도, 경도 받아오기
        const { latitude: userLat, longitude: userLon } = position.coords;

        // 사용자의 현재 위치를 기준으로 맛집 목록을 거리순으로 정렬
        if (
          allRestaurants &&
          Array.isArray(allRestaurants.places) &&
          allRestaurants.places.length > 0
        ) {
          const sorted = sortPlacesByDistance(
            allRestaurants.places,
            userLat,
            userLon
          );
          // 정렬된 데이터를 상태에 저장(장소는 거리순으로 변경됨)
          setAllRestaurants({ ...allRestaurants, places: sorted });
        }
      },
      (error) => {
        console.error("위치 정보를 가져오는 데 실패했습니다.", error);
      }
    );
    //거리 순으로 상태가 변경될 때마다 렌더링
  }, [allRestaurants]);

  useEffect(() => {
    const getData = async () => {
      try {
        const data = await fetchAllRestaurants();
        setAllRestaurants(data);
      } catch (err) {
        console.error("요청 실패:", err);

        if (err instanceof Response) {
          if (err.status === 404) {
            setErrorMessage("요청하신 데이터를 찾을 수 없습니다. (404)");
          } else {
            setErrorMessage(`문제가 발생했습니다. (상태 코드: ${err.status})`);
          }
        } else {
          setErrorMessage("서버와 연결할 수 없습니다. 주소를 확인해주세요.");
        }
      } finally {
        setIsLoading(false);
      }
    };

    getData();
  }, []);

  useEffect(() => {
    const data = async () => {
      try {
        const all = await fetchAllRestaurants();
        // console.log("백엔드 응답 : ", all);
        setAllRestaurants(all);
      } catch (err) {
        console.error("에러 발생!", err);
      } finally {
        setIsLoading(false);
      }
    };
    data();
  }, []);

  //컴포넌트가 처음 화면에 나타났을 때 딱 한 번만 실행
  useEffect(() => {
    const data = async () => {
      try {
        //아까 반환한 JSON 형식을 서버에 요청을 보내 데이터를 받음
        const all = await fetchAllRestaurants();
        //데이터를 받고 상태 변경 적용
        setAllRestaurants(all);
        // console.log("all :", all);
      } catch (error) {
        console.log(error);
      }
    };
    data(); //비동기 함수를 실행
  }, []);
  // console.log("state :", allRestaurants);

  //찜목록
  useEffect(() => {
    const fetchLikes = async () => {
      try {
        const data = await getUserLikes();
        setLikedRestaurants(data.places);
      } catch (err) {
        console.error("찜 목록 불러오기 실패!", err);
      }
    };
    fetchLikes();
  }, []);

  const handleLikeToggle = async (restaurant) => {
    const isLiked = likedRestaurants.some((item) => item.id === restaurant.id);

    try {
      if (isLiked) {
        await deleteUserLike(restaurant.id);
        setLikedRestaurants((prev) =>
          prev.filter((item) => item.id !== restaurant.id)
        );
      } else {
        await postUserLike(restaurant);
        setLikedRestaurants((prev) => [...prev, restaurant]);
      }
    } catch (err) {
      console.error("찜 토글 실패:", err);
    }
  };

  return (
    <div className="min-h-screen bg-[#121212] text-gray-200">
      <Header />
      <main className="max-w-6xl mx-auto px-4 py-8">
        <section className="mb-8 bg-[#1e1e1e] p-8 rounded-xl border-[#333] shadow-xl">
          <h3 className="text-xl font-semibold mb-4 text-center text-white">
            ❤️ 찜한 맛집
          </h3>
          <LikeCardList
            likedRestaurants={likedRestaurants}
            onToggleLike={handleLikeToggle}
          />
        </section>
        <section className="bg-[#1e1e1e] p-8 rounded-xl shadow-xl">
          <h2 className="text-xl font-semibold mb-4 text-center text-white">
            📍전체 맛집 목록
          </h2>
          {errorMessage && (
            <div className="text-red-400 text-center my-4 font-medium">
              {errorMessage}
            </div>
          )}

          {isLoading ? (
            <SkeletonList />
          ) : (
            <List
              allRestaurants={allRestaurants}
              likedRestaurants={likedRestaurants}
              onToggleLike={handleLikeToggle}
            />
          )}
        </section>
      </main>
    </div>
  );
}

export default App;

✅ 변경 후

1️⃣ 먼저 import 필요 없는 부분 삭제 및 추가

import { useEffect, useState } from "react";
  • React Query를 사용하게 되면 사용자가 직접 useEffectuseState로 상태를 관리하지 않음
  • 그리고 React Query를 사용하기 위해 아래와 같이 import해준다.
import { useQuery } from "@tanstack/react-query";

2️⃣ 다음으로 useState코드와 useEffect 코드를 합칠 수 있는 것들을 삭제 후 아래와 같이 합침

  • 먼저 아래 코드를 useQuery로 교체
const [allRestaurants, setAllRestaurants] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState(null);

useEffect(() => {
    const getData = async () => {
      try {
        const data = await fetchAllRestaurants();
        setAllRestaurants(data);
      } catch (err) {
        console.error("요청 실패:", err);

        if (err instanceof Response) {
          if (err.status === 404) {
            setErrorMessage("요청하신 데이터를 찾을 수 없습니다. (404)");
          } else {
            setErrorMessage(`문제가 발생했습니다. (상태 코드: ${err.status})`);
          }
        } else {
          setErrorMessage("서버와 연결할 수 없습니다. 주소를 확인해주세요.");
        }
      } finally {
        setIsLoading(false);
      }
    };

    getData();
  }, []);

  useEffect(() => {
    const data = async () => {
      try {
        const all = await fetchAllRestaurants();
        // console.log("백엔드 응답 : ", all);
        setAllRestaurants(all);
      } catch (err) {
        console.error("에러 발생!", err);
      } finally {
        setIsLoading(false);
      }
    };
    data();
  }, []);

  //컴포넌트가 처음 화면에 나타났을 때 딱 한 번만 실행
  useEffect(() => {
    const data = async () => {
      try {
        //아까 반환한 JSON 형식을 서버에 요청을 보내 데이터를 받음
        const all = await fetchAllRestaurants();
        //데이터를 받고 상태 변경 적용
        setAllRestaurants(all);
        // console.log("all :", all);
      } catch (error) {
        console.log(error);
      }
    };
    data(); //비동기 함수를 실행
  }, []);
  • 해당 코드들은 useEffect가 모두 같은 fetchAllRestaurants()API 요청을 중복해서 보내고 있음
  • 그래서 아래와 같이 저기 코드를 하나로 합침
  • 그리고 아래의 코드로 대체
const {
    data: allRestaurants,
    isLoading,
    isError,
    error,
  } = useQuery({
    queryKey: ["allRestaurants"],
    queryFn: fetchAllRestaurants,
  });
  • 기존 state 값을 모두 data 안에 넣음
  • useQuery에서는 queryKey에 주로 사용했던 allRestaurants로 적용
  • 그리고 queryFn에서는 주로 호출헀던 API 함수 fetchAllRestaurants로 적용
  • error은 원래 객체상태여서 isError는 에러 여부 판단하는 용도이다.
  • 에러 메세지는 error객체 안에 있는 속성 message로 무슨 오류인지 알려줌

3️⃣ 추가로 아래 찜 버튼 클릭 시 찜목록에 추가하기 위한 상태도 useQuery로 변경

const [likedRestaurants, setLikedRestaurants] = useState([]);

useEffect(() => {
    const fetchLikes = async () => {
      try {
        const data = await getUserLikes();
        setLikedRestaurants(data.places);
      } catch (err) {
        console.error("찜 목록 불러오기 실패!", err);
      }
    };
    fetchLikes();
  }, []);
  • 해당 코드를 아래와 같이 변경할 수 있음
const {
    data: likedRestaurants,
    isLoading: isLikesLoading,
    isError: isLikesError,
    error: likesError,
  } = useQuery({
    queryKey: ["likedRestaurants"],
    queryFn: getUserLikes,
    select: (data) => data.places,
  });
  • select 옵션은 queryFn으로 받아온 전체 응답 데이터에서 내가 원하는 데이터만 추출해서 가공할 수 있게 도와주는 기능

💡 select 란?

  • 예를 들어, getUserLikes() API의 응답이 아래와 같다고 가정
{
  userId: "u1",
  places: [
    { id: "r1", title: "냉면집", ... },
    ...
  ]
}
  • 만일 우리가 userId는 필요 없고, places만 꺼내쓰고 싶다면 아래와 같이 작성
select: (data) => data.places

🎯 App.jsx에서 useMutation사용하여 코드 변경

✅ useMutation이란?

  • useMutationReact Query에서 POST, DELETE, PUT 요청처럼 데이터를 서버에 보내는 작업
  • 여기에서는 찜 추가 및 삭제 요청을 서버로 보낼 때 사용

✅ invalidateQueries란?

  • 서버에 요청이 성공한 경우 invalidateQueries를 사용하여 지정한 쿼리를 자동으로 refetch(재요청) 함
  • 여기서는 likedRestaurants 쿼리를 무효화해서 찜 목록을 최신 상태로 유지되도록 함

✅ 변경 전

const handleLikeToggle = async (restaurant) => {
    const isLiked = likedRestaurants.some((item) => item.id === restaurant.id);

    try {
      if (isLiked) {
        await deleteUserLike(restaurant.id);
        likedRestaurants((prev) =>
          prev.filter((item) => item.id !== restaurant.id)
        );
      } else {
        await postUserLike(restaurant);
        likedRestaurants((prev) => [...prev, restaurant]);
      }
    } catch (err) {
      console.error("찜 토글 실패:", err);
    }
  };

✅ 변경 후

1️⃣ 먼저 찜 추가 함수 작성

  const postLikeMutation = useMutation({
    mutationFn: postUserLike,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["likedRestaurants"] });
    },
    onError: (err) => {
      console.error("찜 추가 실패: ", err);
    },
  });
  • mutationFn 에서는 실제 서버에 POST 요청을 보내는 함수 지정
  • 아래 코드에서는 요청 성공했을 때 (onSuccess) 찜 목록 데이터를 다시 불러오도록 하는 것
  • 즉, 찜 목록에 성공적으로 추가가 됐을 때 최신 찜 목록 데이터를 다시 불러오도록 요청하여 화면에 반영되도록 하는 것
queryClient.invalidateQueries({ queryKey: ["likedRestaurants"] });

2️⃣ 찜 삭제 함수 작성

  const deleteLikeMutation = useMutation({
    mutationFn: deleteUserLike,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["likedRestaurants"] });
    },
    onError: (err) => {
      console.error("찜 삭제 실패: ", err);
    },
  });
  • mutationFn 에서 실제 서버에 DELETE 요청을 보내는 함수 지정
  • 아래 코드는 요청이 성공했을 때 (onSuccess) 찜 목록에서 맛집이 삭제가 되고 찜 목록을 다시 불러오는 것
queryClient.invalidateQueries({ queryKey: ["likedRestaurants"] });

3️⃣ 하트 버튼 클릭했을 때 호출되는 함수 작성

  const handleLikeToggle = async (restaurant) => {
    const isLiked = likedRestaurants.some((item) => item.id === restaurant.id);

    if (isLiked) {
      deleteLikeMutation.mutate(restaurant.id);
    } else {
      postLikeMutation.mutate(restaurant);
    }
  };
  • 아래 코드는 isLiked로 찜했던 맛집인지 확인
    const isLiked = likedRestaurants.some((item) => item.id === restaurant.id);
  • 조건문을 통해 만일 맛집을 체크했었다면 삭제 함수를 통해 삭제를 요청
deleteLikeMutation.mutate(restaurant.id);
  • 아니라면 추가 함수를 통해 추가 요청
postLikedMutation.mutate(restaurant);

🎯 거리순 맛집 정렬 코드 수정

✅ 변경 전

useEffect(() => {
    // 브라우저에서 현재 사용자 위치 요청
    navigator.geolocation.getCurrentPosition(
      (position) => {
        // 위도, 경도 받아오기
        const { latitude: userLat, longitude: userLon } = position.coords;

        // 사용자의 현재 위치를 기준으로 맛집 목록을 거리순으로 정렬
        if (
          allRestaurants &&
          Array.isArray(allRestaurants.places) &&
          allRestaurants.places.length > 0
        ) {
          const sorted = sortPlacesByDistance(
            allRestaurants.places,
            userLat,
            userLon
          );
          // 정렬된 데이터를 상태에 저장(장소는 거리순으로 변경됨)
          allRestaurants({ ...allRestaurants, places: sorted });
        }
      },
      (error) => {
        console.error("위치 정보를 가져오는 데 실패했습니다.", error);
      }
    );
    //거리 순으로 상태가 변경될 때마다 렌더링
  }, [allRestaurants]);

✅ 변경 후

const [sortedRestaurants, setSortedRestaurants] = useState([]);

  useEffect(() => {
    // 브라우저에서 현재 사용자 위치 요청
    navigator.geolocation.getCurrentPosition(
      (position) => {
        // 위도, 경도 받아오기
        const { latitude: userLat, longitude: userLon } = position.coords;

        // 사용자의 현재 위치를 기준으로 맛집 목록을 거리순으로 정렬
        if (
          allRestaurants &&
          Array.isArray(allRestaurants.places) &&
          allRestaurants.places.length > 0
        ) {
          const sorted = sortPlacesByDistance(
            allRestaurants.places,
            userLat,
            userLon
          );
          // 정렬된 데이터를 상태에 저장(장소는 거리순으로 변경됨)
          setSortedRestaurants({ ...allRestaurants, places: sorted });
        }
      },
      (error) => {
        console.error("위치 정보를 가져오는 데 실패했습니다.", error);
      }
    );
    //거리 순으로 상태가 변경될 때마다 렌더링
  }, [allRestaurants]);

1️⃣ 새로운 상태 추가 및 생성

  • 먼저 allRestaurantsuseQuery에서 받아온 데이터이므로 setState 함수가 아님
  • 그래서 상태를 새로 추가하여 아래 코드를 수정
allRestaurants({ ...allRestaurants, places: sorted });

위 코드의 allRestaurants 부분을 새로 선언한 상태 함수인 setSortedRestaurants로 변경

2️⃣ return에 있는 반환 코드 수정

      <List
        allRestaurants={sortedRestaurants || allRestaurants}
        likedRestaurants={likedRestaurants}
        onToggleLike={handleLikeToggle}
      />
  • <List /> 렌더링 시 정렬된 데이터 넘김

🎯 최종 App.jsx 코드

import "./App.css";
import Header from "./components/Header";
import LikeCardList from "./components/LikeCardList";
import List from "./components/List";
import SkeletonList from "./components/SkeletonList";
import {
  fetchAllRestaurants,
  getUserLikes,
  postUserLike,
  deleteUserLike,
} from "./api/restaurantAPI";
import { sortPlacesByDistance } from "./utils/loc";
import { QueryClient, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation } from "@tanstack/react-query";
import { useEffect, useState } from "react";

function App() {
  const [sortedRestaurants, setSortedRestaurants] = useState([]);
  const queryClient = useQueryClient();

  const {
    data: allRestaurants,
    isLoading,
    isError,
    error,
  } = useQuery({
    queryKey: ["allRestaurants"],
    queryFn: fetchAllRestaurants,
  });

  const {
    data: likedRestaurants,
    isLoading: isLikesLoading,
    isError: isLikesError,
    error: likesError,
  } = useQuery({
    queryKey: ["likedRestaurants"],
    queryFn: getUserLikes,
    select: (data) => data.places,
  });

  console.log("likedRestaurants : ", likedRestaurants);

  useEffect(() => {
    // 브라우저에서 현재 사용자 위치 요청
    navigator.geolocation.getCurrentPosition(
      (position) => {
        // 위도, 경도 받아오기
        const { latitude: userLat, longitude: userLon } = position.coords;

        // 사용자의 현재 위치를 기준으로 맛집 목록을 거리순으로 정렬
        if (
          allRestaurants &&
          Array.isArray(allRestaurants.places) &&
          allRestaurants.places.length > 0
        ) {
          const sorted = sortPlacesByDistance(
            allRestaurants.places,
            userLat,
            userLon
          );
          // 정렬된 데이터를 상태에 저장(장소는 거리순으로 변경됨)
          setSortedRestaurants({ ...allRestaurants, places: sorted });
        }
      },
      (error) => {
        console.error("위치 정보를 가져오는 데 실패했습니다.", error);
      }
    );
    //거리 순으로 상태가 변경될 때마다 렌더링
  }, [allRestaurants]);

  const postLikeMutation = useMutation({
    mutationFn: postUserLike,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["likedRestaurants"] });
    },
    onError: (err) => {
      console.error("찜 추가 실패: ", err);
    },
  });

  const deleteLikeMutation = useMutation({
    mutationFn: deleteUserLike,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["likedRestaurants"] });
    },
    onError: (err) => {
      console.error("찜 삭제 실패: ", err);
    },
  });

  const handleLikeToggle = async (restaurant) => {
    const isLiked = likedRestaurants.some((item) => item.id === restaurant.id);

    if (isLiked) {
      deleteLikeMutation.mutate(restaurant.id);
    } else {
      postLikeMutation.mutate(restaurant);
    }
  };

  return (
    <div className="min-h-screen bg-[#121212] text-gray-200">
      <Header />
      <main className="max-w-6xl mx-auto px-4 py-8">
        <section className="mb-8 bg-[#1e1e1e] p-8 rounded-xl border-[#333] shadow-xl">
          <h3 className="text-xl font-semibold mb-4 text-center text-white">
            ❤️ 찜한 맛집
          </h3>
          {isLikesError && (
            <div className="text-red-400 text-center my-4 font-medium">
              {likesError.message}
            </div>
          )}
          {isLikesLoading ? (
            <div>찜 목록 불러오는 중...</div>
          ) : (
            <LikeCardList
              likedRestaurants={likedRestaurants}
              onToggleLike={handleLikeToggle}
            />
          )}
        </section>
        <section className="bg-[#1e1e1e] p-8 rounded-xl shadow-xl">
          <h2 className="text-xl font-semibold mb-4 text-center text-white">
            📍전체 맛집 목록
          </h2>
          {isError && (
            <div className="text-red-400 text-center my-4 font-medium">
              {error.message}
            </div>
          )}

          {isLoading ? (
            <SkeletonList />
          ) : (
            <List
              allRestaurants={sortedRestaurants || allRestaurants}
              likedRestaurants={likedRestaurants}
              onToggleLike={handleLikeToggle}
            />
          )}
        </section>
      </main>
    </div>
  );
}

export default App;
profile
개발 공부...

0개의 댓글