본문 바로가기

폼 다루기 기초

input·textarea·select를 상태와 연결하는 제어 컴포넌트를 구현하고 여러 입력을 하나의 핸들러로 관리합니다.

우리는 리스트와 key를 이용하여 여러 데이터를 효율적으로 렌더링하는 방법을 배웠습니다.

이제 사용자로부터 데이터를 입력받는 중요한 UI 요소인 폼(Form)을 리액트에서 어떻게 다루는지 알아볼 차례입니다.

로그인, 회원가입, 게시물 작성, 검색 등 대부분의 웹 애플리케이션은 사용자로부터 다양한 형태의 입력을 받습니다.

리액트는 이러한 폼 요소를 효율적으로 관리하고, 사용자의 입력을 실시간으로 처리하며, 최종적으로 서버로 데이터를 전송하는 과정을 체계적으로 다룰 수 있도록 돕습니다.

이 장에서는 리액트에서 폼을 다루는 핵심 개념인 제어 컴포넌트(Controlled Component)에 대해 집중적으로 알아볼 것입니다.


제어 컴포넌트란?

먼저 폼 값의 주인이 React state인지 DOM인지에 따라 입력 흐름이 어떻게 달라지는지 잡아봅니다.

제어 폼의 입력 루프 — 상태 추적

김코딩 입력의 DOM 현재 값과 React snapshot/요청/다음 value가 루프에서 달라진다. 비제어 DOM과 state 소유권을 구별한다.

입력값은 state를 거쳐 다음 렌더의 value로 돌아옵니다

CONTROLLED INPUT LOOP

입력값은 state를 거쳐 다음 렌더의 value로 돌아옵니다

NameForm은 김코딩이라는 입력을 name에 저장하고 입력 칸과 표시 문장에 같은 값을 사용합니다. 제출은 현재 name을 alert로 보여주며 본문 예제는 서버 요청을 보내지 않습니다.

제어 폼의 입력 루프입력요소의현재DOM값→handleChange의target.value읽기→setName으로다음상태요청→다음렌더에서value전달→입력요소의4노드4간선순환이다. setter가현재함수의상태변수를즉시변경한다고표현하지않는다. 화면값과상태를같은기준으로맞춘다.입력 변경김코딩 입력handleChangetarget.value 읽기setName다음 상태 요청다음 렌더value로 입력에 전달
제어 폼의 입력 루프 · 모바일입력요소의현재DOM값→handleChange의target.value읽기→setName으로다음상태요청→다음렌더에서value전달→입력요소의4노드4간선순환이다. setter가현재함수의상태변수를즉시변경한다고표현하지않는다. 화면값과상태를같은기준으로맞춘다. 모바일은 같은 노드와 방향을 다른 배치로 보여 줍니다.입력 변경김코딩 입력handleChangetarget.value 읽기setName다음 상태 요청다음 렌더value로 입력에 전달

NameForm · 값의 한 경로

onChange → setName(target.value)
name: "김코딩"
value={name}
표시: 입력 중인 이름: 김코딩

state 초기값은 빈 문자열입니다. 제어 입력은 값과 onChange를 연결하고, 제출 시 preventDefault로 기본 페이지 이동을 막습니다.

폼 요소마다 읽을 prop

input / textarea / select: value
checkbox: checked

textarea의 자식 텍스트나 option의 selected 대신 부모의 value를 사용합니다. 체크박스에서는 target.checked를 읽어 불리언 상태를 연결합니다.

여러 입력 · name으로 식별

const {name, value} = event.target;
다음 객체: {...formValues,
  [name]: value}

username·email·password 중 바뀐 필드만 새 객체에서 교체하고 나머지를 보존합니다.

비제어 · DOM에서 읽기

