코딩/FrontEnd

React Query를 공부해봅시다

박강원입니다 2025. 3. 20. 14:42

 

 

이 글의 코드를 이해하려면 이전 글을 읽는 것을 추천드립니다.

 

https://kangwonpark27.tistory.com/67

 

useImperativeHandle | createPortal | forwardRef | Serveraction | middleWare에 대해 공부해봅시다

useImperativeHandleuseImperativeHandle은 React에서 부모 컴포넌트가 자식 컴포넌트의 특정 메서드나 속성을 직접 제어할 수 있도록 도와주는 Hook이다. 보통 forwardRef와 함께 사용하며, ref를 통해 컴포넌트

kangwonpark27.tistory.com

 

 

현재 폴더 구조

 

ReactQuery는 아래의 명령어로 설치가 가능하다

yarn add @tanstack/react-query
yarn add @tanstack/react-query-devtools

 

providers 파일을 app안에 만들고

"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 
import { PropsWithChildren, useState } from "react"; 

// 자식 컴포넌트를 포함할 수 있는 Props 타입 정의
type Props = {} & PropsWithChildren;

// Providers 컴포넌트 정의
const Providers = ({ children }: Props) => {
  // QueryClient 인스턴스를 useState를 사용해 메모이제이션
  // () => new QueryClient()는 컴포넌트가 리렌더링되어도 동일한 QueryClient 인스턴스 유지
  const [queryClient] = useState(() => new QueryClient());

  return (
    // QueryClientProvider로 자식 컴포넌트들을 감싸서 React Query 컨텍스트 제공
    // client prop에 생성된 queryClient 인스턴스 전달
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

export default Providers;

 

layout.tsx의 children을 <providers></providers>로 감쌌다

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <Providers>{children}</Providers>//!!!!!!!!! 여기 !!!!!!!!!!
        {/* 
          Providers 컴포넌트로 자식 컴포넌트들을 감싸서 
          - React Query 
          - 전역 상태 관리 
          - 기타 앱 전역 프로바이더 기능 제공 
        */}
      </body>
    </html>
  );
}

 

 

 

/about/page.ts를 아래와 같이 수정했다.

"use client"; // Next.js 클라이언트 컴포넌트 지시어
import { useQuery } from "@tanstack/react-query"; // React Query의 useQuery 훅 임포트

const AboutPage = () => {
  // useQuery 훅 사용: 데이터 페칭 및 상태 관리
  const { 
    data,     // 페칭된 데이터 
    isLoading // 로딩 상태 
  } = useQuery({
    // 쿼리의 고유 식별자 
    // - 캐싱, 리패칭, 동기화에 사용됨
    // - 여기서는 "about" 문자열 사용
    queryKey: ["about"], 

    // 실제 데이터를 가져오는 비동기 함수
    // JSONPlaceholder API에서 posts 데이터 페칭
    queryFn: () => 
      fetch("https://jsonplaceholder.typicode.com/posts")
        .then((res) => res.json()),
  });

  // 로딩 중일 때 로딩 메시지 표시
  if (isLoading) return <div>Loading...</div>;

  // 데이터 로딩 완료 후 렌더링
  return (
    <div>
      {/* 
        데이터 배열 매핑 
        - 각 포스트의 id와 title 표시 
        - TypeScript 타입 지정으로 타입 안전성 확보
      */}
      {data?.map((item: { id: number; title: string }) => (
        <li key={item.id} className="border rounded p-4">
          {item.title}
        </li>
      ))}
    </div>
  );
};

export default AboutPage;

 

middleware.ts로 가서

import { NextResponse } from "next/server"; // Next.js 서버 응답 유틸리티
import type { NextRequest } from "next/server"; // Next.js 서버 요청 타입

// 미들웨어 함수 정의
export function middleware(request: NextRequest) {
  // 주석 처리된 리다이렉트 로직 삭제
  // return NextResponse.redirect(new URL("/", request.url));//삭제!!!!!

  // 요청을 그대로 다음 미들웨어나 라우트 핸들러로 전달
  return NextResponse.next();
}

// 미들웨어 설정
export const config = {
  // 미들웨어가 적용될 경로 매처
  // "/about" 경로와 그 하위 모든 경로에 적용
  matcher: "/about/:path*",
};

 

/about/page.ts도 다시 수정했음(body 넣고, 스타일들 추가)

"use client";

import { useQuery } from "@tanstack/react-query";

const AboutPage = () => {
  const { data, isLoading } = useQuery({
    queryKey: ["about"],
    queryFn: () =>
      fetch("https://jsonplaceholder.typicode.com/posts").then((res) =>
        res.json()
      ),
  });
  if (isLoading) return <div>Loading...</div>;
  return (
    <div>
      {data?.map((item: { id: number; title: string, body:string }) => (
        <li key={item.id} className="border rounded p-4 list-none flex flex-col gap-2">
          <h1 className="text-2xl font-bold">{item.title}</h1>
          <p className="text-sm">{item.body}</p>
        </li>
      ))}
    </div>
  );
};

export default AboutPage;

 

/about2/page.ts를 만들어서 아래 코드를 넣었다.

 

