데이터를 가져오는 페이지의 경우, 동적 경로를 사용하지 않는 이상 정적 페이지로 사전 렌더링된다.
-> 아래 설정을 주어 사전렌더링을 막을 수 있음.
export const dynamic = 'force-dynamic'
-> 혹은 cookies, headers, searchParam을 사용하는 경우는 자동으로 동적 렌더링됨.
nextjs15에서 fetch를 쓸 때와 axios를 쓸 때의 장단점
Next.js 15에서는 서버에서 fetch가 자동 캐싱 최적화 및 병렬 요청을 지원한다!
-> 따라서 서버 측에서는 fetch를 사용하는 것이 좋다!
서버 컴포넌트에서 응답을 캐시하여 사전 렌더링이 가능하게 해준다!
import { unstable_cache } from 'next/cache'
import { db, posts } from '@/lib/db'
const getPosts = unstable_cache(
async () => {=return await db.select().from(posts)},
['posts'],
{ revalidate: 3600유효시간, tags: ['posts'] }
)
export default async function Page() {
const allPosts = await getPosts()
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
15이전에는 cache: force-cache가 default값이었으나, 이후로는 cache: no-store가 default!
import { notFound } from 'next/navigation'
async function getPost(id: string) {
const res = await fetch(`https://api.vercel.app/blog/${id}`, {
cache: 'force-cache', //캐시 사용 여부
})
const post = await res.json()
if(post) return post
else notFound()
}
//파라미터 동적 생성
export async function generateStaticParams() {
const posts = await fetch('https://api.vercel.app/blog', {
cache: 'force-cache',
}).then((res) => res.json())
return posts.map((post: Post) => ({
id: String(post.id),
}))
}
//메타데이터 동적 생성하기
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post = await getPost(id)
return {
title: post.title,
}
}
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post = await getPost(id) //이렇게 연결할 수 있군요..!!
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
뭔가 했더니 그냥 promise.all쓰는거였음
import Albums from './albums'
async function getArtist(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getAlbums(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page() {
const artistData = getArtist()
const albumsData = getAlbums()
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}
import { getItem } from '@/utils/get-item'
export const preload = (id: string) => {
void getItem(id)
}
export default async function Item({ id }: { id: string }) {
const result = await getItem(id)
// ...
}
뭔 소리냐?
// ✅ preload가 먼저 실행됨
preload('123') // getItem('123') 실행 → 캐시에 저장됨
// ✅ 이후에 getItem('123')을 호출하면 캐시에서 반환
const result = await getItem('123') // 캐시된 데이터 사용
// ❌ preload를 호출하지 않음
const result = await getItem('123') // getItem('123') API 요청 발생
아하, 그러니까 얘도 쿼리 같은 거군요.
있다! 저 id는 동적 파람 가져오기로 꺼내오면 될듯.
export const preload = (id: string) => {
void getItem(id)
}
export default async function Item({ id }: { id: string }) {
preload(id)
const result = await getItem(id)
return <div>{result.name}</div>
}
사용 이유가 다르다!
unstable cache는 "캐시"에 초점을 맞추고,
preloading는 "사전에 로딩"하는데에 초점을 맞춤!
있다!!
//api 함수를 만들 때는 unstable_cache를 쓰고
import { unstable_cache } from "next/cache";
import { db } from "@/lib/db";
const getItem = unstable_cache(
async (id: string) => {
return await db.item.findUnique({ where: { id } });
},
["item"],
{ revalidate: 3600 }
);
export { getItem };
//api함수를 가져다 쓸 때는 preload를 쓰면 됨!
import { getItem } from "@/utils/get-item";
export const preload = (id: string) => {
void getItem(id); // 캐시에 미리 저장
};
export default async function Item({ id }: { id: string }) {
preload(id); // 🔥 페이지 로드 전에 캐싱된 데이터 준비
const result = await getItem(id); // ✅ 캐시에서 가져옴
return <div>{result.name}</div>;
}
서버에서만 실행되도록 보장한다!
import { cache } from 'react'
import 'server-only'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
// ...
})
서버에서 실행되는 비동기 함수.
-> 가장 상위에 use server 선언해야 함.
-> use client한 것 안쪽에 use server를 또 선언해도 된다.
export default function Page() {
async function create() {
'use server'
// 여기에 선언할 수도 있군용
}
return '...'
}
form의 action 특성을 사용하여 호출할 수 있다!
-> 서버 컴포넌트는 기본적으로 점진적 향상을 지원하므로 js가 아직 로드되지 않았거나 비활성화된 경우에도 양식이 제출된다!
-> 참고로, 클라이언트 하이드레이션이 우선됨.
export default function Page() {
async function createInvoice(formData: FormData) {
'use server'
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
}
//이렇게 prop 추가 가능. createInvoice에 userId, formData 가 들어가게 된다.
const updateUserWithId = createInvoice.bind(null, userId)
//이렇게 프로그래밍 방식으로 넣을 수도 있음
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
e.preventDefault()
e.currentTarget.form?.requestSubmit()
}
return <form action={createInvoice}>
<textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
</form>
}
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: 'Invalid Email',
}),
})
export default async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
}
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
import { useFormStatus } from 'react-dom'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction] = useActionState(createUser, initialState)
const { pending } = useFormStatus() // 액션이 실행되는 동안 로딩 표시
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
<p aria-live="polite">{state?.message}</p>
<button disabled={pending} type="submit">
Sign Up
</button>
</form>
)
}
응답을 기다리는 대신 Server Action 실행이 완료되기 전에 UI를 낙관적으로 업데이트할 수 있다!
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] =
useOptimistic(messages,
(state, newMessage) =>
[...state, { message: newMessage }])
const formAction = async (formData: FormData) => {
const message = formData.get('message') as string
addOptimisticMessage(message)
await send(message)
}
return (
<div>
{optimisticMessages.map((m, i) => (
<div key={i}>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
)
}
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidatePath('/posts') //캐시를 업데이트한다.
}
서버 작업이 완료된 후 사용자를 다른 경로로 리디렉션할 수 있다!
-> redirect는 try/catch 블록 외부에서 호출해야 함.
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function createPost(id: string) {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts')
redirect(`/post/${id}`)
}