폼과 변이(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에서 쿠키 삭제에 대한 추가 예제를 확인하세요.
'넥스트' 카테고리의 다른 글
[next.js] 15. 클라이언트 컴포넌트 :: "use client" (0) | 2023.09.07 |
---|---|
[next.js] 14. 서버 컴포넌트 (0) | 2023.09.07 |
[next.js] 12. 데이터 가져오기 패턴 (0) | 2023.09.05 |
[nextjs] globalThis 형식에 인덱스 시그니처가 없다는 타입에러가 뜰 때 (0) | 2023.09.03 |
[next.js] 11. Data Fetching, Caching, and Revalidating (0) | 2023.09.02 |