"use client"; // Next.js 클라이언트 컴포넌트 지시어
import { useState, useEffect } from "react";

const About2Page = () => {
  // 상태 관리를 위한 useState 훅 사용
  // 데이터, 로딩 상태, 에러 상태 정의
  const [data, setData] = useState<
    { id: number; title: string; body: string }[]
  >([]);
  
  // 로딩 상태 관리
  const [isLoading, setIsLoading] = useState(true);
  
  // 에러 상태 관리
  const [error, setError] = useState<Error | null>(null);

  // 데이터 페칭을 위한 useEffect 훅
  useEffect(() => {
    // fetch API를 사용한 데이터 요청
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((res) => res.json()) // JSON으로 응답 변환
      .then((data) => setData(data)) // 데이터 상태 업데이트
      .catch((error) => setError(error)) // 에러 발생 시 에러 상태 업데이트
      .finally(() => setIsLoading(false)); // 요청 완료 시 로딩 상태 해제
  }, []); // 빈 의존성 배열: 컴포넌트 마운트 시 한 번만 실행

  // 로딩 중일 때 로딩 메시지 표시
  if (isLoading) return <div>Loading...</div>;

  // 데이터 렌더링
  return (
    <ul className="p-4 flex flex-col gap-2">
      {data?.map((item: { id: number; title: string; body: string }) => (
        <li 
          key={item.id} 
          className="border rounded p-4 flex flex-col gap-2"
        >
          <h1 className="text-2xl font-bold">{item.title}</h1>
          <p className="text-sm">{item.body}</p>
        </li>
      ))}
    </ul>
  );
};

export default About2Page;

 

 

다시 /about/page 로 돌아와서

"use client";

import { useQuery } from "@tanstack/react-query";
import _ from "lodash";

const AboutPage = () => {
  const { data, isPending, refetch } = useQuery({
    queryKey: ["about"], //전역으로 캐시되는 키
    queryFn: () =>
      //데이터를 가져오는 함수 -> 전역으로 전달됨
      fetch("https://jsonplaceholder.typicode.com/posts")
        .then((res) => res.json())
        .then((res: { id: number; title: string; body: string }[]) =>
          _.shuffle(res)
        ),
  });

  //isLoading은 최초 데이터 패칭 시 true, 새로고침 시 계속 로딩 나옴
  //isPending은 캐시된 데이터말고 데이터가 패칭 중일 때, 
  //isFetching은 데이터가 패칭 중일때, 할 때마다 상태값이 바뀜
  if (isPending) return <div>Loading...</div>;
  return (
    <ul className="p-4 flex flex-col gap-2">
      <button
        onClick={() => refetch()}
        className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 active:bg-blue-700 transition-colors duration-200"
      >
        새로고침
      </button>
      {data?.map((item: { id: number; title: string; body: string }) => (
        <li key={item.id} className="border rounded p-4 flex flex-col gap-2">
          <h1 className="text-2xl font-bold">{item.title}</h1>
          <p className="text-sm">{item.body}</p>
        </li>
      ))}
    </ul>
  );
};

export default AboutPage;

 

그런데 아래와 같이 useQuery를 쓰면 다른 페이지에 있는 정보를 자동으로 다시 불러오기 가능

"use client";

import { useQuery, useQueryClient } from "@tanstack/react-query";//useQuery 해줌
import _ from "lodash";

const AboutPage = () => {
  const queryClient = useQueryClient();
  const { data, isPending } = useQuery({
    queryKey: ["about"], //전역으로 캐시되는 키
    queryFn: () =>
      //데이터를 가져오는 함수 -> 전역으로 전달됨
      fetch("https://jsonplaceholder.typicode.com/posts")
        .then((res) => res.json())
        .then((res: { id: number; title: string; body: string }[]) =>
          _.shuffle(res)
        ),
  });

  //isLoading은 최초 데이터 패칭 시 true, 새로고침 시 계속 로딩 나옴
  //isPending은 캐시된 데이터말고 데이터가 패칭 중일 때, 
  //isFetching은 데이터가 패칭 중일때, 할 때마다 상태값이 바뀜
  if (isPending) return <div>Loading...</div>;
  return (
    <ul className="p-4 flex flex-col gap-2">
      <button
        onClick={() => queryClient.invalidateQueries({ queryKey: ["about"] })}//여기!!!! queryclient 사용
        className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 active:bg-blue-700 transition-colors duration-200"
      >
        새로고침
      </button>
      {data?.map((item: { id: number; title: string; body: string }) => (
        <li key={item.id} className="border rounded p-4 flex flex-col gap-2">
          <h1 className="text-2xl font-bold">{item.title}</h1>
          <p className="text-sm">{item.body}</p>
        </li>
      ))}
    </ul>
  );
};

export default AboutPage;

 

 

 

과제 두두둥장

react-query 이용해서 Todo List 만들기

1. 생성 / 수정 / 삭제
2. 조회
3. 완료 처리
4. 모두 삭제
5. 전체 완료 처리
6. 전체 삭제

React Query, Local Storage 이용해서 구현

예시)
const getTodoList = async =() =>{
  const todoList = localStorage.getItem("todoList);
  return todolist ? Json.parse(todoList) : [] ;
}