use client 사용
)use client 사용 금지
)Image
를 이용
🔗 웹사이트 : https://pokemon-app-six-rosy.vercel.app/
🔗 깃허브 링크 : https://github.com/yeliinbb/pokemon-app
기존 방식처럼 const queryClient = new QueryClient()
이렇게 컴포넌트 안 혹은 밖에서 생성해줄 수도 있다. 하지만 이와 다르게 queryClient를 상태로 관리하는 이유는 컴포넌트가 렌더링될 때마다 queryClient 인스턴스를 재생성하지 않기 위해서이다.
useState 훅에서 초기 상태를 설정할 때, 콜백 함수를 이용하면 이 콜백함수는 컴포넌트가 처음 렌더링될 때 한 번만 실행된다. 이후 상태가 변경되지 않는 한, 이 콜백함수는 다시 실행되지 않는다.
따라서 QueryClient는 컴포넌트의 첫 번째 렌더링 시에만 생성되며, 이후 컴포넌트가 리렌더링되더라도 동일한 QueryClient
인스턴스를 사용하게 된다.
defaultOptions은 QueryClient를 생성할 때 기본 쿼리 동작을 설정할 수 있는 옵션이다. 이를 통해 애플리케이션 전역에서 모든 쿼리에 공통적으로 적용할 기본 설정을 정의할 수 있다.
데이터를 받아오는 빈도수를 낮추기 위해 staleTime을 높게 설정하여 api호출 횟수를 줄일 수 있다.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// QueryClient를 생성하고 기본 옵션을 설정합니다.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5분동안 데이터는 "신선" 상태로 유지
cacheTime: 1000 * 60 * 10, // 10분
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchInterval: 1000 * 60, // 1분
retry: 3, // 기본값
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
enabled: true, // 특정 조건에서만 실행할 경우 false
onSuccess: data => console.log('쿼리 성공:', data),
onError: error => console.error('쿼리 실패:', error),
onSettled: (data, error) => console.log('쿼리 완료')
},
mutations: {
// 변이가 성공적으로 완료되었을 때 호출되는 콜백 함수
onSuccess: data => console.log('변이 성공:', data),
// 변이가 실패했을 때 호출되는 콜백 함수
onError: error => console.error('변이 실패:', error),
// 변이가 성공하거나 실패한 후 항상 호출되는 콜백 함수
onSettled: (data, error) => console.log('변이 완료')
}
}
});
// QueryClientProvider를 사용하여 QueryClient를 애플리케이션에 제공합니다.
const Providers = ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
export default Providers;
staleTime은 데이터가 "오래된" 것으로 간주되기까지의 시간을 설정. 즉, 데이터가 "신선한" 상태로 유지되는 시간을 정의.
cacheTime은 데이터가 캐시에 유지되는 시간을 설정. 이 시간이 지나면 데이터는 가비지 콜렉션의 대상이 됨.
"use client";
import React, { PropsWithChildren, useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
// 전체 provider 관리 컴포넌트
const Providers = ({ children }: PropsWithChildren) => {
// useState안에 콜백 함수로 넣어줄 경우
// 컴포넌트가 처음에 마운트될 때만 초기값이 세팅되고 이후에는 리렌더링 되지 않는다.
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};
export default Providers;
import type { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: { id: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params, searchParams }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
// read route params
const id = params.id
// fetch data
const product = await fetch(`https://.../${id}`).then((res) => res.json())
// optionally access and extend (rather than replace) parent metadata
const previousImages = (await parent).openGraph?.images || []
return {
title: product.title,
openGraph: {
images: ['/some-specific-page-image.jpg', ...previousImages],
},
}
}
export default function Page({ params, searchParams }: Props) {}
title 객체 안에 template과 default 속성을 이용하여, 각 페이지마다 다른 title을 보여줄 수 있다.
title: { template: "%s | Pokemon app", default: "Pokemon app", },
template 속성의 "%s | Pokemon app"에서
%s
는 문자열 포맷팅을 위한 자리 표시자(placeholder)이며, 이는 나중에 특정 문자열을 삽입할 위치를 나타낸다.이 구문은 종종 템플릿 문자열(template string)이나 포맷팅 문자열(format string)이라고 불리며, 동적으로 텍스트를 삽입할 때 사용된다.
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Providers from "./_providers";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: {
template: "%s | Pokemon app",
default: "Pokemon app",
},
description: "Let's go Pokemon!!!",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning={true}>
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
}
export async function generateMetadata({ params: { id } }: DetailPageProps) {
const pokemon: PokemonWithLike = await fetchPokemon(id)
return {
title: pokemon.korean_name,
description: `${pokemon.korean_name} : ${id}`,
}
}
렌더링 하려는 페이지 폴더 안에서 layout, page, error 과 마찬가지로 loading 파일을 만들어준다. 이 때 주의할 점은 파일 이름은 변경하면 안 되며, 서버 컴포넌트에서만 사용 가능하다. 클라이언트 컴포넌트에서 적용이 되지 않는 이유는 데이터의 로딩 상태를 자동으로 확인할 수 없기 때문이다.
이 때 suspense로 감싸준 컴포넌트를 제외하고, 나머지 컴포넌트는 로딩 중에 미리 그려진다.
export default function Loading() {
// You can add any UI inside Loading, including a Skeleton.
return <LoadingSkeleton />
}
Object literal may only specify known properties, and liked does not exist in type 'AxiosRequestConfig any’
사실 좋아요 기능에서는 기존 데이터에 좋아요 속성을 업데이트해서 저장해야하기 때문에 patch매서드를 사용해야하지만, axios에 대해 공부해보고자 아래 내용을 적어본다.
mutationFn: async ({ id, currentLiked }: MutationVariables) => {
await axios.get(`/api/pokemons/${id}`, { liked: !currentLiked },
);
},
mutationFn: async ({ id, currentLiked }: MutationVariables) => {
await axios.get(`/api/pokemons/${id}`, {
params: { liked: !currentLiked }
});
},
GET 요청에서는 데이터(payload)를 본문에 포함하지 않으므로, 이 경우 해당 객체는 쿼리 매개변수나 요청 구성을 포함해야 한다. 따라서 { liked: !currentLiked }
이런 형태가 아닌, { params: { liked: !currentLiked }}
형태로 요청을 보내야 한다.
path/put/post 등 body를 함께 보내야하는 경우에는 추가하거나 업데이트 할 객체를 보내는 것이기 때문에 기존의 객체 형태로 보내는 것이 가능하다.
요청 구성 객체 (Request Config Object)
쿼리 매개변수 (Query Parameters)
axios.get(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
axios.delete(url[, config])
axios.request(config)
axios.head(url[, config])
axios.options(url[, config])
axios.getUri([config])
mutationFn: async ({ id, currentLiked }: MutationVariables) => {
// like 기능 구현 시, 해당 데이터만 업데이트해주는 것이기 때문에 put(덮어주기) 메서드 사용 권장
// liked된 데이터를 저장해주는 공간 필요 -> 로컬 스토리지 or supabase 사용해서 저장 기능 구현 시도
// 현재 이 로직은 작동하고 있지 않음 -> 따로 변경된 데이터를 저장해주고 있지 않기 때문에
// await axios.patch(`/api/pokemons/${id}`, {
// params: { liked: !currentLiked },
// })
// 이런 식으로 쿼리스트링으로 보내는 방법도 있음.
// const newLikedState = !currentLiked ? 'true' : 'false';
// await axios.get(`/api/pokemons/${id}/liked=${newLikedState}`)
},
우와 대박 예린님 퀄리티 장난아니네요...👍 👍 👍