본문 바로가기

넥스트

[nextjs] 13. 폼과 변이(Form and Mutations) - 서버액션 활용 예시

반응형

  폼과 변이(Form and Mutations)

폼을 사용하면 웹 애플리케이션에서 데이터를 생성하고 업데이트할 수 있습니다. Next.js는 서버 액션(Server Actions)을 사용하여 폼 제출 및 데이터 변이를 강력하게 처리할 수 있는 방법을 제공합니다.

 


서버 액션이 어떻게 동작하는지

서버 액션을 사용하면 API 엔드포인트를 수동으로 생성할 필요가 없습니다. 대신 컴포넌트에서 직접 호출할 수 있는 비동기 서버 함수를 정의합니다.

서버 액션은 서버 컴포넌트에서 정의하거나 클라이언트 컴포넌트에서 호출할 수 있습니다. 서버 컴포넌트에서 액션을 정의하면 JavaScript가 없어도 폼이 작동하여 점진적으로 향상됩니다.

next.config.js 파일에서 Server Actions을 활성화하세요:

// next.config.js

module.exports = {
  experimental: {
    serverActions: true, // 현재는 실험적인 기능
  },
}

 

유용한 정보

- 서버 컴포넌트에서 Server Actions을 호출하는 폼은 JavaScript 없이 작동할 수 있습니다.
- 클라이언트 컴포넌트에서 Server Actions을 호출하는 폼은 JavaScript가 아직 로드되지 않은 경우 제출을 대기하고 클라이언트 하이드레이션을 우선시합니다.
- Server Actions은 페이지나 레이아웃에서 사용된 런타임을 상속합니다.

현재로서는 경로에 Server Action이 사용되면 동적으로 렌더링해야 합니다.

 

  캐시된 데이터 재유효화

Server Actions은 Next.js 캐싱 및 재유효화 아키텍처와 깊게 통합됩니다. 폼이 제출되면 Server Action은 캐시된 데이터를 업데이트하고 변경해야 할 캐시 키를 재유효화할 수 있습니다.


전통적인 애플리케이션과 달리 Server Actions을 사용하면 경로당 하나의 폼에 제한되지 않고 여러 액션을 하나의 경로에 둘 수 있습니다. 브라우저는 폼 제출 시 새로 고침할 필요가 없습니다. 단일 네트워크 라운드트립에서 Next.js는 업데이트된 UI와 새로 고침된 데이터를 동시에 반환할 수 있습니다.

(개인정리) 서버액션을 활용하면, 우리가 흔히 html form 태그에서 prevent 를 하지 않아서 생기는 불가피한 새로고침을 경험할 필요도 없이. next.js 에서 제공해주는 데이터 재유효화 방식대로 처리하면 미리 새로고침된 데이터를 브라우저에 즉시 렌더링할 수 있다.

다음은 Server Actions에서 데이터를 재유효화하는 예제입니다.

 

  예제

    서버 전용 폼(Server-only Forms)

서버 전용 폼을 만들려면 Server Component에서 Server Action을 정의합니다. 액션은 함수의 맨 위에서 "use server" 지시어와 함께 인라인으로 정의하거나 파일의 맨 위에서 지시어를 사용하여 별도로 정의할 수 있습니다.

// app/page.tsx

export default function Page() {
  async function create(formData: FormData) {
    'use server'

    // 데이터 변이
    // 캐시 재유효화
  }

  return <form action={create}>...</form>
}

 

유용한 정보

<form action={create}>FormData 데이터 유형을 사용합니다. 위 예제에서 HTML 폼을 통해 제출된 FormData는 서버 액션인 create에서 접근할 수 있습니다.


   데이터 재유효화(Revalidating Data)

(개인정리) 재유효화는 쉽게 말해 새로고침과 유사하다고 생각하면 된다.

Server Actions을 사용하면 필요할 때 Next.js 캐시를 무효화할 수 있습니다. revalidatePath를 사용하여 전체 경로 세그먼트를 무효화하거나 revalidateTag를 사용하여 캐시 태그를 사용하여 특정 데이터를 무효화할 수 있습니다.

// app/actions.ts

'use server'

import { revalidatePath } from 'next/cache'

export default async function submit() {
  await submitForm()
  revalidatePath('/') // 전송 시 전체 경로에 대하여 캐시 새로고침(개인추가)
}

 

또는 revalidateTag를 사용하여 캐시 태그를 사용하여 특정 데이터를 무효화할 수 있습니다.

// app/actions.ts

'use server'
import { revalidateTag } from 'next/cache'

export default async function submit() {
  await addPost()
  revalidateTag('posts')
}
(개인정리)  revalidateTag('posts')  에서 posts 는 경로로 따지면 '/posts' 를 의미한다. 즉, 해당 경로에 렌더링 되는 페이지와 공유하는 캐시를 새로고침함으로써 사용자는 해당 페이지에서 새롭게 추가된 데이터를 확인할 수 있게 된다. 즉, 위 코드는 서버로 포스트를 추가하는 요청을 보내고, 그 후 지정한 태그(경로)의 캐시를 사전에 새로고침함으로써 실제 브라우저의 새로고침 필요없이 새 포스트에 대한 데이터를 사용자가 바로 확인할 수 있게 해준다.

 

   리디렉션(Redirecting)

