Data Fetching

MM·2023년 11월 24일

NEXTJS15

목록 보기
1/2
post-thumbnail

fetch

사전 렌더링

데이터를 가져오는 페이지의 경우, 동적 경로를 사용하지 않는 이상 정적 페이지로 사전 렌더링된다.
-> 아래 설정을 주어 사전렌더링을 막을 수 있음.

export const dynamic = 'force-dynamic'

-> 혹은 cookies, headers, searchParam을 사용하는 경우는 자동으로 동적 렌더링됨.

nextjs15에서 fetch를 쓸 때와 axios를 쓸 때의 장단점

Next.js 15에서는 서버에서 fetch가 자동 캐싱 최적화 및 병렬 요청을 지원한다!
-> 따라서 서버 측에서는 fetch를 사용하는 것이 좋다!




caching

unstable cache

서버 컴포넌트에서 응답을 캐시하여 사전 렌더링이 가능하게 해준다!

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>
  )
}


Patterns

병렬 데이터 fetching

뭔가 했더니 그냥 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} />
    </>
  )
}

Preloading Data

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의 차이점

사용 이유가 다르다!
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>;
}


server only

서버에서만 실행되도록 보장한다!

import { cache } from 'react'
import 'server-only'
 
export const preload = (id: string) => {
  void getItem(id)
}
 
export const getItem = cache(async (id: string) => {
  // ...
})



Server Actions

서버에서 실행되는 비동기 함수.
-> 가장 상위에 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>
}


서버 측 유효성 검사

zod 라이브러리 사용하기

'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}`)
}
profile
중요한 건 꺾여도 그냥 하는 마음

0개의 댓글