๋‚ด์žฅ ๋ธŒ๋ผ์šฐ์ € <form> ์ปดํฌ๋„ŒํŠธ๋กœ ์ •๋ณด ์ œ์ถœ์„ ์œ„ํ•œ ๋Œ€ํ™”ํ˜• ์ปจํŠธ๋กค์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<form action={search}>
<input name="query" />
<button type="submit">๊ฒ€์ƒ‰</button>
</form>

๋ ˆํผ๋Ÿฐ์Šค

<form>

์ •๋ณด ์ œ์ถœ์„ ์œ„ํ•œ ๋Œ€ํ™”ํ˜• ์ปจํŠธ๋กค์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด, ๋‚ด์žฅ ๋ธŒ๋ผ์šฐ์ € <form> ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•˜์„ธ์š”.

<form action={search}>
<input name="query" />
<button type="submit">๊ฒ€์ƒ‰</button>
</form>

์•„๋ž˜ ์˜ˆ์‹œ๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.

Props

<form>์€ ๋ชจ๋“  ๊ณตํ†ต ์—˜๋ฆฌ๋จผํŠธ Props๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

action: a URL or function. When a URL is passed to action the form will behave like the HTML form component. When a function is passed to action the function will handle the form submission in a Transition following the Action prop pattern. The function passed to action may be async and will be called with a single argument containing the form data of the submitted form. The action prop can be overridden by a formAction attribute on a <button>, <input type="submit">, or <input type="image"> component.

์ฃผ์˜ ์‚ฌํ•ญ

  • ํ•จ์ˆ˜๋ฅผ action์ด๋‚˜ formAction์— ์ „๋‹ฌํ•˜๋ฉด, HTTP ๋ฉ”์„œ๋“œ๋Š” method ํ”„๋กœํผํ‹ฐ์˜ ๊ฐ’๊ณผ ๊ด€๊ณ„์—†์ด POST๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ๋ฒ•

ํด๋ผ์ด์–ธํŠธ์—์„œ ํผ ์ œ์ถœ ์ฒ˜๋ฆฌํ•˜๊ธฐ

ํผ์ด ์ œ์ถœ๋  ๋•Œ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด, ํผ์˜ action ํ”„๋กœํผํ‹ฐ์— ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•˜์„ธ์š”. formData๊ฐ€ ํ•จ์ˆ˜์— ์ธ์ˆ˜๋กœ ์ „๋‹ฌ๋˜์–ด, ํผ์—์„œ ์ „๋‹ฌ๋œ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์ ์ด URL๋งŒ ๋ฐ›๋˜ ๊ธฐ์กด HTML action๊ณผ์˜ ์ฐจ์ด์ ์ž…๋‹ˆ๋‹ค. After the action function succeeds, all uncontrolled field elements in the form are reset.

export default function Search() {
  function search(formData) {
    const query = formData.get("query");
    alert(`'${query}'์„(๋ฅผ) ๊ฒ€์ƒ‰ํ–ˆ์Šต๋‹ˆ๋‹ค.`);
  }
  return (
    <form action={search}>
      <input name="query" />
      <button type="submit">๊ฒ€์ƒ‰</button>
    </form>
  );
}

์„œ๋ฒ„ ํ•จ์ˆ˜์—์„œ ํผ ์ œ์ถœ ์ฒ˜๋ฆฌํ•˜๊ธฐ

์ž…๋ ฅ ๋ฐ ์ œ์ถœ ๋ฒ„ํŠผ๊ณผ ํ•จ๊ป˜ <form>์„ ๋ Œ๋”๋งํ•˜์„ธ์š”. ํผ์„ ์ œ์ถœํ•  ๋•Œ ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ์„œ๋ฒ„ ํ•จ์ˆ˜('use server'๊ฐ€ ํ‘œ์‹œ๋œ ํ•จ์ˆ˜)๋ฅผ ํผ์˜ action ํ”„๋กœํผํ‹ฐ๋กœ ์ „๋‹ฌํ•˜์„ธ์š”.

<form action>์— ์„œ๋ฒ„ ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•˜๋ฉด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ํ™œ์„ฑํ™”๋˜๊ธฐ ์ „์ด๋‚˜ ์ฝ”๋“œ๊ฐ€ ๋กœ๋“œ๋˜๊ธฐ ์ „์— ์‚ฌ์šฉ์ž๊ฐ€ ํผ์„ ์ œ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์—ฐ๊ฒฐ ์ƒํƒœ๋‚˜ ๊ธฐ๊ณ„๊ฐ€ ๋А๋ฆฌ๊ฑฐ๋‚˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์œ ์šฉํ•˜๊ณ , action ํ”„๋กœํผํ‹ฐ์— URL์ด ์ „๋‹ฌ๋  ๋•Œ์™€ ํผ์ด ๋™์ž‘ํ•˜๋Š” ๋ฐฉ์‹์€ ๋น„์Šทํ•ฉ๋‹ˆ๋‹ค.

