묻고 답해요
156만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
해결됨Next + React Query로 SNS 서비스 만들기
SignupModal 타입 에러에 관해서 질문드립니다!
SignupModal 에서 에러가 하나 나오는데 관련해서 질문드립니다.S2322: Type '(formData: FormData) => Promise<{ message: string; } | undefined>' is not assignable to type 'NonNullable<string | ((formData: FormData) => void | Promise<void>) | undefined>'.<br/>Type '(formData: FormData) => Promise<{ message: string; } | undefined>' is not assignable to type '(formData: FormData) => void | Promise<void>'.<br/>Type 'Promise<{ message: string; } | undefined>' is not assignable to type 'void | Promise<void>'.<br/>Type 'Promise<{ message: string; } | undefined>' is not assignable to type 'Promise<void>'.<br/>Type '{ message: string; } | undefined' is not assignable to type 'void'.<br/>Type '{ message: string; }' is not assignable to type 'void'.이런 에러가 나옵니다. formData 의 타입이 정확하지 않아서 생기는 에러 같습니다. 근데 정상적으로 실행은 됩니다.제 꺼에선 이렇게 에러가 나오는데 제로초님 화면에서는 에러가 나오는 것 같지 않아서 어떻게 그렇게 된 것인지 궁금해서 질문 남깁니다. 감사합니다.
-
미해결Next + React Query로 SNS 서비스 만들기
usePathname 과 /explore 페이지 부분은 좀 너무 빠르네요...
밑에 수업노트에 주신 링크도 그 파일이 아니라 깃헙 레포로 연결되고, 너무 빠르네요 ㅠㅠ _component 부분 보면 RightSearchZone 컴포넌트도 없어서 좀 헤매게 되는 것 같습니다...css 내용도 갑자기 어디서 튀어나온건지 좀 파악이 어렵네요 ㅠ
-
미해결Next + React Query로 SNS 서비스 만들기
[next15] msw 질문드립니다.
기존에 강의로 msw로 코드만들었던 수강생입니다.여러개의 질문과 에러?라고할만한 사항이 있는데요.기존강의에는 http.js 파일에서 express 기반으로 서버를 만들어서 9090포트를 사용했는데요. 새로 강의하신 내용에서는 express를 배제하고 코드가 간결하게 바꿨는데 이렇게 바꾼 내용의 대해서 조금 설명 부탁드립니다. 간략히 알기로는 기존 MSW는 클라이언트(브라우저)에서 동작하기 때문에 서버에서 html 요청이 발생하기 때문에 요청을 가로챌수 없기 때문에 express썻다고 알고있습니다. 이게 맞을까요? 그리고 바뀐코드는 이런문제가 해결이 된건가요?1번의 문제가 해결되었다면, 기존의 9090 서버포트와 더불어 npm run mock를 실행하여 서버를 동작했던걸로 알고있는데 이기능은 필요한가요?msw 세팅과 .env 강의부분에서 환경변수를 api에 직접 넣도록 수정했는데, 이전꺼랑 차이가 뭔가요? 추가로 클라이언트컴포넌트에서는 환경변수가 콘솔로 읽히고있는데 handler에서는 읽히지 않고있습니다. 코드는 이전강의 코드(백엔드 db연결전까지)와 같고요. 추가로 nextjs나 react를 버전업을 하지는 않았습니다. 이유가뭔지 유추해주실수있을까요?
-
미해결Next + React Query로 SNS 서비스 만들기
새로 올라온 강의 영상에 문제가 있는거 같습니다.
"섹션4 그런데 백엔드 개발자가 API를 아직 못 만들었다"의 "msw 세팅과 .env" 강좌가 8분 38초까지 영상이 검은 화면으로 나옵니다. 해당 영상에서 검은 화면 중간 중간 노란색 안내 문구는 나오는 것과 다른 영상에서는 해당 증상이 보이지 않은 것으로 보아서 제 환경에서 해결할 수 없는 문제로 보이는데, 확인 한번 부탁드립니다.
-
미해결Next + React Query로 SNS 서비스 만들기
게시글 삭제하기
안녕하세요 제로초님! 게시글 삭제하기 기능을 구현하고 있습니다!구현해도 안되길래 스웨거가서 테스트를 하고 서버를 봤더니 오류가 나더라구요혹시 이유를 알 수 있을까요??
-
미해결Next + React Query로 SNS 서비스 만들기
구글 로그인 구현하기
안녕하세요 제로초님! 완강하고 지금 구글 로그인 구현중입니다.구글로 로그인하다가 마지막에 인가토큰받는 부분에서 404 에러가나는데 이유를 모르겠습니다... 도와주세요 ㅠ_ㅠ
-
미해결Next + React Query로 SNS 서비스 만들기
msw 쿠키 관련 궁금증!
안녕하세요, 강좌들으며, msw를 프로젝트에 도입하다 궁금한점이 있어 남깁니다!현재 위와 같이 코드를, 작성하여, 로그인 성공시, 위와 같이, 데이터와, 쿠키를 동시에 세팅해주고 있는 상황입니다!실제로 아래와 같이, response들은 잘 넘어오나response headers에 쿠키가, 오지 않아, 실제로 스토리지에 쿠키가 담기지 않습니다!원래, 가상으로 심어주는 쿠키는, 브라우저의 쿠키에 담기지 않는건가요?? 궁금하여 질문남깁니다!!좋은 강의 감사합니다!
-
미해결Next + React Query로 SNS 서비스 만들기
Nextjs dev모드 오류
nextjs dev모드로 실행하다가 코드 저장하면 자꾸 서버 다운될 때도 많고, 코드 새로 수정해도 반영 안 될 때가 있어서, 서버 껐다 키는 경우가 빈번한데.. 이런거 해결하는 방법 없나요.. 제 컴퓨터에서는 nextjs가 너무 무거워서 리로드 하는 시간도 너무 오래 걸려서요 ㅠㅠ
-
미해결Next + React Query로 SNS 서비스 만들기
쿠키 로그인 방식에 대한 궁금한 점이 있습니다!
서버 배포후 제공해주는 쿠키의 domain이, .naver.com이고, 만약 로컬호스트로 개발 할 경우 localhost의 도메인을 갖게되어, 둘의 도메인이 달라, 쿠키가 담기지 않는 경우가 발생합니다. 모든 통신 요청을 서버와 직접 진행 할 경우, .naver.com의 쿠키로, 모든것이 공유가 가능하니, 정상적으로 접근이 가능한 것 같은데이제 간혹, 라우트 핸들러나, 미들웨어를 거쳐서, 통신 할 경우 도메인이 localhost가 되어, 401 에러가 발생하게 됩니다!!이를 해결하기 위해, 로컬에서 두개의 서버를 띄워서 테스트하는 방법말고, 보통 실무나, 현업에서는 배포된 쿠키 로그인 방식을 활용할 떄, 어떻게 Next.js에서 환경 구성을 하는지, 로컬호스트가 아닌, 다른 도메인으로 띄울 수 있는 방법이 있는지 궁금합니다.
-
해결됨Next + React Query로 SNS 서비스 만들기
배포하신 깃헙 auth.ts signOut 질문입니다
https://github.com/ZeroCho/z-com/blob/master/src/auth.ts#L16배포하신 깃헙에서 궁금한점이 생겨서 질문드립니다1. signOut에서 백엔드로 로그아웃 요청만 보내고 authorize()처럼 쿠키값을 set해주지 않았으니 connect.sid는 남아있는게 맞나요? 2. next auth 세션값이 지워지면서 프론트서버와 인증이 끊기는데 프론트서버에서 굳이 백엔드로 요청을 보내는 이유가 뭘까요? 백엔드 서버에 로그 남기기 위해서인가요?
-
미해결Next + React Query로 SNS 서비스 만들기
form으로 put, patch, delete 요청이 안되는걸로 알고있습니다.
form으로 put, patch, delete 요청이 안되는걸로 알고있는데그럼 msw로 목업 요청을 작성할때랑 실제 서버를 작성할때랑 컴포넌트를 두개만들어서 해야하나요?
-
미해결Next + React Query로 SNS 서비스 만들기
next-auth의 refresh token rotation에 관한 질문입니다.
안녕하세요 next-auth로 토큰 갱신 로직을 개발하는 중에 해결되지 않는 문제가 발생하여 질문 드립니다.현재 클라이언트에서 api 요청을 하면 middleware에서 const token = await auth(); 이런식으로 세션 정보를 가져와 accessToken을 헤더에 삽입하는 방식을 구현하였습니다. if (request.nextUrl.pathname.startsWith("/gateway")) { const session = await auth(); const accessToken = session?.accessToken; const { device } = userAgent(request); const localeFromCookie = cookieStore.get("dp_lang")?.value as string; const defaultLocale = getLanguageCodeFromLocale(localeFromCookie); request.headers.set("Accept", "*/*"); request.headers.set("Authorization", `Bearer ${accessToken}`); request.headers.set("Access-Control-Allow-Origin", "*"); request.headers.set("deviceType", "1"); request.headers.set("deviceId", getDeviceIdFromCookie(cookieStore)); request.headers.set("osType", getOsTypeFromCookie(cookieStore)); request.headers.set("User-Agent", device.model ?? ""); request.headers.set("locale", localeFromCookie); request.headers.set("language", defaultLocale); const response = NextResponse.next({ request: { headers: request.headers, }, }); return response; } 따라서 미들웨어에서 세션에 접근하는 순간에 jwt콜백이 실행되어 jwt 토큰에 저장되어 있는 토큰 expire 시간을 비교하여 만료여부를 판단 한 뒤에 토큰 갱신이 되도록 구현을 하였습니다. async jwt({ token, account, user }) { // Initial sign in if (account && user) { return { accessToken: user.accessToken, expiresAt: new Date( Date.now() + (user?.expiresIn ?? 0) ).toISOString(), refreshToken: user.refreshToken, serviceAvailable: true, }; } // Return previous token if the access token has not expired yet if ( new Date() < new Date(new Date(token.expiresAt as string).getTime() - 5000) ) { console.log("@@@@@@valid"); return token; } else { if (token.error === "RefreshTokenInvalid") { return token; } // Access token has expired, try tㄴo update it try { console.log("@@@@@@expired"); const refreshedTokens = await authApi.refreshAuthToken({ refreshToken: token.refreshToken as string, accessToken: token.accessToken as string, }); console.log("==========token refreshed========"); // reissue token return { ...token, accessToken: refreshedTokens.accessToken, expiresAt: new Date( Date.now() + (refreshedTokens.expiresIn ?? 18000) ).toISOString(), refreshToken: refreshedTokens.refreshToken ?? token.refreshToken, error: null, }; } catch (error) { console.error("Token refresh failed", error); if (error.status === RestApiErrorType.invalidTokenException) { return { ...token, error: "RefreshTokenInvalid", }; } return { ...token, error: "RefreshAccessTokenError" }; } } }, 기존에는 세션에 접근을 할때 next-auth의 jwt 콜백이 돌고 값이 리턴되면 쿠키의 session-token값에 암호화된 값들이 업데이트 되는 걸로 알고 있었는데 찾아보니 현재 세션에 접근하고 갱신하는 부분이 서버단에 있어서 쿠키가 업데이트 되지 않는다고 합니다. 따라서 현재 토큰이 만료된 후 한번에 3가지의 api 콜을 요청했을때 첫번째 api요청에서는 정상적으로 토큰 refresh가 요청이 되며 middleware에서 세션에 접근해 갱신된 토큰이 가져와져 요청이 성공되나 그 뒤 요청부터는 쿠키에 업데이트가 되지 않아서인지 세션에서 갱신 전 토큰이 가져와지는 현상이 발생하고 있습니다.클라이언트에서 세션 업데이트 후 세션에 접근을 해야 쿠키값이 갱신이 된다고 하는데 다른 방법이 있는지 문의드립니다.
-
미해결Next + React Query로 SNS 서비스 만들기
nextjs middleware에서 쿠키 업데이트에 관한 질문입니다.
안녕하세요 프로젝트 진행 중에 해결되지 않는 부분이 있어 질문 드립니다.middleware에서 쿠키의 값을 업데이트 하려고 하는데 업데이트 되지 않는 현상이 발생하고 있습니다.아래의 코드와 같이 NextResponse.next() 실행 후에 response에 쿠키를 업데이트를 하려 하는데 반영이 되지 않습니다. request에서 세팅해도 마찬가지입니다. 쿠키 세팅이 되지 않는 원인에 대해 알고 계신가요?? export default async function middleware(request: NextAuthRequest) { if (request.nextUrl.pathname.startsWith("/gateway")) { const token = await getToken({ req: request, secret: process.env.AUTH_SECRET as string, secureCookie: process.env.NODE_ENV === "production", }); const accessToken = token?.accessToken; const { device } = userAgent(request); request.headers.set("Accept", "*/*"); request.headers.set("Authorization", `Bearer ${accessToken}`); request.headers.set("Access-Control-Allow-Origin", "*"); request.headers.set("deviceType", "1"); request.headers.set("User-Agent", device.model ?? ""); request.headers.set("locale", localeFromCookie); request.headers.set("language", defaultLocale); const response = NextResponse.next({ request: request.headers }); response.cookies.set("test", "test"); return response; } ... }
-
미해결Next + React Query로 SNS 서비스 만들기
searchParams 질문있습니다.
const onClickHot = () => { setCurrent('hot'); router.replace(`/search?q=${searchParams.get('q')}`) } 이 코드가 있으면 q=null일때 주소가 http://localhost:3000/search?q=null이렇게 이동되는데 제로초님은 아무런 검색어가 없는데 어떻게 null로 안나오죠?null체크를해서 주소를 2개 분기해도 될거같긴한데 제로초님은 q가 null일땐 주소창의 쿼리가 안생기더라고요 . 뭐가다른건지 모르겠네요ㅠ
-
미해결Next + React Query로 SNS 서비스 만들기
error.js 와 loading.js 에 대한 질문이 있습니다.
Next에서 자체적으로 제공하는 에러 바운더리인 error.js 와 서스펜스인 loading.js는 '서버 컴포넌트'의 에러와 로딩만 처리하는 것인가요? 예를 들어 서버 컴포넌트인 page.tsx의 하위 클라이언트 컴포넌트에서 에러가 발생했을 때는 error.js에서 캐치가 안 되는 것인가요...?
-
해결됨Next + React Query로 SNS 서비스 만들기
route handlers 에 대한 질문이 있습니다
안녕하세요, app router에 대해 계속 공부하다가 route handlers 에 대한 궁금증이 해결되지 않아 질문하게 됐습니다. Next app router에서 정확히 route handlers를 사용해야 하는 이유가 무엇인가요?제가 생각했을 땐 서버 데이터 캐싱이나 API 엔드포인트를 숨길 수 있다는 장점이 있는데 이건 서버 컴포넌트에서 fetch하는 것으로도 대체가 되는데 route handlers를 사용해야 하는 특별한 이유가 따로 있는 것인가요? 모든 API를 route handlers로 하면 Next서버에 부하가 걸릴텐데 어떻게 해결할 수 있을까요? 이 부분은 공식문서에서 제가 못 찾은 것 같은데, 만약 외부 백엔드 API가 있고 여기에 데이터 요청을 할 때클라이언트 컴포넌트에서의 모든 API 요청을 1차로 route handler에 하고 여기서 외부 백엔드 API로 요청하게되면 route handlers에 요청이 몰리게 되는데 이때 Next 서버에 걸리는 부하를 어떻게 해소할 수 있을까요?
-
미해결Next + React Query로 SNS 서비스 만들기
hydration 에러
제로초님 하신대로 그대로 프로젝트를 생성하고 실행했더니 화면은 그대로 뜨는데 에러가 1개 있어서 봤더니 hydration 에러라고 뜨네요 ㅠㅠ 지금 z-com 프로젝트를 수업용 / 연습용으로 총 2개 진행중인데 수업용 프로젝트는 이런 에러가 없었는데 연습용에서 에러가 발생하네요. 아래는 에러 코드입니다 ! Console ErrorHydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used- A server/client branch if (typeof window !== 'undefined'). - Variable input such as Date.now() or Math.random() which changes each time it's called. - Date formatting in a user's locale which doesn't match the server. - External changing data without sending a snapshot of it along with the HTML. - Invalid HTML tag nesting. It can also happen if the client has a browser extension installed which messes with the HTML before React loaded
-
해결됨Next + React Query로 SNS 서비스 만들기
배포 환경에서 request.nextUrl.origin 질문
안녕하세요 nextjs를 활용해서 프로젝트를 진행하는 중에 해결되지 않는 문제가 발생하여 질문 드립니다.현재 도커를 활용해서 nextjs를 배포하고 있는데middleware에서 request.nextUrl.origin값이 https://localhost:3000이 들어가고 있습니다.x-forwarded-host를 찍었을때는 제가 사용하는 https://test.domain.com이 들어가는데 request.nextUrl.origin에 x-forwared-host와 같은 값이 들어가도록 설정하기 위한 방법이 있는지 알 수 있을까요??
-
해결됨Next + React Query로 SNS 서비스 만들기
두개의 차이점
안녕하세요, 아래 ISR에 대해서 질문드렸었는데, nextjs 에서 2가지 구현 방법이있다고 해서두가지가 어떤 차이점이 있고 어떻게 활용하는게 좋을지 궁금해서 질문드립니다!1번 방법 = fetch API 의 revlidate를 활용하여 ISR 구현import ProductList from "@/component/ProductList"; import { getQueryClient } from "@/component/TanstackQueryOption"; import { getProducts } from "@/fetch/getProducts"; import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query"; import Image from "next/image"; export default async function ProductPage () { const queryClient = getQueryClient(); await queryClient.prefetchQuery({ queryKey:['products'], queryFn: getProducts, }) return ( <> <section className='visual-sec'> <Image src="/visual.png" alt="visual" width={1920} height={300}/> </section> <section className="product-sec"> <h2>상품 리스트</h2> <HydrationBoundary state={dehydrate(queryClient)}> <ProductList /> </HydrationBoundary> </section> </> ) }import { isServer, QueryClient, defaultShouldDehydrateQuery, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 10 * 1000, }, dehydrate: { shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', }, }, }) } let browserQueryClient: QueryClient | undefined = undefined export function getQueryClient() { if (isServer) { return makeQueryClient() } else { if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } }'use client' import { getQueryClient } from '@/component/TanstackQueryOption'; import { QueryClientProvider, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactNode } from 'react' export default function TanstackQueryProvider({ children }: { children: ReactNode }) { const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools initialIsOpen={true} /> </QueryClientProvider> ) } export const getProducts = async () => { const res = await fetch(`http://localhost:9090/api/products`, { method: "GET", headers: { "Content-Type": "application/json", }, next: { revalidate: 10, } }); const data = await res.json(); const currentTime = new Date().toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3, hour12: false, }); if (typeof window === "undefined") { console.log('fetch products', 'server', currentTime); console.table(data); } else { console.log('fetch products', 'client', currentTime); console.table(data); } if(!res.ok) { throw new Error("Failed to fetch products"); } return data; }2번 방법 = export const revalidate 로 시간 설정 후 fetch로 받은 data 값을 바로 렌더링 시키기 import Product from "@/component/Product"; import styles from '@/component/ProductList.module.css'; import Image from "next/image"; export const revalidate = 10; export default async function Product2Page() { const data = await fetch('/api/products'); const products = await data.json(); console.log(revalidate, 'Product2Page'); return ( <> <section className='visual-sec'> <Image src="/visual.png" alt="visual" width={1920} height={300}/> </section> <section className="product-sec"> <h2>상품 리스트</h2> <div className={styles.productList}> {products.map((product: any) => ( <Product key={product.item_no} product={product} /> ))} </div> </section> </> ) } 2개다 ISR로 구현되며, 1번 방법은 Data Cache 캐싱 매커니즘을 활용하고,2번 방법은 Full Router Cache 캐싱 매커니즘을 활용한다의 차이점으로 생각이 드는데,이 외에 다른 차이점과 실제 개발을 하면서 선호되는 방식이 따로 있을까요? 해당 페이지만 보았을때는 2번 방법으로 해도 어차피 주기적으로 다시 생성해서 최신 데이터를 반영하면 복잡하게 react-query를 사용하고, Hydration을 하면서 데이터를 동기화할 필요가 있나 싶어서 어떻게 사용해야할지 감이 안잡히네여
-
해결됨Next + React Query로 SNS 서비스 만들기
ISR 테스트 중 궁금점
// src/components/TanstackQueryOption.ts import { isServer, QueryClient, defaultShouldDehydrateQuery, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 15 * 1000, }, dehydrate: { shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', }, }, }) } let browserQueryClient: QueryClient | undefined = undefined export function getQueryClient() { if (isServer) { return makeQueryClient() } else { if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } }// src/components/TanstackQueryProvider.tsx 'use client' import { getQueryClient } from '@/component/TanstackQueryOption'; import { QueryClientProvider, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactNode } from 'react' export default function TanstackQueryProvider({ children }: { children: ReactNode }) { const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools initialIsOpen={process.env.NEXT_PUBLIC_MODE === 'local'} /> </QueryClientProvider> ) } // src/app/layout.tsx import Banner from "@/component/Banner"; import Footer from "@/component/Footer"; import Header from "@/component/Header"; import TanstackQueryProvider from "@/component/TanstackQueryProvider"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "@/app/global.css"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="en"> <body> <TanstackQueryProvider> <div className='container'> <Banner/> <Header/> <main>{children}</main> <Footer/> </div> </TanstackQueryProvider> </body> </html> ); } // src/app/page.tsx import ProductList from "@/component/ProductList"; import { getQueryClient } from "@/component/TanstackQueryOption"; import { getProducts } from "@/fetch/getProducts"; import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query"; import Image from "next/image"; export default function Page () { const newQueryClient = getQueryClient(); newQueryClient.prefetchQuery({ queryKey:['products'], queryFn: getProducts, }) return ( <> <section className='visual-sec'> <Image src="/visual.png" alt="visual" width={1920} height={300}/> </section> <section className="product-sec"> <h2>상품 리스트</h2> <HydrationBoundary state={dehydrate(newQueryClient)}> <ProductList /> </HydrationBoundary> </section> </> ) }; 'use client' // src/components/ProductList.tsx import Product from "@/component/Product"; import { getProducts } from "@/fetch/getProducts"; import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; import styles from "@/component/ProductList.module.css"; export const ProductList = () => { const {data, isLoading, isFetching} = useSuspenseQuery({queryKey: ['products'], queryFn: getProducts}); console.log(`isLoading: ${isLoading}, isFetching: ${isFetching}`) return ( <div className={styles.productList}> {data?.map((product: any) => ( <Product key={product.item_no} product={product} /> ))} </div> ) }; export default ProductList;// src/components/Product.tsx import Link from "next/link"; import Image from "next/image"; export const Product = ({product} : any) => { return ( <Link href={`/product/${product.item_no}`} prefetch> <Image src={product.detail_image_url} alt={product.item_name} width={500} height={300} /> <h3>{product.item_name}</h3> <span>{product.price}</span> </Link> ) } export default Product;// src/app/product/[id]/page.tsx export default function ProductDetailPage() { return ( <> 상품 상세페에지 </> ) }// src/fetch/getProducts.ts export const getProducts = async () => { const res = await fetch(`http://localhost:9090/api/products`, { method: "GET", headers: { "Content-Type": "application/json", }, next: { revalidate: 10, } }); const currentTime = new Date().toLocaleTimeString(); const data = await res.json(); if (typeof window === "undefined") { console.log('fetch products', 'server', currentTime); console.table(data); } else { console.log('fetch products', 'client', currentTime); console.table(data); } if(!res.ok) { throw new Error("Failed to fetch products"); } return data; }// src/server/server.js import express from "express"; import cors from "cors"; const app = express(); const port = 9090; app.use(cors()); app.use(express.json()); app.get("/api/products", (req, res) => { const currentTime = new Date().toLocaleTimeString(); console.log(`Received request at ${currentTime}`); const products = [ { item_no: 122997, item_name: '상품 1', detail_image_url: 'https://picsum.photos/id/237/500/500', price: 75000, }, { item_no: 768848, item_name: '상품 2', detail_image_url: 'https://picsum.photos/id/238/500/500', price: 42000, }, { item_no: 552913, item_name: '상품 3', detail_image_url: 'https://picsum.photos/id/239/500/500', price: 240000, }, // { // item_no: 1045738, // item_name: '상품 4', // detail_image_url: // 'https://picsum.photos/id/240/500/500', // price: 65000, // }, ]; res.json(products); }); app.listen(port, () => console.log('Server is running')); 안녕하세요, fetch와 tanstackQuery를 사용해서 ISR 동작을 테스트하고있었습니다.테스트 마다 .next 파일은 지우고 새로 build 하여 run start를 통하여 확인하였습니다.staleTime과 revalidate 의 시간이 서로 상이한데, 동일하게 설정했을때, 시간의 간격을 두었을때의 차이점을 직접 확인하려고 하였는데 어떤점에서 차이가 나는지 보고도 이해가 안가서 질문드립니다.궁금점 1. staleTime과 revalidate 는 gcTime 처럼 staleTime이 revalidate보다 적은 시간으로 설정을 해야하는지? 그렇다면 그 이유는 gcTime보다 작게 설정하는 이유와 같은지? 가 궁금합니다.2. server.js에 주석처리해놓은 item을 다시 주석을 해지하면 처음 revaildate의 10초 설정으로 인해새로고침을해도 아이템은 계속 페이지에서 3개만 노출되고있고, 상품을 클릭해서 이동을 하면서staleTime의 설정인 15초가 되었을때는 client 요청이 발생하여 아이템이 4개로 잘 노출되고있습니다.하지만 이 때 새로고침을 하게되면 처음 fetch revalidate로 cache되어있던 데이터인 아이템 3개까지만 노출이 되고 새로고침을 한번 더 진행해야 그때서야 4개로 노출이되는데 클라이언트와 서버 쪽이 서로 싱크가 안맞는거같은데 이러한 문제점이 왜 일어나는지 이해가 잘안됩니다!3. 확장된 fetch와 tanstackQuery를 어떻게 분리해서 사용해야할까도 많이 고민이 되는데.. queryFn 에 이미 fetch로 만들어둔 함수를 가져와 사용하니 분리라는 개념을 생각하면 안되는걸까요? fetch를 독립적으로 사용하는 경우도있다고하는데 이 경우는 왜 독립적으로 사용하는지 잘모르겠습니다.