코딩/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) : [] ;
}