[Next] Forms and Mutations

Taemin Jang·2023년 10월 3일

Next.js 13

목록 보기
3/3
post-thumbnail

Next 13 Forms and Mutations 기반으로 작성되었습니다.

How Server Actions Work

Server Actions을 사용하면 API endpoint를 만들 필요가 없는 대신 컴포넌트에서 직접 호출할 수 있는 비동기 서버 함수를 정의해야한다.

Server Actions은 서버 컴포넌트에서 정의하거나 클라이언트 컴포넌트에서 호출할 수 있다. 서버 컴포넌트에서 action을 정의하면 form이 자바스크립트 없이 작동하여 점진적인 향상을 제공할 수 있다.

next.config.js 파일에서 Server Actions를 허용해야한다.

// next.config.js

module.exports = {
  experimental: {
    serverActions: true,
  },
}

Next 13 Forms and Mutations 설명이 잘되어있음

Server Actions

  • 서버 컴포넌트에 작성된 Server Actions을 호출하는 Form은 자바스크립트 없이 작동할 수 있다.
  • 클라이언트 컴포넌트에 작성된 Server Actions을 호출하는 Form은 자바스크립트가 아직 로드되지 않은 경우 queue에 올리고, 클라이언트 Hydration 우선순위를 정한다.
  • Server Actions은 사용 중인 페이지 또는 레이아웃에서 런타임(Edge, Node)을 상속한다.
  • Server Actions은 ISR로 데이터를 재검증하는 것을 포함하여 완전히 정적인 경로에서 작동한다.

Server-only Forms

Server-only Form을 만들려면 서버 컴포넌트에서 Server Action을 정의해야 하는데 action은 함수의 상단에 'use server'가 정의되거나 파일 상단에 있는 별도의 파일에서 정의될 수 있다.

// app/page.tsx
// 함수 상단에 정의되어 있는 Server Action
export default function Page() {
  async function create(formData: FormData) {
    'use server'
    // mutate data
    // revalidate cache
  }
  return <form action={create}>...</form>
}

<form action={create}>는 FormData 데이터 형식을 취한다. 위 예에서 HTML form을 통해 제출된 FormData는 server action create에서 접근할 수 있다.

// app/action.ts
'use server'

import { revalidatePath } from 'next/cache'
import { sql } from '@vercel/postgres'
import { z } from 'zod'

export async function createTodo(prevState: any, formData: FormData) {
  const schema = z.object({
    todo: z.string().nonempty(),
  })
  const data = schema.parse({
    todo: formData.get('todo'),
  })

  try {
    await sql`
    INSERT INTO todos (text)
    VALUES (${data.todo})
  `
    revalidatePath('/')
    return { message: `Added todo ${data.todo}` }
  } catch (e) {
    return { message: 'Failed to create todo' }
  }
}

Revalidating Data

Server Actions을 사용하면 Next.js Cache를 revalidatePath 또는 revalidateTag를 사용하여 무효화할 수 있다.

// app/actions.ts
'use server'
 
import { revalidatePath } from 'next/cache'
import { revalidateTag } from 'next/cache'

export async function submit1() {
  await submitForm()
  // 전체 경로 세그먼트 무효화
  revalidatePath('/')
}
 
export async function submit2() {
  await addPost()
  // 캐시 태그로 특정 데이터 fetch 무효화
  revalidateTag('posts')
}

Redirecting

Server Action이 완료된 후 사용자를 다른 경로로 리다이렉트하려면 redirect와 함께 절대 경로 URL이나 상대 경로 URL을 사용할 수 있다.

// app/actions.ts
'use server'
 
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
 
export default async function submit() {
  const id = await addPost()
  revalidateTag('posts') // Update cached posts
  redirect(`/post/${id}`) // Navigate to new route
}

Form Validation

required와 같은 HTML 유효성 검사를 사용하고 기본 Form 유효성 검사를 위해 type='email'을 사용하는 것을 추천한다.

더욱 고도화된 서버측 유효성 검사를 하기 위해서는 zod와 같은 스키마 유효성 검사 라이브러리를 사용하면 구문 분석된 Form 데이터의 구조를 검사한다.

// app/actions.ts
import { z } from 'zod'
 
const schema = z.object({
  // ...
})
 
export default async function submit(formData: FormData) {
  const parsed = schema.parse({
    id: formData.get('id'),
  })
  // ...
}

Displaying Loading State

서버에서 Form을 제출할 때 useFormStatus 훅을 사용하여 로딩 상태를 보여줄 수 있다. useFormStatus 훅은 Server Action을 사용하는 form 요소의 자식으로만 사용할 수 있다.

// app/submit-button.tsx
'use client'
 
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
 
export function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button type="submit" aria-disabled={pending}>
      Add
    </button>
  )
}

작성된 SubmitButton 컴포넌트를 Server Action과 form에서 사용할 수 있다.

// app/page.tsx
import { SubmitButton } from '@/app/submit-button'
 
export default async function Home() {
  return (
    <form action={...}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}

Error Handling

Server Actions은 직렬화 가능한 객체를 반환할 수 있다. 예를 들어, Server Action에 새 항목을 만들 때 발생하는 오류를 처리할 수 있다.

// app/actions.ts
'use server'
 
export async function createTodo(prevState: any, formData: FormData) {
  try {
    await createItem(formData.get('todo'))
    return revalidatePath('/')
  } catch (e) {
    return { message: 'Failed to create' }
  }
}

그리고나서 클라이언트 컴포넌트에서 해당 값을 읽고 오류 메시지를 보여줄 수 있다.

// app/add-form.tsx
'use client'
 
import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'
 
const initialState = {
  message: null,
}
 
function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button type="submit" aria-disabled={pending}>
      Add
    </button>
  )
}
 
export function AddForm() {
  const [state, formAction] = useFormState(createTodo, initialState)
 
  return (
    <form action={formAction}>
      <label htmlFor="todo">Enter Task</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
    </form>
  )
}

Optimistic Updates

응답을 기다리지 않고 useOptimistic을 사용하여 Server Action이 완료되기 전에 UI를 최적으로 업데이트한다.

// app/page.tsx
'use client'
 
import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './actions'
 
type Message = {
  message: string
}
 
export function Thread({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[]>(
    messages,
    (state: Message[], newMessage: string) => [
      ...state,
      { message: newMessage },
    ]
  )
 
  return (
    <div>
      {optimisticMessages.map((m, k) => (
        <div key={k}>{m.message}</div>
      ))}
      <form
        action={async (formData: FormData) => {
          const message = formData.get('message')
          addOptimisticMessage(message)
          await send(message)
        }}
      >
        <input type="text" name="message" />
        <button type="submit">Send</button>
      </form>
    </div>
  )
}

Setting Cookies

cookies함수를 사용하여 Server Action 내부에 쿠키를 설정할 수 있다.

// app/actions.ts
'use server'
 
import { cookies } from 'next/headers'
 
export async function create() {
  const cart = await createCart()
  cookies().set('cartId', cart.id)
}

Deleting Cookies

cookies함수를 사용하여 Server Action 내부에 쿠키를 삭제할 수 있다.

// app/actions.ts
'use server'
 
import { cookies } from 'next/headers'
 
export async function delete() {
  cookies().delete('name')
  // ...
}
profile
하루하루 공부한 내용 기록하기

0개의 댓글