๋ด์ฅ ๋ธ๋ผ์ฐ์ <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> ); }
์๋ฐ์คํฌ๋ฆฝํธ ์์ด ํผ ์ ์ถ ์ค๋ฅ ๋ณด์ฌ์ฃผ๊ธฐ
์ ์ง์ ํฅ์์ ์ํด ์๋ฐ์คํฌ๋ฆฝํธ ๋ฒ๋ค์ด ๋ก๋๋๊ธฐ ์ ์ค๋ฅ ๋ฉ์์ง๋ฅผ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด ๋ค์ ์์๋ค์ด ์ง์ผ์ ธ์ผ ํฉ๋๋ค.
<form>be rendered by a Client Component- the function passed to the
<form>โsactionprop be a Server Function - the
useActionStateHook 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> ); }