반응형
2. Server Actions and Mutations
- Server Actions는 Server에서 실행되는 비동기 함수를 의미.
- Server Components, Client Componenets에서 사용될 수 있으며, 폼 제출, Mutations 처리 등을 한다.
2-1. Convention
- 비동기 함수 내부의 최상단 또는 분리된 파일 내 최상단에 "use server" 지시어로 Server Actions를 정의할 수 있다.
1. Server Components
// Server Component
export default function Page() {
// Server Action
async function create() {
'use server'
// ...
}
return (
// ...
)
}
- inline 비동기 함수 또는 module level에서 "use server" 지시어를 사용한다.
- inline 비동기 함수 body 상단에 적을 수 있다.
2. Client Components
'use server'
export async function create() { // Server Actions
// ...
}
import { create } from '@/app/actions'
export function Button() { // Client Components
return (
// ...
)
}
- Client Components에서는 오직 module level에서 최상단에 "use server"지시어를 사용할 수 있다.
- 해당 module 내의 모든 함수는 Server Actions가 되고, Client Components 또는 Server Components에서 사용할 수 있다.
2-2. 동작
- Server Actions는 <form> 태그의 action attribute와 함께 쓰일 수 있다.
- Server Components는 점진적 향상을 기본적으로 지원하므로, Javascript가 로드되지 않았어도 폼 제출이 가능하다.
- Client Components에서는 Javascript가 로드되지 않았으면 큐에 등록해두고, hydration을 우선 진행.
- hydration이 종료된 후, 폼을 제출해도 새로고침이 발생하지 않음.
- Server Actions는 <form> 뿐만 아니라, event handler, useEffect, third-party libraries 등과 같이 다른 요소들과도 쓰일 수 있다.
- Server Actions는 Next.js Caching revalidation 구조와 통합되어 있어, 실행 시 한 번에 UI 업데이트와 데이터 최신화가 가능하다.
- HTTP POST Method만 호출할 수 있다.
- Server Actions의 인자와 반환 값은 React에 의해 직렬화 가능한 값이어야 한다.
- Server Actions는 함수이기 때문에 어디서든 사용할 수 있다.
- Server Actions는 Page나 Layout의 Runtime을 상속받는다.
- Server Actions는 Page나 Layout의 Segment Config Option을 상속받는다.
2-3 Example
1. Forms
- React는 <form> 태그의 action 속성에 Server Actions를 사용하는 것을 허용한다.
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'),
}
// mutate data
// revalidate cache
}
return <form action={createInvoice}>...</form>
}
- form 제출 시, Server Actions은 FormData를 인자로 자동적으로 받고, 이를 활용해서 입력 값을 가공한다.
- useState를 사용할 필요가 없다.
1-1. 추가적인 인자 전달.
'use server'
export async function updateUser(userId, formData) {
// ...
}
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Update User Name</button>
</form>
)
}
- bind 함수를 이용해 Server Actions에 추가적인 인자 전달이 가능하다.
- Server Components와 Client Components 모두 bind의 실행이 가능하며, 이것 또한 점진적 향상 원칙을 따르는 것.
1-2. 상태 보류
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
Add
</button>
)
}
import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'
export default async function Home() {
return (
<form action={createItem}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
- React hook 중 useFormStatus을 사용하면 제출된 폼에 대한 보류 상태를 알 수 있다.
- useFormStatus hook은 폼의 구체적인 상태를 제공하고, 반드시 <form> 태그 자식 컴포넌트에서 사용되어야 한다.
- useFormStatus hook은 React hook이므로 Client Components와 함께 쓰여야 한다.
1-3. Server에서 검증 및 에러 처리.
- HTML Validation을 사용해 기본적인 Client 측 검증을 추천.
- Server 측 검증은 zod와 같은 라이브러리를 사용해 mutate 전에 검증을 수행.
'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'),
})
// Return early if the form data is invalid
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Mutate data
}
- 필드 검증이 완료된 후, 직렬화 가능한 객체를 반환하면 useFormState를 활용해 사용자에게 Form data를 보여줄 수 있게 된다.
'use server'
export async function createUser(prevState: any, formData: FormData) { // 서명 변경
// ...
return {
message: 'Please enter a valid email',
}
}
- useFormState에 Server Actions를 전달하면, 서명이 변경되어 prevState 또는 initialState 인자를 첫 번째로 받게 된다.
'use client'
import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction] = useFormState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button>Sign up</button>
</form>
)
}
- useFormState hook은 Client Components에서 사용되어야 한다.
- 첫 번째 인자로 Server Actions, 두 번째 인자로 초기 상태 값을 넘겨준다.
1-4. Optimistic Updates(낙관적 업데이트)
- useOptimistic hook을 사용하면 Server Actions가 종료되기 전에 응답을 기다리지 않고 UI를 업데이트할 수 있다.
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
type Message = {
message: string
}
export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<
Message[],
string
>(messages, (state, newMessage) => [...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>
)
}
1-5. 중첩된 요소
- Server Actions는 <form> 태그 안에 <button>, <input type="submit" /> 과 같은 요소들에서 formAction 속성 또는 event handler와 사용할 수 있다.
- 이는, 폼 내에서 여러 방식의 Server Actions를 실행할 수 있는 방법을 제공한다.
1-6. 프로그래밍 적 폼 제출
'use client'
export function Entry() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.key === 'Enter' || e.key === 'NumpadEnter')
) {
e.preventDefault()
e.currentTarget.form?.requestSubmit()
}
}
return (
<div>
<textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
</div>
)
}
- requestSubmit() 함수를 이용해서도 폼을 제출할 수 있다.
2. Non-form
- Server Actions는 <form> 태그와 함께 일반적으로 사용되만, event handler 또는 useEffect와 함께 쓰일 수도 있다.
2-1. Event Handler
'use client'
import { incrementLike } from './actions'
import { useState } from 'react'
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes)
return (
<>
<p>Total Likes: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
}}
>
Like
</button>
</>
)
}
'use client'
import { publishPost, saveDraft } from './actions'
export default function EditPost() {
return (
<form action={publishPost}>
<textarea
name="content"
onChange={async (e) => {
await saveDraft(e.target.value)
}}
/>
<button type="submit">Publish</button>
</form>
)
}
- useOptimistic 또는 useTransition과 함께 사용하면 사용자 경험을 상승시킬 수 있고, 대기 상태를 보여줄 수도 있다.
- onChange와 같이 여러 번 Action이 발생할 수 있는 경우는 debounce와 같은 기법을 적용해야 한다.
2-2. useEffect
- useEffect안에서 의존성이 변경되거나 컴포넌트가 마운트된 후 실행 시킬 수도 있다.
- 이는 자동적으로 실행되어야 하거나, 전역 이벤트에서 유용할 수 있다.
'use client'
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews)
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
}
updateViews()
}, [])
return <p>Total Views: {views}</p>
}
3. Error Handling
- 에러가 발생하면 가까운 error.js 또는 <Suspense> client boundary에서 잡는다.
- Server Actions에서는 try/catch를 이용해 에러 처리를 하고 이를 활용해 UI에 적절하게 제공하는 것이 중요하다.
'use server'
export async function createTodo(prevState: any, formData: FormData) {
try {
// Mutate data
} catch (e) {
throw new Error('Failed to create task')
}
}
4. Revalidating data
- Server Actions에서 revalidatePath와 revalidateTags 함수를 이용해 Cache data에 대한 재검증이 가능하다.
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidatePath('/posts')
revalidateTag('posts')
}
5. Redirecting
- redirect 함수를 이용해 사용자를 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') // Update cached posts
redirect(`/post/${id}`) // Navigate to the new post page
}
6. Cookies
- get, set, delete와 같은 함수를 cookies API를 이용해 쓸 수 있다.
'use server'
import { cookies } from 'next/headers'
export async function exampleAction() {
// Get cookie
const value = cookies().get('name')?.value
// Set cookie
cookies().set('name', 'Delba')
// Delete cookie
cookies().delete('name')
}
2-4. Security
1. 인증과 인가
- Server Actions는 공개 API 처럼 처리해야 하므로, 접근에 대한 인증과 인가 처리가 필요하다.
'use server'
import { auth } from './lib'
export function addItem() {
const { user } = auth()
if (!user) {
throw new Error('You must be signed in to perform this action')
}
// ...
}
2. 클로저와 암호화
export default function Page() {
const publishVersion = await getLatestVersion();
async function publish(formData: FormData) {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('The version has changed since pressing publish');
}
...
}
return <button action={publish}>Publish</button>;
}
- Server Actions를 inline level에 생성하면, closure가 생성되고 바깥 함수 스코프에 접근이 가능 하다.
- closure는 데이터의 렌더링 순간 값을 캡쳐하고 Server Actions 실행 시 해당 값을 참조할 수 있어 유용하다.
- 그러나 이렇게 캡쳐하기 위해선, 민감한 정보가 client에 있어야 될 수도 있다.
- Next.js는 매 빌드마다 새로운 개인키를 생성해 캡처된 변수들을 암호화한다.
React taint API를 활용하면 Client에 민감한 정보가 노출되는 것을 방지할 수 있다.
3. 암호화 key overwrite
- Next.js를 여러 서버 인스턴스에 걸쳐 배포할 경우, 잠재적으로 암호화 키가 불일치할 수 있는 문제가 있다.
- process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY 환경변수를 통해 암호화 키를 지정하면 빌드 전체에 지속되어 동일한 키를 사용할 수 있다.
4. 출처 허용
- Server Actions는 <form> 태그와 함께 쓸 수 있으므로 CSRF 공격에 노출될 수 있다.
- HTTP POST Method만을 허용하기 때문에 대부분의 공격을 방어할 수 있다.
- 추가적으로, Server Actions는 Origin header와 Host header를 비교하여, 일치하지 않으면 요청을 무시한다. (동일한 host에서만 동작한다.)
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
},
},
}
- reverse proxy 또는 n중화 구성의 backend와 통신하기 위해 위 설정을 통해 허용할 origin을 지정할 수 있다.
처음 뵙겠습니다.
Next.js 사용하면서 처음 본 개념이거나, 이해하기 어려운 부분(?)은 붉은색 볼드처리 해봤다.
- 점진적 향상 : 브라우저가 Javascript가 없어도 기본적인 동작을 보장하는 원칙이라고 하며, Server Actions는 기본적으로 이걸 따른다고 한다.
- hydration : 서버에서 렌더링 된 HTML을 React가 활성화 시켜 이벤트를 등록하거나, 상태 관리가 가능하도록 만드는 것을 의미한다.
Preference
Data Fetching: Server Actions and Mutations | Next.js
Learn how to handle form submissions and data mutations with Next.js.
nextjs.org