파일 입력은 보통 ref.current.files로 선택 결과를 읽습니다. NameForm의 value 제어와 구분하고, 선택 전 파일 접근·실제 업로드는 별도로 구현합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 설명NameForm은 김코딩이라는 입력을 name에 저장하고 입력 칸과 표시 문장에 같은 값을 사용합니다. 제출은 현재 name을 alert로 보여 주며 본문 예제는 서버 요청을 보내지 않습니다.
전체 관계·조건 설명입력 요소의 현재 DOM 값→handleChange의 target.value 읽기→setName으로 다음 상태 요청→다음 렌더에서 value 전달→입력 요소의 노드 4개·간선 4개 순환이다. setter가 현재 함수의 상태 변수를 즉시 변경한다고 표현하지 않는다. 화면 값과 상태를 같은 기준으로 맞춘다.
전체 관계·조건 설명입력 요소의 현재 DOM 값→handleChange의 target.value 읽기→setName으로 다음 상태 요청→다음 렌더에서 value 전달→입력 요소의 노드 4개·간선 4개 순환이다. setter가 현재 함수의 상태 변수를 즉시 변경한다고 표현하지 않는다. 화면 값과 상태를 같은 기준으로 맞춘다. 모바일은 같은 노드와 방향을 다른 배치로 보여 줍니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
사용자 입력DOM: 김코딩 / state: 현재 nameDOM 값은 김코딩이고 React name은 현재 snapshot이다입력 사건을 받은 순간 두 관찰은 다를 수 있다
setName요청: 김코딩 / 현재 함수: 이전 snapshot 유지target.value로 다음 name을 요청한다handler의 변수를 즉시 교체하는 동작으로 해석하지 않는다
value 반영state: 김코딩 / input value: 김코딩다음 렌더가 김코딩을 input value로 전달한다제어 입력의 값 소유자는 React state다

제어 폼의 입력 루프 — 상태 추적

김코딩 입력의 DOM 현재 값과 React snapshot/요청/다음 value가 루프에서 달라진다. 비제어 DOM과 state 소유권을 구별한다.

입력값은 state를 거쳐 다음 렌더의 value로 돌아옵니다

CONTROLLED INPUT LOOP

입력값은 state를 거쳐 다음 렌더의 value로 돌아옵니다

NameForm은 김코딩이라는 입력을 name에 저장하고 입력 칸과 표시 문장에 같은 값을 사용합니다. 제출은 현재 name을 alert로 보여주며 본문 예제는 서버 요청을 보내지 않습니다.

제어 폼의 입력 루프입력요소의현재DOM값→handleChange의target.value읽기→setName으로다음상태요청→다음렌더에서value전달→입력요소의4노드4간선순환이다. setter가현재함수의상태변수를즉시변경한다고표현하지않는다. 화면값과상태를같은기준으로맞춘다.입력 변경김코딩 입력handleChangetarget.value 읽기setName다음 상태 요청다음 렌더value로 입력에 전달
제어 폼의 입력 루프 · 모바일입력요소의현재DOM값→handleChange의target.value읽기→setName으로다음상태요청→다음렌더에서value전달→입력요소의4노드4간선순환이다. setter가현재함수의상태변수를즉시변경한다고표현하지않는다. 화면값과상태를같은기준으로맞춘다. 모바일은 같은 노드와 방향을 다른 배치로 보여 줍니다.입력 변경김코딩 입력handleChangetarget.value 읽기setName다음 상태 요청다음 렌더value로 입력에 전달

NameForm · 값의 한 경로

onChange → setName(target.value)
name: "김코딩"
value={name}
표시: 입력 중인 이름: 김코딩

state 초기값은 빈 문자열입니다. 제어 입력은 값과 onChange를 연결하고, 제출 시 preventDefault로 기본 페이지 이동을 막습니다.

폼 요소마다 읽을 prop

input / textarea / select: value
checkbox: checked

textarea의 자식 텍스트나 option의 selected 대신 부모의 value를 사용합니다. 체크박스에서는 target.checked를 읽어 불리언 상태를 연결합니다.

여러 입력 · name으로 식별

const {name, value} = event.target;
다음 객체: {...formValues,
  [name]: value}

