[Next.js 14] 2. Data Fetching - 2

2025. 2. 19. 16:30·Skill/Next.js
반응형

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

  • https://nextjs.org/docs/14/app/building-your-application/data-fetching/server-actions-and-mutations
 

Data Fetching: Server Actions and Mutations | Next.js

Learn how to handle form submissions and data mutations with Next.js.

nextjs.org

 

저작자표시 (새창열림)
'Skill/Next.js' 카테고리의 다른 글
  • [Next.js 14] 3. Rendering - 1
  • [Next.js 14] 2. Data Fetching - 3
  • [Next.js 14] 2. Data Fetching - 1
  • [Next.js 14] 1. Routing - 13
뜸부깅
뜸부깅
코딩에 대한 여러 개인적인 생각을 정리하고 공부를 하는 공간입니다!!
  • 뜸부깅
    코오오딩
    뜸부깅
  • 전체
    오늘
    어제
    • Note (429)
      • Skill (31)
        • Java & Spring (9)
        • Javascript & HTML & CSS (0)
        • React (0)
        • Next.js (22)
      • CodingTest (389)
        • 백준 온라인 저지(BOJ) (140)
        • 프로그래머스(Programmers) (79)
        • LeetCode (170)
      • Algorithm & Data Structure (6)
      • [Project] 포트폴리오 (3)
        • Front end (3)
        • Back end (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Java
    백준
    medium
    백준1260
    boj1427
    자바
    백준1427
    next 14
    boj2108
    TypeScript
    백준2751
    프로그래머스
    백준7576자바
    백준7576
    leetcode 2236
    meidum
    알고리즘
    Easy
    BOJ
    component-scan
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
뜸부깅
[Next.js 14] 2. Data Fetching - 2
상단으로

티스토리툴바