앱 라우트 next 13이 안정화된 후 사용해보지 못해, 언젠가는 사용하지 않을까 하는 생각에 직접 사용해보면서 이를 정리해보려고 합니다.
🙈 직접 해보고 제가 이해하고 있는선에서 정리 하는거라 잘못된 부분이 있다면 피드백 해주시면 적극 반영하겠습니다
예제는
여기서👈👈 확인 가능합니다
next 공식 홈페이지 Data Fetching 하는 방법은 총 4가지라고 설명하고 있습니다. 4가지 전부 사용해보도록 하겠습니다!
fetch
API 를 사용하여 서버에서 데이터 가져오기fetch
API 를 사용하여 서버에서 데이터 가져오기App Router
에서는 이제 getStaticProps
, getServerSideProps
를 더 이상 사용하지 않습니다.
Page Router
와 비교하여 이를 어떻게 사용해야 하는지 살펴보겠습니다.
AS-IS getStaticProps
interface Props {
posts:Posts[]
}
const Page = (props:Props) => {
const { posts } = props
return (
<main>
<ul className="flex flex-col gap-30">
{posts?.map((item) => {
return (
<li key={item.id}>
<Link href={`/ssr/${item.id}`}>
<h1>{item.title}</h1>
<p>{item.body}</p>
</Link>
</li>
);
})}
</ul>
</main>
);
};
export const getStaticProps:GetStaticProps = async () => {
const posts = (await fetch(`https://jsonplaceholder.typicode.com/posts`).then((res) => res.json())) as Posts[];
return {
props:{
posts
}
}
}
TO-BE
기본적으로 next는 fetch API init option에 force-cache 값을 넣어주면, fetch의 반환 값을 서버의 데이터 캐시에 자동으로 캐시하도록 되어 있습니다. fetch의 cache 기본값은 force-cache 입니다.
// 'force-cache' 가 기본값이므로 생략 가능합니다.
fetch('https://...', { cache: 'force-cache' })
App Router 의 getStaticProps
는 아래와 같습니다.
❗❗ 타입스크립트가 포함된 서버 컴포넌트에서 async
/await
을 사용하려면 TypeScript 5.1.3
이상 및 @types/react 18.2.8
이상 사용해야 합니다.
// page.tsx
export default async function Page() {
const posts = (await fetch(`https://jsonplaceholder.typicode.com/posts`).then((res) => res.json())) as Posts[]
return (
<main>
<ul className="flex flex-col gap-30">
{posts?.map((item) => {
return (
<li key={item.id}>
<Link href={`/ssg/${item.id}`}>
<h1>{item.title}</h1>
<p>{item.body}</p>
</Link>
</li>
);
})}
</ul>
</main>
);
}
정상적으로 Caching 이 되었다면 런타임 환경 log에 cache 가 HIT 가 된걸 확인 가능합니다.
AS-IS ISR
기존에는 getStaticProps에서 반환할 때 revalidate를 추가하여 재검증 시간을 지정했습니다.
export const getStaticProps: GetStaticProps = async ({ params }) => {
const posts = (await fetch(`https://jsonplaceholder.typicode.com/posts`).then((res) => res.json())) as Posts[]
return {
revalidate: 3600,
props: {
posts
},
};
};
TO-BE
APP Router에서는 Fetch API에 revalidate 옵션 값을 추가하여 재검증 시간을 지정할 수 있도록 변경되었습니다.
또는 Route Segment Config 을 사용하도록 되어있습니다
// page.tsx
export default async function Page() {
const posts =
(await fetch(`https://jsonplaceholder.typicode.com/posts`,
{ next: { revalidate: 3600 } }).then((res) => res.json())) as Posts[]
return (
<main>
<ul className="flex flex-col gap-30">
{posts?.map((item) => {
return (
<li key={item.id}>
<Link href={`/ssg/${item.id}`}>
<h1>{item.title}</h1>
<p>{item.body}</p>
</Link>
</li>
);
})}
</ul>
</main>
);
}
generateStaticParams
APP Router에 새로 추가된 기능인 generateStaticParams
입니다. getStaticPaths
를 대체하는 기능입니다.
AS-IS getStaticPaths
export const getStaticPaths: GetStaticPaths = async () => {
const data = (await fetch(`https://jsonplaceholder.typicode.com/posts`).then((res) => res.json())) as Posts[];
const paths = data.map((res) => ({ id: String(res.id) }));
return { paths, fallback: "blocking" };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const posts = await fetch(
`https://jsonplaceholder.typicode.com/posts/${params.id}`,
).then(res=>res.json());
return {
revalidate: 3600,
props: {
posts
},
};
};
TO-BE
약간 다른 부분이 있다면 getStaticPaths
에서 blocking | true
옵션을 주어 페이지가 존재하는지 확인할 수 있었지만, APP Router에서는 dynamicParams
를 통해 제어가 가능해졌습니다. default
값은 true
이므로 변경할 필요는 없지만, 사용하지 않으려는 경우 false
로 설정할 수 있습니다.
//page.tsx
export async function generateStaticParams(): Promise<{ id: string }[]> {
const data = await fetch(
`https://jsonplaceholder.typicode.com/posts`,
).then((res)=>res.json());
return data!.map((res) => ({ id: String(res.id) }));
}
export const dynamicParams = true
export default async function Page({ params }: Props) {
const data = await fetch(
`https://jsonplaceholder.typicode.com/posts/${params.id}`,
).then((res)=>res.json());
return (
<div className="flex flex-col">
<div className="">
<h1 className="truncate text-2xl font-medium capitalize text-gray-200">
{data?.title}
</h1>
<p className="line-clamp-3 font-medium text-gray-500">{data?.body}</p>
</div>
</div>
);
}
Opting out of Data Caching
마지막으로 fetch
API 의 no-store
입니다 getServerSideProps
를 대체하는 기능입니다.
AS-IS getServerSideProps
export const getServerSideProps: GetServerSideProps = async () => {
const posts = (await fetch(`https://jsonplaceholder.typicode.com/posts`).then((res) => res.json())) as Posts[];
return {
props:{
posts
}
}
};
TO-BE
const Page = async () => {
const data = (await fetch(`https://jsonplaceholder.typicode.com/posts`, {
cache: 'no-cache',
}).then((res) => res.json())) as Posts[];
return (
<main>
<ul className="flex flex-col gap-30">
{data?.map((item) => {
return (
<li key={item.id}>
<Link href={`/ssr/${item.id}`}>
<h1>{item.title}</h1>
<p>{item.body}</p>
</Link>
</li>
);
})}
</ul>
</main>
);
};
외부 라이브러리(ex: axios
)를 사용하여 데이터를 가져오는 경우는 아직 개발 중인 것 같습니다만 공식 홈페이지의 예제를 따라 사용해 보겠습니다.
다른점이 있다면 fetch
API 같은 경우 옵션을 주입하여 렌더링 방식을 선택 했다면 axios 같은 라이브러리 같은경우 Route Segment Config 를 사용해야 합니다.
import axios from 'axios';
import { cache } from 'react';
export const revalidate = 3600
const getPosts = cache(async () => {
return await axios.get(`https://jsonplaceholder.typicode.com/posts`);
});
const Page = async () => {
const data = await getPosts()
return (
<main>
<ul className="flex flex-col gap-30">
{data?.map((item) => {
return (
<li key={item.id}>
<Link href={`/ssr/${item.id}`}>
<h1>{item.title}</h1>
<p>{item.body}</p>
</Link>
</li>
);
})}
</ul>
</main>
);
};
Route Handler
는 서버에서 실행되고 데이터를 클라이언트에 반환합니다. 이 기능은 API 토큰이나 민감한 정보를 클라이언트에 노출하고 싶지 않을 때 유용합니다.
// /app/api/posts/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const data = await fetch('https://jsonplaceholder.typicode.com/posts', {
headers: {
'Content-Type': 'application/json',
'API-Key': '클라이언트에서는_못보지롱',
'Token':'클라이언트에_노출이되면_안되는_토큰'
},
}).then((res) => res.json());
return NextResponse.json({ data });
}
// page.tsx
'use client';
import { useEffect } from 'react';
const Page = () => {
const [data,setData] =useState(null)
const handleFetch = async () => {
const res = await fetch('/api/posts').then((res)=>res.json());
setData(res)
}
useEffect(() => {
handleFetch()
}, []);
return (
<main>
<ul className="flex flex-col gap-30">
{data?.map((item) => {
return (
<li key={item.id}>
<h1>{item.title}</h1>
<p>{item.body}</p>
</li>
);
})}
</ul>
</main>
);
};
마지막으로, React-Query
나 SWR
와 같은 라이브러리를 사용하는 경우입니다. 저는 react-query를 사용하겠습니다.
Next.js에서 react-query
를 사용할 때 가장 중요한 pre-render
를 어떻게 하는지 알아보겠습니다.
AS-IS
// _app.tsx
const App = ({Component, pageProps}: AppPropsWithLayout) => {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
)
}
export default App
// home.tsx
const getPosts = () => {
const data = (await fetch(`https://jsonplaceholder.typicode.com/posts`, {
cache: 'no-cache',
}).then((res) => res.json())) as Posts[];
return data
}
const HomePage:NextPage = () =>{
const query = useQuery(['posts], () => getPosts())
return (
...
)
}
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(['posts'], getPosts)
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
TO-BE
App Router 에서 다른점이 있다면 _app.tsx
에서 pageProps
를 받아 하이드레이션 작업을 해주었는데 App Router
에서는 각 page.tsx
에서 하이드레이션을 해주어야 합니다.
간단하게 아래 처럼 바꼇다고 보면 될꺼 같습니다.
// page router
getStaticProps
=>_app.tsx
=>Hydrate
=>react-query
// app router
page.tsx
=>Hydrate
=>react-query
app router 에서는 서버 컴포넌트가 default
이기 때문에 react-query
를 사용 하기 위해선 몇가지 작업을 해주어야 합니다.
// src/app/registry.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { PropsWithChildren, useState } from 'react';
const QueryClientRegistry = ({ children }: PropsWithChildren) => {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
export default QueryClientRegistry;
// src/utils/getQueryClient.ts
import { QueryClient } from "@tanstack/query-core";
import { cache } from "react";
const getQueryClient = cache(() => new QueryClient());
export default getQueryClient;
// src/utils/hydrate.client.tsx
"use client";
import { Hydrate as RQHydrate, HydrateProps } from "@tanstack/react-query";
function Hydrate(props: HydrateProps) {
return <RQHydrate {...props} />;
}
export default Hydrate;
작성하셨다면 page.tsx
로 가서 사용해보겠습니다.
주의 해야할 점은 react-query
,swr
같은 경우는 서버 컴포넌트에서 작동하지 않기 때문에 'use client'
을 주입하여 클라이언트 컴포넌트에서 작동해야 합니다.
// src/app/ssg/page.tsx
import SsgView from '@/views/SsgPage';
import { getPosts } from '@/utils/getPosts';
export default async function Page() {
const queryClient = getQueryClient();
await queryClient.prefetchQuery(['ssg-posts'], () => getPosts());
const dehydratedState = dehydrate(queryClient);
return (
<Hydrate state={dehydratedState}>
<SsgView />
</Hydrate>
);
}
// src/view/SsgView.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
import { type Posts } from '@/types/posts';
import { getPosts } from '@/utils/getPosts';
const SsgView = () => {
const { data } = useQuery<Posts[]>(['ssg-posts'], () => getPosts());
return (
....
);
};
export default SsgView;
https://codevoweb.com/setup-react-query-in-nextjs-13-app-directory/