<form>์˜ ์•ก์…˜์— ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ํผ ํ•„๋“œ์˜ hidden์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„ ํ•จ์ˆ˜๋Š” formData ๋Œ€์‹  hidden์ด ์ ์šฉ๋œ ํผ ํ•„๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { updateCart } from './lib.js';

function AddToCart({productId}) {
async function addToCart(formData) {
'use server'
const productId = formData.get('productId')
await updateCart(productId)
}
return (
<form action={addToCart}>
<input type="hidden" name="productId" value={productId} />
<button type="submit">์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€</button>
</form>
);
}

ํผ ์•ก์…˜์— ๋”ฐ๋ฅธ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด hidden ํผ ํ•„๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋Œ€์‹ ์— bind๋ฅผ ํ˜ธ์ถœํ•ด ์ถ”๊ฐ€ ์ธ์ˆ˜๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ•จ์ˆ˜์— ์ธ์ˆ˜๋กœ ์ „๋‹ฌ๋˜๋Š” formData ์™ธ์— ์ƒˆ ์ธ์ˆ˜(productId)๊ฐ€ ํ•จ์ˆ˜์— ๋ฐ”์ธ๋”ฉ๋ฉ๋‹ˆ๋‹ค.

import { updateCart } from './lib.js';

function AddToCart({productId}) {
async function addToCart(productId, formData) {
"use server";
await updateCart(productId)
}
const addProductToCart = addToCart.bind(null, productId);
return (
<form action={addProductToCart}>
<button type="submit">์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€</button>
</form>
);
}

<form>์ด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์— ์˜ํ•ด ๋ Œ๋”๋ง๋˜๊ณ  ์„œ๋ฒ„ ํ•จ์ˆ˜๊ฐ€ <form>์˜ action ํ”„๋กœํผํ‹ฐ์— ์ „๋‹ฌ๋˜๋ฉด, ํผ์€ ์ ์ง„์ ์œผ๋กœ ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

ํผ์ด ์ œ์ถœ๋˜๋Š” ๋™์•ˆ ๋Œ€๊ธฐ ์ƒํƒœ ๋ณด์—ฌ์ฃผ๊ธฐ

ํผ์ด ์ œ์ถœ๋˜๋Š” ๋™์•ˆ ๋Œ€๊ธฐPending ์ƒํƒœ๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด, <form>์ด ๋ Œ๋”๋ง๋˜๋Š” ์ปดํฌ๋„ŒํŠธ ์•ˆ์—์„œ useFormStatus Hook์„ ํ˜ธ์ถœํ•ด ๋ฐ˜ํ™˜๋œ pending ํ”„๋กœํผํ‹ฐ๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ ํผ์ด ์ œ์ถœ๋˜๊ณ  ์žˆ์Œ์„ ๋‚˜ํƒ€๋‚ด๊ธฐ ์œ„ํ•ด pending ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

import { useFormStatus } from "react-dom";
import { submitForm } from "./actions.js";

function Submit() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "์ œ์ถœ์ค‘..." : "์ œ์ถœ"}
    </button>
  );
}

function Form({ action }) {
  return (
    <form action={action}>
      <Submit />
    </form>
  );
}

export default function App() {
  return <Form action={submitForm} />;
}

useFormStatus Hook์— ๋Œ€ํ•ด ๋” ์•Œ๊ณ  ์‹ถ๋‹ค๋ฉด, ์ฐธ๊ณ  ๋ฌธ์„œ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

๋‚™๊ด€์ ์œผ๋กœ ํผ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ

