Next.js Server Action와 React useActionState 알아보기

useActionStateAndServerAction.png

🚀 Next.js 15v의 Server Action

1. Server Action이란?

Next.js 15버전에 새롭게 도입된 Server Action은 서버에서 직접 실행되는 함수로, 클라이언트와 서버 간의 복잡한 데이터 요청 흐름을 단순화하는 기능을 제공합니다. 이를 통해 서버에서 처리해야 할 작업들을 클라이언트 코드에서 분리하고, 클라이언트에서 서버 함수 호출을 간편하게 할 수 있습니다.

2. Server Action이 해결하는 문제들

  • 복잡한 데이터 요청 로직: 기존에는 클라이언트에서 데이터를 fetch하거나 서버와 통신하기 위해 여러 API 호출 코드를 작성해야 했습니다.
  • 클라이언트-서버 분리: 클라이언트 코드에서 서버 로직을 분리함으로써 코드의 유지보수성과 가독성을 향상시킵니다.
  • 직접적인 서버 실행: Server Action을 통해 서버에서 실행할 수 있는 로직을 클라이언트에서 간단히 호출할 수 있으므로, 번거로운 REST API나 GraphQL 설정이 필요하지 않습니다.

REST API 설정의 번거로움 예시

// api/handler.js import db from './db'; export default async function handler(req, res) { if (req.method === 'POST') { const { userId } = req.body; const user = await db.users.findOne({ id: userId }); res.status(200).json(user); } else { res.status(405).json({ error: 'Method not allowed' }); } } // 클라이언트에서 호출 코드 import axios from 'axios'; async function fetchUser(userId) { const response = await axios.post('/api/handler', { userId }); return response.data; } export default function UserComponent() { const handleFetch = async () => { const user = await fetchUser('123'); console.log(user); }; return <button onClick={handleFetch}>Fetch User</button>; }

Server Action 사용 시

"use server"; export async function fetchUser(userId) { const user = await db.users.findOne({ id: userId }); return user; } ("use client"); import { fetchUser } from "./actions/fetchUser"; export default function UserComponent() { const handleFetch = async () => { const user = await fetchUser("123"); console.log(user); }; return <button onClick={handleFetch}>Fetch User</button>; }

이와 같이 Server Action을 사용하면 API 핸들러 작성과 클라이언트에서의 별도 HTTP 요청 코드 작성이 필요 없습니다.

3. Server Action의 사용법

Server Action은 Next.js의 특수한 파일 구조와 함께 사용됩니다. 예를 들어

// app/actions/myAction.js "use server"; export async function myAction(data) { // 서버에서 실행될 로직 console.log("Received data:", data); return { success: true }; }

클라이언트에서 이를 호출하려면

"use client"; import { myAction } from "./actions/myAction"; export default function MyComponent() { const handleAction = async () => { const result = await myAction({ key: "value" }); console.log(result); }; return <button onClick={handleAction}>Call Action</button>; }

⚛️ React 19v의 useActionState

1. useActionState란?

React 19버전에 추가된 useActionState는 Server Action과 함께 사용되며, Action의 상태를 관리하는 React 훅입니다. 주로 Action의 요청 상태(pending, success, error)를 추적하는 데 사용됩니다.

2. useActionState가 해결하는 문제들

  • 비동기 상태 관리: 기존에는 Action 상태를 관리하기 위해 useState와 useEffect를 조합해 작성해야 했습니다. useActionState는 이를 단순화합니다.
  • 로딩 상태 관리: 요청 중 상태를 쉽게 파악할 수 있어 사용자 경험을 개선합니다.
  • 중복 로직 제거: 요청 상태를 관리하기 위한 별도의 로직이 필요 없으므로 코드가 간결해집니다.

3. useActionState의 사용법

import { useActionState } from 'react'; import { myAction } from './actions/myAction'; export default function MyComponent() { const [isPending, action] = useActionState(myAction); const handleClick = async () => { const result = await action({ key: 'value' }); console.log(result); }; return ( <div> <button onClick={handleClick} disabled={isPending}> {isPending ? 'Loading...' 'Call Action'} </button> </div> ); }

🛠️ Server Action과 useActionState 사용의 예시

자세한 코드를 확인하시고 싶으시면 아래 링크를 참고해주세요!

