본문 바로가기
C.W.K.
Stream
Lesson 01 of 06 · published

React 19의 폼 액션 모델

~13 min · form-action, react-19, forms

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
React 19는 form의 action에 함수를 연결해 제출 생명주기를 관리할 수 있어. 브라우저 기본 동작과 React 액션의 차이를 먼저 잡아 보자.

action의 두 가지 모드

  • <form action="/save" method="POST">: 브라우저가 전체 페이지 제출을 처리해. JavaScript가 없어도 동작하는 네이티브 방식이야.
  • <form action={saveFn}>: React가 submit을 가로채 saveFn(formData)을 호출하고 대기 상태와 결과를 관리해.

함수를 넘기는 두 번째 방식이 React 19 Actions의 출발점이야. 뒤에서 다룰 useActionState, useFormStatus, useOptimistic도 이 제출 흐름을 전제로 해.

입력은 FormData로 받는다

액션 함수는 브라우저 표준 타입인 FormData를 받아. formData.get('name')으로 텍스트 필드를 읽고, 파일은 instanceof File로 확인할 수 있어. 직접 모든 input 값을 모아 JSON으로 만들 필요가 없어.

성공 뒤 form 초기화

액션이 성공하면 React는 uncontrolled input을 초기화해. 입력 값을 계속 유지해야 하는 다단계 form이라면 useState로 controlled input을 만들고 성공 경로에서 상태를 직접 정리해야 해.

브라우저 검증은 먼저 동작한다

required, type="email", minlength 같은 네이티브 검증을 통과하지 못하면 액션은 호출되지 않아. 그래도 클라이언트 검증만 믿으면 안 돼. 서버가 신뢰 경계라면 입력을 다시 검증해야 하고, Lesson 6에서 Zod로 그 경계를 만들 거야.

form이 입력 모델이고 함수가 처리기야. input에 name을 붙이면 브라우저가 값을 모으고, React에는 실제 작업을 할 함수를 넘겨. 예전의 onSubmit, preventDefault(), 수동 필드 수집, fetch, 여러 setState를 한꺼번에 다루던 흐름보다 짧고 책임도 분명해져.

Code

가장 단순한 Action을 form에 전달하기·tsx
async function saveDraft(formData: FormData) {
  const title = formData.get("title") as string;
  const body = formData.get("body") as string;
  await fetch("/api/drafts", {
    method: "POST",
    body: JSON.stringify({ title, body }),
  });
}

function DraftForm() {
  return (
    <form action={saveDraft} className="space-y-4">
      <input name="title" required className="w-full px-3 py-2 border" />
      <textarea name="body" required className="w-full px-3 py-2 border" />
      <button type="submit" className="px-4 py-2 bg-brand text-bg rounded">
        Save draft
      </button>
    </form>
  );
}

// e.preventDefault 없음, title/body의 useState 없음, onSubmit의 fetch 없음.
// React가 제출할 때 FormData를 saveDraft에 전달해.
Form 없이 버튼 하나에 Action 연결하기·tsx
// 자체 Action을 지정한 버튼이 form의 기본 Action을 덮어써.
async function deleteDraft() {
  await fetch("/api/drafts/current", { method: "DELETE" });
}

function DraftForm2() {
  return (
    <form action={saveDraft}>
      <input name="title" required />
      <textarea name="body" required />
      <div className="flex gap-2">
        <button type="submit">Save</button>
        <button type="submit" formAction={deleteDraft} className="text-danger">
          Delete
        </button>
      </div>
    </form>
  );
}

// 'Delete' 클릭이 saveDraft 대신 deleteDraft 호출.
// 같은 form, 버튼 둘, action 둘. 네이티브 HTML이 지원하고 React 19가 그 동작을 존중해.

External links

Exercise

텍스트 input과 submit 버튼이 있는 NewMessageForm을 만들고 <form action={sendMessage}>에 연결해. 제출 뒤 uncontrolled input이 비워지는지 확인해. 이어서 POST하지 않고 초안만 저장하는 formAction={saveDraft} 버튼을 하나 더 넣고 두 버튼이 각자 올바른 함수를 호출하는지 확인해.
Hint
자동 초기화를 보려면 input에 value prop을 넣지 마. controlled input으로 바꾸면 액션은 실행되지만 표시 값은 상태에서 직접 지워야 해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.