username·email·password 중 바뀐 필드만 새 객체에서 교체하고 나머지를 보존합니다.

비제어 · DOM에서 읽기

파일 입력은 보통 ref.current.files로 선택 결과를 읽습니다. NameForm의 value 제어와 구분하고, 선택 전 파일 접근·실제 업로드는 별도로 구현합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 설명NameForm은 김코딩이라는 입력을 name에 저장하고 입력 칸과 표시 문장에 같은 값을 사용합니다. 제출은 현재 name을 alert로 보여 주며 본문 예제는 서버 요청을 보내지 않습니다.
전체 관계·조건 설명입력 요소의 현재 DOM 값→handleChange의 target.value 읽기→setName으로 다음 상태 요청→다음 렌더에서 value 전달→입력 요소의 노드 4개·간선 4개 순환이다. setter가 현재 함수의 상태 변수를 즉시 변경한다고 표현하지 않는다. 화면 값과 상태를 같은 기준으로 맞춘다.
전체 관계·조건 설명입력 요소의 현재 DOM 값→handleChange의 target.value 읽기→setName으로 다음 상태 요청→다음 렌더에서 value 전달→입력 요소의 노드 4개·간선 4개 순환이다. setter가 현재 함수의 상태 변수를 즉시 변경한다고 표현하지 않는다. 화면 값과 상태를 같은 기준으로 맞춘다. 모바일은 같은 노드와 방향을 다른 배치로 보여 줍니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
사용자 입력DOM: 김코딩 / state: 현재 nameDOM 값은 김코딩이고 React name은 현재 snapshot이다입력 사건을 받은 순간 두 관찰은 다를 수 있다
setName요청: 김코딩 / 현재 함수: 이전 snapshot 유지target.value로 다음 name을 요청한다handler의 변수를 즉시 교체하는 동작으로 해석하지 않는다
value 반영state: 김코딩 / input value: 김코딩다음 렌더가 김코딩을 input value로 전달한다제어 입력의 값 소유자는 React state다

HTML에서 <input>, <textarea>, <select>와 같은 폼 요소들은 사용자 입력을 통해 자체적인 상태(state)를 가집니다.

예를 들어, input 필드에 텍스트를 입력하면 해당 필드 자체가 그 텍스트 값을 내부적으로 관리합니다.

이러한 폼 요소를 비제어 컴포넌트(Uncontrolled Component)라고 합니다.

하지만 리액트에서는 폼 요소의 상태를 리액트 컴포넌트의 state로 관리하는 방식을 선호합니다.

즉, 사용자가 폼 요소에 값을 입력하면, 그 값이 컴포넌트의 state에 저장되고, 폼 요소는 이 state에 의해 제어됩니다.

이렇게 리액트의 state에 의해 그 값이 제어되는 폼 요소를 제어 컴포넌트(Controlled Component)라고 부릅니다.

제어 컴포넌트의 장점
  • 단일 진실 공급원(Single Source of Truth): 폼 요소의 현재 값이 항상 리액트 컴포넌트의 state에 저장되어 있으므로, 데이터의 흐름을 예측하기 쉽고 디버깅이 용이합니다.
  • 실시간 유효성 검사 및 피드백: state가 실시간으로 업데이트되므로, 사용자가 입력하는 즉시 유효성 검사를 수행하고 오류 메시지 등을 제공할 수 있습니다.
  • 쉬운 값 조작: 프로그래밍 방식으로 폼 요소의 값을 변경하거나 초기화하는 것이 쉽습니다.
  • 다양한 사용자 정의 로직 적용: state를 기반으로 복잡한 사용자 정의 로직(예: 입력 포맷팅, 자동 완성)을 적용할 수 있습니다.

input 태그 다루기: 텍스트 입력

가장 기본적인 폼 요소인 <input type="text">를 제어 컴포넌트로 다루는 방법을 살펴보겠습니다.

예제: 사용자 이름 입력 폼

NameForm.jsx 컴포넌트 생성: src/components 폴더 안에 NameForm.jsx 파일을 생성합니다.