링크 미리보기 이미지GitHub - lurgi/action-state-pracContribute to lurgi/action-state-prac development by creating an account on GitHub.

1. Server Action 함수 만들기

"use server"; // ... export async function login(currentState: LoginReturn, formData: FormData): Promise<LoginReturn> { const email = formData.get("email") as string; const password = formData.get("password") as string; if (email !== "lurgi@gmail.com" || password !== "qwer1234") { return { email, password, message: "fail", isError: true, }; } return { email, password, message: "success", isError: false, }; }

2. useActionState로 불러오기

"use client"; import { useActionState, useEffect, useState } from "react"; import { login } from "./action/login"; export default function Home() { const [state, formAction, isPending] = useActionState(login, { email: "", password: "", message: undefined, isError: false, }); const { email, password, message, isError } = state; return ( <div className="grid place-items-center h-screen"> <form className="w-96" action={formAction}> <label className="form-control w-full"> <div className="label"> <span className="label-text">Email</span> </div> <input name="email" type="email" placeholder="Email" className="input input-bordered w-full" defaultValue={email} /> </label> <label className="form-control w-full"> <div className="label"> <span className="label-text">Password</span> </div> <input name="password" type="password" placeholder="Password" className="input input-bordered w-full" defaultValue={password} /> </label> <div className="label"> <button className="btn btn-primary w-full" disabled={isPending}> Login </button> </div> </form> </div> ); }

3. 로딩 상태(isPending) 컨트롤

isPending을 활용해 로딩 상태를 표시하고, 사용자가 요청이 완료될 때까지 기다리도록 안내합니다. 이를 통해 더 나은 UX를 제공합니다. 아래는 isPending을 이용하여 Toast 메세지를 컨트롤 하는 예시입니다.

export default function Home() { const [state, formAction, isPending] = useActionState(login, { email: "", password: "", message: undefined, isError: false, }); const [hasToast, setHasToast] = useState(false); const { email, password, message, isError } = state; useEffect(() => { let timer: NodeJS.Timeout | undefined; if (isPending) { setHasToast(true); } if (!isPending) { timer = setTimeout(() => setHasToast(false), 3000); } return () => { if (timer) clearTimeout(timer); }; }, [isPending, setHasToast]); return ( {/*...*/} <div className="toast toast-bottom toast-center"> {hasToast && message && (isError ? ( <div role="alert" className="alert alert-error"> <span>Error! {message}</span> </div> ) : ( <div role="alert" className="alert alert-success"> <span>{message}</span> </div> ))} </div> ) }

📝 변화하는 데이터 fetching과 mutate 생태계

Next.js의 Server Action과 React의 useActionState는 클라이언트-서버 통신에서 발생하는 복잡성을 크게 줄여주는 도구입니다. 이 두 기능은 서버에서 실행되는 로직과 클라이언트 상태 관리를 자연스럽게 결합하여 데이터 요청과 상태 관리를 훨씬 간단하게 처리할 수 있는 방법을 제공합니다.

기존의 mutate 방식과의 비교

React 애플리케이션에서 데이터를 변형(mutate)하는 가장 일반적인 방식은 Tanstack Query의 useMutate 훅을 사용하는 것이었습니다. 이 방식은 강력한 캐싱과 데이터 동기화 기능을 제공하며, 대규모 애플리케이션에서도 안정적으로 사용할 수 있는 도구로 자리 잡았습니다.

하지만 useActionState가 등장하면서 mutate 상태 관리는 이전보다 훨씬 간소화되었습니다. useActionState는 서버에서 실행되는 Server Action과 직접 통합되어, 데이터를 수정하는 로직과 상태 관리 로직을 한 곳에 결합합니다. 이를 통해 별도의 캐싱이나 로직 분리가 필요 없으며, 직관적인 사용 방식 덕분에 코드 작성이 간결해졌습니다.

결론적으로, Next.js와 React가 제공하는 최신 도구들은 데이터 요청과 상태 관리의 방식에 큰 변화를 가져오고 있습니다. 이로 인해 Tanstack Query와 같은 라이브러리는 더 특화된 영역에서 활용될 가능성이 높아지며, React의 생태계는 점점 더 간단하고 효율적인 방향으로 발전할 것입니다.

추후에 useLoaderState와 같은 fetching을 담당하는 API가 제공된다면, Tanstack Query와 같은 라이브러리들은 어떤 역할로 활용될까요?