<form>브라우저 내장 <form> 컴포넌트를 사용하면 정보를 제출하기 위한 인터랙티브 컨트롤을 만들 수 있어요.
<form action={search}>
<input name="query" />
<button type="submit">Search</button>
</form>
폼은 사용자로부터 데이터를 입력받아 서버나 클라이언트 측 함수로 전송하는 가장 기본적인 방법이에요. React에서는 이 <form> 컴포넌트를 더욱 강력하게 활용할 수 있어요!
<form>정보를 제출하기 위한 인터랙티브 컨트롤을 만들려면, 브라우저 내장 <form> 컴포넌트를 렌더링하세요.
<form action={search}>
<input name="query" />
<button type="submit">Search</button>
</form>
<form>은 모든 공통 요소 props를 지원해요.
action: URL 또는 함수예요. action에 URL이 전달되면 폼은 HTML form 컴포넌트처럼 동작해요. action에 함수가 전달되면 그 함수가 Action prop 패턴을 따르는 Transition 안에서 폼 제출을 처리해요. action에 전달된 함수는 async일 수 있고, 제출된 폼의 form data를 담은 단일 인자로 호출될 거예요. action prop은 <button>, <input type="submit">, 또는 <input type="image"> 컴포넌트의 formAction 속성으로 재정의할 수 있어요.
action이나 formAction에 함수가 전달되면, method prop의 값과 관계없이 HTTP 메서드는 POST가 될 거예요.폼의 action prop에 함수를 전달해서 폼이 제출될 때 그 함수를 실행할 수 있어요. formData가 함수의 인자로 전달되어서 폼에서 제출된 데이터에 접근할 수 있어요. 이건 URL만 받는 기존 HTML action과는 달라요. action 함수가 성공하면, 폼 안의 모든 비제어 필드 요소들이 리셋돼요.
// src/App.js
export default function Search() {
function search(formData) {
const query = formData.get("query");
alert(`You searched for '${query}'`);
}
return (
<form action={search}>
<input name="query" />
<button type="submit">Search</button>
</form>
);
}
input과 submit 버튼이 있는 <form>을 렌더링하세요. 폼의 action prop에 Server Function('use server'로 표시된 함수)을 전달해서 폼이 제출될 때 그 함수를 실행할 수 있어요.
<form action>에 Server Function을 전달하면 JavaScript가 비활성화되어 있거나 코드가 로드되기 전에도 사용자가 폼을 제출할 수 있어요. 이건 느린 연결, 기기, 또는 JavaScript가 비활성화된 사용자에게 유용하고, URL이 action prop에 전달될 때 폼이 동작하는 방식과 비슷해요.
hidden form 필드를 사용해서 <form>의 action에 데이터를 제공할 수 있어요. Server Function은 FormData 인스턴스로 hidden form 필드 데이터와 함께 호출될 거예요.
// 예시
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">Add to Cart</button>
</form>
);
}
hidden form 필드를 사용하는 대신, 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">Add to Cart</button>
</form>
);
}
<form>이 Server Component에 의해 렌더링되고, Server Function이 <form>의 action prop에 전달되면, 폼은 점진적으로 향상(progressively enhanced)돼요.
폼이 제출되는 동안 pending 상태를 표시하려면, <form> 안에 렌더링된 컴포넌트에서 useFormStatus Hook을 호출하고 반환된 pending 속성을 읽을 수 있어요.
여기서는 pending 속성을 사용해서 폼이 제출 중임을 표시해요.
// src/App.js
import { useFormStatus } from "react-dom";
import { submitForm } from "./actions.js";
function Submit() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting..." : "Submit"}
</button>
);
}
function Form({ action }) {
return (
<form action={action}>
<Submit />
</form>
);
}
export default function App() {
return <Form action={submitForm} />;
}
// src/actions.js
export async function submitForm(query) {
await new Promise((res) => setTimeout(res, 1000));
}
useFormStatus Hook에 대해 더 알아보려면 레퍼런스 문서를 참고하세요.
useOptimistic Hook은 네트워크 요청 같은 백그라운드 작업이 완료되기 전에 사용자 인터페이스를 낙관적으로 업데이트하는 방법을 제공해요. 폼의 맥락에서 이 기법은 앱이 더 반응적으로 느껴지게 해줘요. 사용자가 폼을 제출하면, 서버의 응답을 기다려서 변경 사항을 반영하는 대신, 예상되는 결과로 인터페이스가 즉시 업데이트돼요.
예를 들어, 사용자가 폼에 메시지를 입력하고 "Send" 버튼을 누르면, useOptimistic Hook은 메시지가 실제로 서버에 전송되기 전에 "Sending..." 라벨과 함께 목록에 즉시 나타나게 해요. 이 "낙관적" 접근 방식은 속도와 반응성의 인상을 줘요. 그 다음 폼은 백그라운드에서 실제로 메시지를 전송하려고 시도해요. 서버가 메시지가 수신되었음을 확인하면, "Sending..." 라벨이 제거돼요.
// src/App.js
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> (Sending...)</small>}
</div>
))}
<form action={formAction} ref={formRef}>
<input type="text" name="message" placeholder="Hello!" />
<button type="submit">Send</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} />;
}
// src/actions.js
export async function deliverMessage(message) {
await new Promise((res) => setTimeout(res, 1000));
return message;
}
어떤 경우에는 <form>의 action prop에 의해 호출된 함수가 에러를 던질 수 있어요. 이런 에러는 <form>을 Error Boundary로 감싸서 처리할 수 있어요. <form>의 action prop에 의해 호출된 함수가 에러를 던지면, error boundary의 fallback이 표시될 거예요.
// src/App.js
import { ErrorBoundary } from "react-error-boundary";
export default function Search() {
function search() {
throw new Error("search error");
}
return (
<ErrorBoundary
fallback={<p>There was an error while submitting the form</p>}
>
<form action={search}>
<input name="query" />
<button type="submit">Search</button>
</form>
</ErrorBoundary>
);
}
// package.json
{
"dependencies": {
"react": "19.0.0-rc-3edc000d-20240926",
"react-dom": "19.0.0-rc-3edc000d-20240926",
"react-scripts": "^5.0.0",
"react-error-boundary": "4.0.3"
},
"main": "/index.js",
"devDependencies": {}
}
점진적 향상을 위해 JavaScript 번들이 로드되기 전에 폼 제출 에러 메시지를 표시하려면 다음이 필요해요:
<form>이 Client Component에 의해 렌더링되어야 해요<form>의 action prop에 전달된 함수가 Server Function이어야 해요useActionState Hook을 사용해서 에러 메시지를 표시해야 해요useActionState는 두 개의 매개변수를 받아요: Server Function과 초기 상태. useActionState는 두 개의 값을 반환해요: state 변수와 action. useActionState가 반환한 action은 폼의 action prop에 전달해야 해요. useActionState가 반환한 state 변수는 에러 메시지를 표시하는 데 사용할 수 있어요. useActionState에 전달된 Server Function이 반환한 값은 state 변수를 업데이트하는 데 사용될 거예요.
// src/App.js
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(`Added "${email}"`);
} catch (err) {
return err.toString();
}
}
const [message, signupAction] = useActionState(signup, null);
return (
<>
<h1>Signup for my newsletter</h1>
<p>Signup with the same email twice to see an error</p>
<form action={signupAction} id="signup-form">
<label htmlFor="email">Email: </label>
<input name="email" id="email" placeholder="react@example.com" />
<button>Sign up</button>
{!!message && <p>{message}</p>}
</form>
</>
);
}
// src/api.js
let emails = [];
export async function signUpNewUser(newEmail) {
if (emails.includes(newEmail)) {
throw new Error("This email address has already been added");
}
emails.push(newEmail);
}
폼 action에서 state를 업데이트하는 방법에 대해 더 알아보려면 useActionState 문서를 참고하세요.
폼은 사용자가 누른 버튼에 따라 여러 제출 action을 처리하도록 설계할 수 있어요. 폼 안의 각 버튼은 formAction prop을 설정해서 별개의 action이나 동작과 연결될 수 있어요.
사용자가 특정 버튼을 탭하면, 폼이 제출되고, 해당 버튼의 속성과 action에 의해 정의된 해당 action이 실행돼요. 예를 들어, 폼은 기본적으로 기사를 리뷰용으로 제출하지만 기사를 초안으로 저장하도록 formAction이 설정된 별도의 버튼을 가질 수 있어요.
// src/App.js
export default function Search() {
function publish(formData) {
const content = formData.get("content");
const button = formData.get("button");
alert(`'${content}' was published with the '${button}' button`);
}
function save(formData) {
const content = formData.get("content");
alert(`Your draft of '${content}' has been saved!`);
}
return (
<form action={publish}>
<textarea name="content" rows={4} cols={40} />
<br />
<button type="submit" name="button" value="submit">Publish</button>
<button formAction={save}>Save draft</button>
</form>
);
}
위 예시에서 "Publish" 버튼을 누르면 publish 함수가 실행되고, "Save draft" 버튼을 누르면 formAction에 설정된 save 함수가 대신 실행돼요. 이렇게 하나의 폼에서 여러 동작을 처리할 수 있어요!