src/components/NameForm.jsx
import { useState } from 'react';

function NameForm() {
  // (1) input의 현재 값을 저장할 state 선언
  const [name, setName] = useState('');

  // (2) input 값이 변경될 때 호출될 이벤트 핸들러
  const handleChange = (event) => {
    // event.target.value: input 필드의 현재 값
    setName(event.target.value); // state 업데이트
    console.log('현재 입력 값:', event.target.value);
  };

  // (3) 폼 제출 시 호출될 이벤트 핸들러
  const handleSubmit = (event) => {
    alert('제출된 이름: ' + name); // state에 저장된 이름 출력
    event.preventDefault(); // (4) 폼의 기본 동작(페이지 새로고침) 방지
  };

  return (
    <div style={{ border: '1px solid #009688', padding: '20px', margin: '20px', borderRadius: '8px' }}>
      <h2>이름 입력 폼</h2>
      <form onSubmit={handleSubmit}>
        <label>
          이름:
          <input
            type="text"
            value={name}
            onChange={handleChange}
            style={{ marginLeft: '10px', padding: '8px', fontSize: '16px', borderRadius: '4px', border: '1px solid #ccc' }}
          />
        </label>
        <button type="submit" style={{ marginLeft: '10px', padding: '8px 15px', fontSize: '16px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
          제출
        </button>
      </form>
      <p style={{ marginTop: '15px' }}>
        입력 중인 이름: <span style={{ fontWeight: 'bold', color: '#007bff' }}>{name}</span>
      </p>
    </div>
  );
}

export default NameForm;
  • name state: 입력 필드의 현재 텍스트 값을 저장합니다. 초기값은 빈 문자열('')입니다.
  • handleChange: <input>의 onChange 이벤트에 연결된 함수입니다. event.target.value를 통해 사용자가 입력한 값을 가져와 setName 함수로 name state를 업데이트합니다. 이 과정이 실시간으로 입력 필드의 값을 state와 동기화하는 핵심입니다.
  • handleSubmit: <form>의 onSubmit 이벤트에 연결된 함수입니다. 폼이 제출될 때 호출됩니다. event.preventDefault()는 폼 제출 시 페이지가 새로고침되는 브라우저의 기본 동작을 막아줍니다.
  • value={name}: <input> 태그의 value 속성을 name state에 연결했습니다. 이로써 input 필드의 값은 항상 name state에 의해 제어됩니다. 이것이 바로 제어 컴포넌트의 핵심입니다.
App.jsx에서 NameForm 컴포넌트 사용
src/App.jsx
import './App.css';
import NameForm from './components/NameForm'; // NameForm 컴포넌트 불러오기

function App() {
  return (
    <div className="App">
      <h1>React 폼 다루기</h1>
      <NameForm />
    </div>
  );
}

export default App;

결과 확인: 브라우저에서 입력 필드에 텍스트를 입력해보고, 아래 입력 중인 이름 텍스트가 실시간으로 변하는 것을 확인해 보세요.

제출 버튼을 클릭하면 alert 창에 입력된 이름이 나타나고 페이지는 새로고침되지 않을 것입니다.


textarea 태그 다루기

<textarea> 태그는 여러 줄의 텍스트 입력을 받을 때 사용합니다.

input과 거의 동일한 방식으로 다룰 수 있습니다.

예제: 자유 텍스트 입력 폼
src/components/EssayForm.jsx
import { useState } from 'react';

function EssayForm() {
  const [essay, setEssay] = useState('이곳에 에세이를 작성하세요...'); // 초기값 설정

  const handleChange = (event) => {
    setEssay(event.target.value);
  };

  const handleSubmit = (event) => {
    alert('제출된 에세이:\n' + essay);
    event.preventDefault();
  };

  return (
    <div style={{ border: '1px solid #fd7e14', padding: '20px', margin: '20px', borderRadius: '8px' }}>
      <h2>에세이 작성 폼</h2>
      <form onSubmit={handleSubmit}>
        <label>
          에세이:
          <textarea
            value={essay}
            onChange={handleChange}
            rows="5"
            cols="40"
            style={{ display: 'block', marginTop: '10px', padding: '8px', fontSize: '16px', borderRadius: '4px', border: '1px solid #ccc', width: '90%' }}
          />
        </label>
        <button type="submit" style={{ marginTop: '15px', padding: '8px 15px', fontSize: '16px', backgroundColor: '#fd7e14', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
          에세이 제출
        </button>
      </form>
      <p style={{ marginTop: '15px' }}>
        현재 에세이:<br />
        <span style={{ whiteSpace: 'pre-wrap', color: '#dc3545' }}>{essay}</span>
      </p>
    </div>
  );
}

export default EssayForm;
  • <textarea>는 HTML에서는 자식 노드로 텍스트를 초기화했지만, 리액트에서는 value 속성으로 초기화합니다.
  • whiteSpace: 'pre-wrap' 스타일은 HTML의 <pre> 태그처럼 공백과 줄바꿈을 유지하도록 해줍니다.

select 태그 다루기

HTML의 <select> 태그는 드롭다운 목록을 만듭니다.

리액트에서 <select> 태그도 제어 컴포넌트로 다룰 수 있습니다.

HTML에서는 selected 속성을 사용했지만, 리액트에서는 <select> 태그의 value 속성에 선택된 값을 지정합니다.

예제: 과일 선택 폼
src/components/FlavorForm.jsx
import { useState } from 'react';

function FlavorForm() {
  // state의 초기값으로 기본 선택될 option의 value를 지정
  const [selectedFlavor, setSelectedFlavor] = useState('grapefruit');

  const handleChange = (event) => {
    setSelectedFlavor(event.target.value);
  };

  const handleSubmit = (event) => {
    alert('선택한 과일: ' + selectedFlavor);
    event.preventDefault();
  };

  return (
    <div style={{ border: '1px solid #20c997', padding: '20px', margin: '20px', borderRadius: '8px' }}>
      <h2>좋아하는 과일 선택</h2>
      <form onSubmit={handleSubmit}>
        <label>
          좋아하는 과일을 선택하세요:
          <select value={selectedFlavor} onChange={handleChange}
                  style={{ marginLeft: '10px', padding: '8px', fontSize: '16px', borderRadius: '4px', border: '1px solid #ccc' }}>
            <option value="grapefruit">자몽</option>
            <option value="lime">라임</option>
            <option value="coconut">코코넛</option>
            <option value="mango">망고</option>
          </select>
        </label>
        <button type="submit" style={{ marginLeft: '10px', padding: '8px 15px', fontSize: '16px', backgroundColor: '#20c997', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
          제출
        </button>
      </form>
      <p style={{ marginTop: '15px' }}>
        선택된 과일: <span style={{ fontWeight: 'bold', color: '#20c997' }}>{selectedFlavor}</span>
      </p>
    </div>
  );
}

export default FlavorForm;
  • <select> 태그 자체의 value 속성에 selectedFlavor state를 연결했습니다.
  • option 태그에는 value 속성만 부여하고 selected 속성은 사용하지 않습니다.

여러 입력 요소에 대한 단일 이벤트 핸들러

여러 개의 입력 필드가 있는 폼을 만들 때, 각 입력 필드마다 별도의 handleChange 함수를 만드는 것은 비효율적입니다.

하나의 이벤트 핸들러 함수로 여러 입력 필드를 처리할 수 있습니다.

이를 위해서는 event.target.name 속성을 활용합니다.

예제: 여러 입력 필드 폼
src/components/MultiInputForm.jsx
import { useState } from 'react';

function MultiInputForm() {
  const [formValues, setFormValues] = useState({
    username: '',
    email: '',
    password: ''
  });

  const handleChange = (event) => {
    // (1) event.target에서 name과 value를 비구조화 할당으로 추출
    const { name, value } = event.target;
    setFormValues({
      ...formValues, // (2) 기존 formValues 객체를 복사
      [name]: value // (3) 변경된 input의 name 속성에 해당하는 값만 업데이트
    });
  };

  const handleSubmit = (event) => {
    alert(`제출된 정보:\n사용자 이름: ${formValues.username}\n이메일: ${formValues.email}\n비밀번호: ${formValues.password}`);
    event.preventDefault();
  };

  return (
    <div style={{ border: '1px solid #6f42c1', padding: '20px', margin: '20px', borderRadius: '8px' }}>
      <h2>회원가입 폼</h2>
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '15px' }}>
          <label>
            사용자 이름:
            <input
              type="text"
              name="username"
              value={formValues.username}
              onChange={handleChange}
              style={{ marginLeft: '10px', padding: '8px', fontSize: '16px', borderRadius: '4px', border: '1px solid #ccc' }}
            />
          </label>
        </div>
        <div style={{ marginBottom: '15px' }}>
          <label>
            이메일:
            <input
              type="email"
              name="email"
              value={formValues.email}
              onChange={handleChange}
              style={{ marginLeft: '10px', padding: '8px', fontSize: '16px', borderRadius: '4px', border: '1px solid #ccc' }}
            />
          </label>
        </div>
        <div style={{ marginBottom: '15px' }}>
          <label>
            비밀번호:
            <input
              type="password"
              name="password"
              value={formValues.password}
              onChange={handleChange}
              style={{ marginLeft: '10px', padding: '8px', fontSize: '16px', borderRadius: '4px', border: '1px solid #ccc' }}
            />
          </label>
        </div>
        <button type="submit" style={{ padding: '8px 15px', fontSize: '16px', backgroundColor: '#6f42c1', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
          가입하기
        </button>
      </form>
      <p style={{ marginTop: '15px' }}>
        입력된 사용자 이름: {formValues.username}<br />
        입력된 이메일: {formValues.email}
      </p>
    </div>
  );
}

export default MultiInputForm;
  • formValues state: 여러 입력 필드의 값을 하나의 객체로 관리합니다.
  • handleChange: event.target.name을 이용하여 어떤 입력 필드의 값이 변경되었는지 식별하고, 해당 필드의 값만 formValues 객체에 업데이트합니다. ...formValues는 기존 state를 복사하는 중요한 문법입니다. (객체 state를 업데이트할 때는 불변성(Immutability)을 지켜야 합니다. 이 개념은 나중에 더 자세히 다루겠습니다.)

제어 컴포넌트에서는 onChange가 입력 요소의 현재 DOM 값을 읽고, setState로 다음 상태를 요청합니다. 다음 렌더에서 그 상태를 value로 전달하여 화면의 값과 React가 관리하는 값을 맞춥니다. setter 호출이 현재 함수에서 읽는 상태 변수를 즉시 바꾸는 것은 아닙니다.

여러 입력을 하나의 핸들러로 다룰 때는 name 값으로 어떤 필드를 갱신할지 분기하며, 이 흐름 덕분에 폼의 현재 값이 항상 React state 한곳에서 관리됩니다.

예시

사용자 이름에 지수를 입력한 뒤 이메일에 [email protected]을 입력한다고 합시다. 첫 변경은 name이 username이므로 해당 속성만 바꾸고, 두 번째 변경은 email 속성만 바꿉니다. 두 번째 갱신에서도 기존 객체를 펼쳐 복사하므로 사용자 이름 지수가 남습니다. 비밀번호 필드는 빈 문자열을 유지합니다.

이 예제의 가입하기 버튼은 상태 값을 경고창에 표시할 뿐입니다. 계정을 생성하거나 서버에 전송하지 않습니다. 입력을 수집하는 단계와 실제 회원가입 처리 단계를 구별합니다.

독립 연습: 입력 이름이 상태 속성과 다르면?

원래 이메일 입력에서 value={formValues.email}과 핸들러는 그대로 두고, name="email"만 name="mail"로 바꿨다고 가정합니다. [email protected]을 입력하면 객체의 어느 속성을 바꾸나요? 아래 이메일 표시가 갱신되지 않는 이유를 설명한 뒤 원래 이름을 복원하세요.

필드 흐름 확인

핸들러는 읽은 이름을 그대로 속성 이름으로 사용하므로 새 mail 속성을 만듭니다. 하지만 입력의 value와 아래 표시는 여전히 email 속성을 읽습니다. email은 빈 문자열이므로 다음 렌더에서 빈 값을 전달합니다. name은 자동으로 email 속성에 연결되는 특별한 기능이 아니라, 이 핸들러가 어떤 키를 쓸지 정한 값입니다.


비제어 컴포넌트 (선택적)

일반적으로 리액트에서는 제어 컴포넌트를 사용하여 폼을 다루는 것이 권장됩니다.

하지만 특정 상황(예: 파일 업로드, 외부 라이브러리 연동)에서는 폼 요소의 값을 직접 DOM에서 가져와야 할 때가 있습니다.

이 경우 비제어 컴포넌트 (Uncontrolled Component)를 사용하고, ref 속성을 통해 DOM 요소에 직접 접근합니다.

다음 예제는 파일을 선택한 뒤 제출하는 경우만 보여줍니다. 선택하지 않았을 때의 처리와 실제 업로드는 생략했습니다.

import { useRef } from 'react';

function FileInputForm() {
  const fileInputRef = useRef(null); // useRef 훅으로 ref 생성

  const handleSubmit = (event) => {
    event.preventDefault();
    // ref.current를 통해 실제 DOM 요소에 접근하여 값 가져오기
    alert(`선택된 파일: ${fileInputRef.current.files[0].name}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="file" ref={fileInputRef} /> {/* ref 속성 연결 */}
      <button type="submit">파일 제출</button>
    </form>
  );
}
// 실제 파일 업로드는 더 복잡한 서버 통신을 포함합니다.

직접 실행할 때는 위 컴포넌트를 FileInputForm.jsx에 저장하고 파일 끝에 export default FileInputForm;을 추가한 뒤 App.jsx에서 렌더합니다. 파일 하나를 선택하고 제출했을 때 경고창에 그 이름이 나오는지 확인합니다. 파일을 고르지 않은 상태에서는 배열의 첫 파일이 없으므로 원래 예제의 제출 조건을 충족하지 않습니다.

빈 선택도 처리하려면 아래 핸들러를 별도 변형으로 적용합니다. 파일 이름을 읽기 전에 파일 존재 여부를 검사하며 실제 업로드는 여전히 수행하지 않습니다.

const handleSubmit = event => {
  event.preventDefault();
  const file = fileInputRef.current?.files?.[0];
  if (!file) {
    alert('파일을 먼저 선택하세요.');
    return;
  }
  alert(`선택된 파일: ${file.name}`);
};

useRef 훅은 DOM 요소에 직접 접근할 때 사용됩니다.

하지만 이 방법은 폼 데이터 관점에서 보면 비제어 방식이므로, 가능한 한 제어 컴포넌트 방식을 사용하는 것이 리액트의 철학과 더 잘 맞습니다.


폼 다루기 기초는 여기까지입니다.

input, textarea, select 값을 React state와 연결하면 입력 중인 값, 검증, 제출 처리를 한 흐름에서 다룰 수 있습니다.

여러 입력 요소를 하나의 핸들러로 처리할 때도 결국 기준은 같습니다.

리액트 폼은 입력값을 상태와 연결해 사용자 입력을 제어합니다.

핵심은 value를 state에서 읽고, onChange에서 다음 값을 저장해 같은 기준으로 화면과 데이터를 맞추는 것입니다. React의 입력 설명에서 제어 입력의 값과 변경 핸들러를 함께 지정하는 이유를 확인할 수 있습니다.

다음 절에서는 useEffect로 컴포넌트의 표시와 해제 시점에 작업을 실행하는 방법을 다룹니다.