useOptimistic Hook์€ ๋„คํŠธ์›Œํฌ ์š”์ฒญ๊ณผ ๊ฐ™์€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์˜ ์ž‘์—…์ด ๋๋‚˜๊ธฐ ์ „์— ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค์— ๋‚™๊ด€์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํผ์˜ ๋งฅ๋ฝ์—์„œ ์ด ๊ธฐ์ˆ ์€ ์•ฑ์„ ๋”์šฑ ๋ฐ˜์‘ํ˜•์œผ๋กœ ๋А๋ผ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ํผ์„ ์ œ์ถœํ•˜๋ฉด ์ธํ„ฐํŽ˜์ด์Šค๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ๊ฒฐ๊ณผ๋ฌผ๋กœ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž๊ฐ€ ํผ์— ๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•˜๊ณ  โ€œ์ „์†กโ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด useOptimistic Hook์€ โ€œ์ „์†ก์ค‘โ€ฆโ€ ๋ผ๋ฒจ๊ณผ ํ•จ๊ป˜ ๋ฉ”์‹œ์ง€๊ฐ€ ์„œ๋ฒ„์— ๋ณด๋‚ด์ง€๊ธฐ ์ „์— ๋ฆฌ์ŠคํŠธ์— ์ฆ‰์‹œ ๋ณด์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ โ€˜๋‚™๊ด€์ ์ธโ€™ ์ ‘๊ทผ ๋ฐฉ์‹์€ ์†๋„์™€ ๋ฐ˜์‘์„ฑ์ด ๋›ฐ์–ด๋‚˜๋‹ค๋Š” ์ธ์ƒ์„ ์ค๋‹ˆ๋‹ค. ๊ทธ๋‹ค์Œ ํผ์€ ์‹ค์ œ๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์— ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ๋ฅผ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„์— ๋ฉ”์‹œ์ง€๊ฐ€ ์ž˜ ๋„์ฐฉํ•˜๋ฉด, โ€œ์ „์†ก์ค‘โ€ฆโ€ ๋ผ๋ฒจ์€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

import { useOptimistic, useState, useRef } from "react";
import { deliverMessage } from "./actions.js";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  async function formAction(formData) {
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true
      }
    ]
  );

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small> (์ „์†ก์ค‘...)</small>}
        </div>
      ))}
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">์ „์†ก</button>
      </form>
    </>
  );
}

export default function App() {
  const [messages, setMessages] = useState([
    { text: "Hello there!", sending: false, key: 1 }
  ]);
  async function sendMessage(formData) {
    const sentMessage = await deliverMessage(formData.get("message"));
    setMessages((messages) => [...messages, { text: sentMessage }]);
  }
  return <Thread messages={messages} sendMessage={sendMessage} />;
}

ํผ ์ œ์ถœ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌํ•˜๊ธฐ

<form>์˜ action ํ”„๋กœํผํ‹ฐ๋กœ ์ „๋‹ฌ๋œ ์–ด๋–ค ํ•จ์ˆ˜๋Š” ์˜ค๋ฅ˜๋ฅผ ๋˜์ง€๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฐ ์˜ค๋ฅ˜๋ฅผ <form>์— ์—๋Ÿฌ ๋ฐ”์šด๋”๋ฆฌ๋ฅผ ๊ฐ์‹ธ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ <form>์˜ action ํ”„๋กœํผํ‹ฐ์—์„œ ํ˜ธ์ถœ๋œ ํ•จ์ˆ˜๊ฐ€ ์˜ค๋ฅ˜๋ฅผ ๋˜์ง„๋‹ค๋ฉด ์—๋Ÿฌ ๋ฐ”์šด๋”๋ฆฌ์˜ Fallback์ด ๋ณด์ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

import { ErrorBoundary } from "react-error-boundary";

export default function Search() {
  function search() {
    throw new Error("search error");
  }
  return (
    <ErrorBoundary
      fallback={<p>ํผ ์ œ์ถœ ์ค‘์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.</p>}
    >
      <form action={search}>
        <input name="query" />
        <button type="submit">๊ฒ€์ƒ‰</button>
      </form>
    </ErrorBoundary>
  );
}

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์—†์ด ํผ ์ œ์ถœ ์˜ค๋ฅ˜ ๋ณด์—ฌ์ฃผ๊ธฐ

์ ์ง„์  ํ–ฅ์ƒ์„ ์œ„ํ•ด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค์ด ๋กœ๋“œ๋˜๊ธฐ ์ „ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด ๋‹ค์Œ ์š”์†Œ๋“ค์ด ์ง€์ผœ์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  1. <form> be rendered by a Client Component
  2. the function passed to the <form>โ€™s action prop be a Server Function
  3. the useActionState Hook be used to display the error message