서버 액션 완료 후 사용자를 다른 경로로 리디렉션하려면 리디렉션과 절대 또는 상대 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') // 캐시된 게시물 업데이트
  redirect(`/post/${id}`) // 새 경로로 이동
}

 

  폼 유효성 검사(Form Validation)

기본적인 폼 유효성 검사에는 required 및 type="email"과 같은 HTML 유효성 검사를 사용하는 것이 좋습니다.

더 고급 서버 측 유효성 검사를 위해서는 폼 데이터의 구조를 검사하는 스키마 유효성 검사 라이브러리인 zod와 같은 스키마 유효성 검사 라이브러리를 사용하는 것이 좋습니다.

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

폼이 서버로 제출될 때 로딩 상태를 표시하려면 useFormStatus 훅을 사용하세요.

// app/page.tsx

'use client'

import { experimental_useFormStatus as useFormStatus } from 'react-dom'

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button disabled={pending}>{pending ? '제출 중...' : '제출'}</button>
  )
}


유용한 정보:
로딩 또는 오류 상태를 표시하려면 현재 클라이언트 컴포넌트를 사용해야 합니다. Server Actions의 안정성을 향상시키며 이러한 값을 검색하기 위한 서버 측 함수 옵션을 탐구하고 있습니다.

 

  에러 처리(Error Handling)

Server Actions은 직렬화 가능한 객체를 반환할 수도 있습니다. 예를 들어 Server Action에서 새 항목을 생성하는 작업의 오류를 처리할 수 있으며 성공 또는 오류 메시지를 반환할 수 있습니다.

// app/actions.ts

'use server'

export async function create(formData: FormData) {
  try {
    await createItem(formData.get('item'))
    revalidatePath('/') // 전체 경로에 대한 캐시를 새로고침(리로드)
    return { message: '성공!' }
  } catch (e) {
    return { message: '오류가 발생했습니다.' }
  }
}


그런 다음 클라이언트 컴포넌트에서 이 값을 읽고 상태에 저장하여 Server Action의 결과를 뷰어에게 표시할 수 있습니다.

// app/page.tsx

'use client'

import { create } from './actions'
import { useState } from 'react'

export default function Page() {
  const [message, setMessage] = useState<string>('')

  async function onCreate(formData: FormData) {
    const res = await create(formData)
    setMessage(res.message)
  }

  return (
    <form action={onCreate}>
      <input type="text" name="item" />
      <button type="submit">추가</button>
      <p>{message}</p>
    </form>
  )
}


유용한 정보:

로딩 또는 오류 상태를 표시하려면 현재 클라이언트 컴포넌트를 사용해야 합니다. Server Actions의 안정성을 향상시키며 이러한 값을 검색하기 위한 서버 측 함수 옵션을 탐구하고 있습니다.

(개인정리) 현재는 서버 측 컴포넌트에서 로딩 및 에러 상태에 대한 분기처리 로직을 작성하고, 해당 반환값을 클라이언트 컴포넌트로 가져와서 화면에 그려야 하지만, Server Actions의 안정성을 향상시키며, 동일한 검색 로직을 갖추기 위해 노력중이다 라고 해석된다.

 

   낙관적 업데이트(Optimistic Updates)

Server Action의 응답을 기다리지 않고 Server Action이 완료되기 전에 UI를 낙관적으로 업데이트하려면 useOptimistic을 사용하세요.

// 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) => (
        <div>{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">전송</button>
      </form>
    </div>
  )
}

 

   쿠키 설정(Setting Cookies)

Server Action 내에서 쿠키를 설정하려면 cookies 함수를 사용하세요.

// app/actions.ts

'use server'

import { cookies } from 'next/headers'

export async function create() {
  const cart = await createCart() // 장바구니 데이터를 비동기적으로 가져와서 cart 변수에 담는다.
  cookies().set('cartId', cart.id) // 쿠키에 cartId 라는 키(key)로 cart.id 를 value에 담는다.
}


   쿠키 읽기(Reading Cookies)

Server Action 내에서 쿠키를 읽으려면 cookies 함수를 사용하세요.

// app/actions.ts

'use server'

import { cookies } from 'next/headers'

export async function read() {
  const auth = cookies().get('authorization')?.value // 키가 authorization 인 value 을 가져온다.
  // ...
}


   쿠키 삭제(Deleting Cookies)

Server Action 내에서 쿠키를 삭제하려면 cookies 함수를 사용하세요.

// app/actions.ts

'use server'

import { cookies } from 'next/headers'

export async function delete() {
  cookies().delete('name') // 키가 name 인 쿠키를 제거한다.
  // ...
}


Server Actions에서 쿠키 삭제에 대한 추가 예제를 확인하세요.

반응형