React 폼은 각 필드의 값을 state에 보관합니다. input은 value={state}를 보여 주고 onChange로 그 값을 업데이트합니다. 사용자가 제출하면 onSubmit 핸들러가 e.preventDefault()를 호출해 페이지 새로고침을 막고, 이미 state에 있는 값을 사용합니다.
이름을 입력하고 Enter를 누르거나 버튼을 클릭하세요. 그다음 e.preventDefault(); 줄을 지워 보세요. 미리보기는 브라우저가 기본 제출에서 보냈을 필드를 보여 주며, 실제 페이지라면 새로고침도 일어났을 것입니다.
값이 state에서 오는 input을 제어 input이라고 합니다. DOM이 값을 보관하게 두고 나중에 읽는 다른 방식은 제어 컴포넌트와 비제어 컴포넌트에서 다룹니다.
텍스트 input과 textarea
모든 텍스트 필드는 같은 방식으로 동작합니다. value는 state에서 오고, onChange는 e.target.value를 읽습니다. <textarea>도 같은 패턴을 따릅니다. HTML에서는 텍스트가 태그 사이에 들어가지만, React에서는 value로 넘깁니다.
값을 직접 설정하기 때문에 사용자가 입력한 내용을 표시하기 전에 바꿀 수 있습니다. 제목 필드는 모든 글자를 대문자로 바꿉니다. body가 state이고 변경될 때마다 다시 렌더링되므로, 글자 수 카운터는 키를 누를 때마다 업데이트됩니다.
select
<select>는 option의 selected가 아니라 select 요소 자체에 value를 받습니다. e.target.value는 선택된 option의 value입니다.
다중 선택이라면 multiple을 추가하고 value로 배열을 넘기세요. 선택된 값은 Array.from(e.target.selectedOptions, (o) => o.value)로 읽습니다.
체크박스와 라디오 버튼
체크박스는 value가 아니라 checked로 제어하며, 핸들러는 true 또는 false인 e.target.checked를 읽습니다. 한 그룹의 라디오 버튼은 같은 name을 공유하고, 각 버튼은 state가 자기 값과 같을 때 체크됩니다.
체크박스에서 e.target.value를 읽는 것은 흔한 실수입니다. value 속성이 없는 체크박스에서는 체크 여부와 상관없이 문자열 "on"을 반환합니다.
여러 필드를 위한 하나의 state 객체
필드가 열 개인 폼에 useState 호출 열 개와 핸들러 열 개가 필요하지는 않습니다. 객체 하나를 두고, 모든 input에 키와 일치하는 name을 주고, 그 이름을 계산된 키 [e.target.name]으로 쓰는 핸들러 하나를 작성하세요.
핸들러는 input 타입을 확인해서 체크박스는 불리언을, 나머지는 텍스트를 저장합니다. 필드를 추가하려면 시작 객체에 키를 추가하고 같은 name을 가진 input을 추가하면 됩니다. 핸들러는 바꿀 필요가 없습니다. 전개가 다른 모든 필드를 유지합니다(배열과 객체 업데이트 참고).
폼 제출하기
제출 로직은 버튼의 onClick이 아니라 폼의 onSubmit에 두세요. onSubmit은 사용자가 텍스트 필드에서 Enter를 누를 때도 발생하는데, 클릭 핸들러는 이를 놓칩니다. 폼 안의 <button>은 기본적으로 제출 버튼이므로, 다른 버튼에는 제출하지 않도록 type="button"을 주세요.
콘솔을 열고 비밀번호를 입력한 다음 Enter를 누르세요. 폼이 객체 전체를 로그로 남깁니다. "Clear"를 클릭하면 type="button"이므로 아무것도 기록되지 않습니다. 그 속성을 지우면 Clear도 제출하기 시작합니다.
유효성 검사와 오류 표시
간단한 유효성 검사는 값을 받아 메시지 객체를 반환하는 함수입니다. 제출할 때 실행하고, 결과를 state에 저장하고, 각 메시지를 해당 필드 아래에 보여 주세요. 요청이 진행되는 동안 제출 버튼을 비활성화하면 더블 클릭으로 폼이 두 번 전송되지 않습니다.
필드를 비운 채로 Save를 클릭하면 두 메시지가 모두 보이고, 그다음 필드를 채워 보세요. saveProfile은 실제 요청을 대신하는 함수로, 800밀리초를 기다린 뒤 완료됩니다. noValidate 속성은 브라우저 자체의 유효성 검사 말풍선을 꺼서 직접 만든 메시지만 보이게 합니다. 이 속성을 지우고 input에 required를 추가해 비교해 보세요.
오류를 저장하지 않고 렌더링 중에 계산할 수도 있습니다. 예를 들어 const emailError = form.email && !form.email.includes('@')는 사용자가 입력하는 동안 메시지를 보여 줍니다. 폼에 맞는 시점을 고르세요. 제출할 때 검사하면 더 차분하고, 입력하는 동안 검사하면 피드백이 더 빠릅니다.
React 19의 폼 액션
React 19는 폼을 처리하는 또 다른 방법을 추가합니다. 폼의 action prop에 함수를 넘기세요. React가 대신 페이지 새로고침을 막고, name을 가진 모든 필드를 담은 FormData 객체와 함께 함수를 호출합니다. 필드에는 state가 전혀 필요 없습니다. 비제어 필드이며, 액션이 끝나면 React가 비웁니다.
직접 댓글을 입력해서 올려 보세요. 목록에는 입력한 텍스트가 보이고, input은 Nice docs로 돌아갑니다. 액션이 완료되면 React가 비제어 필드를 defaultValue로 초기화하기 때문입니다. 액션은 비동기일 수 있으며, 실행되는 동안 React는 그 업데이트를 트랜지션으로 취급합니다.
액션의 대기 상태나 오류를 보여 주려면 useActionState로 감싸세요. 최신 결과, 폼에 넘길 감싼 액션, 그리고 isPending 플래그를 반환합니다. 폼 깊숙이 있는 버튼도 react-dom의 useFormStatus로 대기 상태를 읽을 수 있습니다.
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Posting...' : 'Post'}</button>;
}
useFormStatus는 여기 에디터에서 불러오지 않으므로 이 정적 블록에서만 보여 줍니다. 폼을 렌더링하는 컴포넌트가 아니라 <form> 안에서 렌더링되는 컴포넌트에서 호출해야 합니다.
React 19에서는 onSubmit과 함께 쓰는 제어 state와 FormData를 쓰는 폼 액션이 모두 동작합니다. 제어 필드는 입력하는 동안 반응하는 폼(실시간 유효성 검사, 글자 수 카운터, 서로 의존하는 필드)에 적합합니다. 액션은 사용자가 제출할 때만 값이 필요한 폼에 적합합니다.
자주 묻는 질문
React에서 input의 값은 어떻게 가져오나요?
state에 저장하고 onChange에서 업데이트하세요: <input value={name} onChange={e => setName(e.target.value)} />. 그러면 현재 텍스트가 항상 name에 있습니다. 체크박스는 e.target.value 대신 e.target.checked를 읽으세요.
React에서 폼이 페이지를 새로고침하지 않게 하려면 어떻게 하나요?
onSubmit 핸들러의 시작 부분에서 e.preventDefault()를 호출하세요. 이것이 없으면 브라우저가 일반적인 폼 제출을 수행해서 페이지를 이동하고 새로고침합니다.
React에서 여러 input을 하나의 state로 처리하려면 어떻게 하나요?
state에 객체 하나를 두고, 모든 input에 키와 일치하는 name을 주고, 계산된 키를 쓰는 핸들러 하나를 사용하세요: setForm({ ...form, [e.target.name]: e.target.value }).
React에서 textarea는 왜 children 대신 value를 쓰나요?
HTML에서 textarea의 텍스트는 태그 사이에 있지만, React에서는 input과 마찬가지로 value(또는 defaultValue)로 넘깁니다. 그래서 React 폼의 모든 텍스트 필드가 똑같이 동작합니다.
React 19의 폼 액션이란 무엇인가요?
React 19에서는 <form action={fn}>에 함수를 넘길 수 있습니다. React는 페이지 새로고침을 막고, 폼의 FormData와 함께 함수를 호출하며, 끝나면 비제어 필드를 초기화합니다. useActionState는 그 위에 대기 상태 플래그와 결과를 더합니다.