useActionState๋Š” ์„œ๋ฒ„ ํ•จ์ˆ˜์™€ ์ดˆ๊ธฐ State๋ผ๋Š” ๋‘ ๊ฐœ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค. useActionState๋Š” State ๋ณ€์ˆ˜์™€ ์•ก์…˜์ด๋ผ๋Š” ๋‘ ๊ฐœ์˜ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. useActionState๋ฅผ ํ†ตํ•ด ๋ฐ˜ํ™˜๋œ ์•ก์…˜์€ ํผ์˜ action ํ”„๋กœํผํ‹ฐ์— ์ „๋‹ฌ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. useActionState๋ฅผ ํ†ตํ•ด ๋ฐ˜ํ™˜๋œ ์ƒํƒœ ๋ณ€์ˆ˜๋Š” ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. useActionState์— ์ „๋‹ฌ๋œ ์„œ๋ฒ„ ํ•จ์ˆ˜์—์„œ ๋ฐ˜ํ™˜๋œ ๊ฐ’์€ State ๋ณ€์ˆ˜๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

import { useActionState } from "react";
import { signUpNewUser } from "./api";

export default function Page() {
  async function signup(prevState, formData) {
    "use server";
    const email = formData.get("email");
    try {
      await signUpNewUser(email);
      alert(`"${email}"์„ ๋“ฑ๋กํ–ˆ์–ด์š”`);
    } catch (err) {
      return err.toString();
    }
  }
  const [message, signupAction] = useActionState(signup, null);
  return (
    <>
      <h1>๋‰ด์Šค๋ ˆํ„ฐ์— ๊ฐ€์ž…ํ•˜์„ธ์š”</h1>
      <p>๊ฐ™์€ ์ด๋ฉ”์ผ๋กœ ๋‘ ๋ฒˆ ๊ฐ€์ž…ํ•˜์—ฌ ์˜ค๋ฅ˜๋ฅผ ํ™•์ธํ•˜์„ธ์š”.</p>
      <form action={signupAction} id="signup-form">
        <label htmlFor="email">์ด๋ฉ”์ผ: </label>
        <input name="email" id="email" placeholder="react@example.com" />
        <button>๊ฐ€์ž…ํ•˜๊ธฐ</button>
        {!!message && <p>{message}</p>}
      </form>
    </>
  );
}

useActionState ๋ฌธ์„œ๋ฅผ ํ†ตํ•ด ํผ ์ž‘์—…์—์„œ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์•Œ์•„๋ณด์„ธ์š”.

๋‹ค์–‘ํ•œ ์ œ์ถœ ํƒ€์ž… ์ฒ˜๋ฆฌํ•˜๊ธฐ

์‚ฌ์šฉ์ž๊ฐ€ ๋ˆ„๋ฅธ ๋ฒ„ํŠผ์— ๋”ฐ๋ผ ์—ฌ๋Ÿฌ ์ œ์ถœ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํผ์„ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํผ ๋‚ด๋ถ€์˜ ๊ฐ ๋ฒ„ํŠผ์€ formAction ํ”„๋กœํผํ‹ฐ๋ฅผ ์„ค์ •ํ•˜์—ฌ ๊ณ ์œ ํ•œ ๋™์ž‘ ๋˜๋Š” ๋™์ž‘๊ณผ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ํผ์ด ์ œ์ถœ๋˜๊ณ  ํ•ด๋‹น ๋ฒ„ํŠผ์˜ ์†์„ฑ ๋ฐ ๋™์ž‘์œผ๋กœ ์ •์˜๋œ ํ•ด๋‹น ๋™์ž‘์ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํผ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ฒ€ํ† ๋ฅผ ์œ„ํ•ด ๋ฌธ์„œ๋ฅผ ์ œ์ถœํ•˜์ง€๋งŒ formAction์ด ์„ค์ •๋œ ๋ณ„๋„์˜ ๋ฒ„ํŠผ์ด ์žˆ์–ด ๋ฌธ์„œ๋ฅผ ์ดˆ์•ˆ์œผ๋กœ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

export default function Search() {
  function publish(formData) {
    const content = formData.get("content");
    const button = formData.get("button");
    alert(`'${button}' ๋ฒ„ํŠผ์œผ๋กœ '${content}'๊ฐ€ ๋ฐœํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`);
  }

  function save(formData) {
    const content = formData.get("content");
    alert(`'${content}' ์ดˆ์•ˆ์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!`);
  }

  return (
    <form action={publish}>
      <textarea name="content" rows={4} cols={40} />
      <br />
      <button type="submit" name="button" value="submit">๋ฐœํ–‰</button>
      <button formAction={save}>์ดˆ์•ˆ ์ €์žฅ</button>
    </form>
  );
}