Logo

React로 비제어 양식 UI를 만드는 방법 (Uncontrolled Components)

이번 포스팅에서는 Uncontrolled Components 방식을 활용하여 React로 비제어 양식 UI를 만드는 몇가지 방법에 대해서 알아보겠습니다.

Uncontrolled Components란?

웹에서 양식 UI를 구현할 때 <input>, <select>, <textarea>와 같은 HTML 요소를 사용하게 되는데요. 이러한 요소들은 valuechecked와 같은 내부 상태를 가지는데 기본적으로는 브라우저의 DOM이 상태를 제어해줍니다.

React를 사용해서 양식 관련 HTML 요소들이 포함된 컴포넌트를 작성할 때는 이 상태 제어를 React에게 맡기는 경우가 많은데요. 이렇게 하면 브라우저를 통해서는 어려운 좀 더 섬세한 상태 제어가 가능하고 입력값 검증이 용이해지기 때문입니다.

하지만 그렇다고 해서 반드시 React를 통해서 양식 관련 HTML 요소의 상태 관리를 해야하는 것은 아니며, React 외에도 다른 자바스크립트 라이브러리가 혼용되서 사용되는 프로젝트에서는 불가능한 상황도 생길 수 있습니다.

React에서는 React가 직접 상태 제어를 하는 컴포넌트를 Controlled Components라고 부르며, 브라우저가 상태 제어를 하도록 자연스럽게 두는 컴포넌트를 Uncontrolled Components라고 부릅니다.

DOM API

Uncontrolled Components 방식으로 양식을 개발하는 첫번째 방법은 마치 React 없이 개발하듯이 순수하게 자바스크립트의 DOM API를 사용하는 것인데요.

바로 제출(submit) 이벤트의 target.elements 속성을 통해서 양식 내부에 있는 HTML 요소의 valuechecked 속성값을 쉽게 읽어올 수 있습니다.

한 가지 조심할 점은 각 HTML 요소의 초기 상태를 지정해줄 때 HTML 처럼 valuechecked 속성을 사용하면 값이 고정되어 사용자가 변경할 수 없게 된다는 것입니다. 대신에 React의 defaultValuedefaultChecked prop을 사용해야 하며 React에서 valuechecked 속성은 Controlled Components를 구현할 때만 사용이 가능하다는 점 주의하세요.

function Form() {
  const handleSubmit = (event) => {
    event.preventDefault();
    const {
      title: { value: input },
      country: { value: select },
      description: { value: textArea },
      size: { value: radio },
      terms: { checked: checkbox },
    } = event.target;
    alert(
      JSON.stringify({
        input,
        select,
        textArea,
        radio,
        checkbox,
      })
    );
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        제목
        <input name="title" defaultValue="" />
      </label>

      <label>
        국가
        <select name="country" defaultValue="한국">
          <option>한국</option>
          <option>미국</option>
          <option>중국</option>
          <option>영국</option>
          <option>태국</option>
        </select>
      </label>

      <label>
        내용
        <textarea name="description" defaultValue="안녕하세요?" />
      </label>

      <fieldset>
        <legend>크기</legend>
        <label>
          <input type="radio" name="size" value="" /></label>
        <label>
          <input type="radio" name="size" value="" defaultChecked /></label>
        <label>
          <input type="radio" name="size" value="" /></label>
      </fieldset>

      <label>
        <input type="checkbox" name="terms" />
        약관에 동의합니다.
      </label>

      <button type="submit">제출</button>
    </form>
  );
}

export default Form;

useState() 후크

DOM API를 사용하지 않고 좀 더 React 답게 useState() 후크(hook) 함수를 사용해서도 Uncontrolled Components을 구현할 수 있는데요.

useState 후크 함수에 대한 좀 더 자세한 내용은 관련 포스트를 참고 바랍니다.

우선 useState() 후크(hook) 함수를 호출하여 양식 관련 HTML 요소를 저장하기 위한 상태 변수와 상태를 변경하기 위한 함수를 생성합니다. 그 다음, 양식 내부에 있는 모든 HTML 요소에 ref prop을 통해 콜백 함수를 설정해주는데요. 이 콜백 함수의 인자로 넘어온 각 HTML 요소를 상태 변경 함수를 호출하여 각 상태 변수에 저장해주는 것입니다.

import { useState } from "react";

function Form() {
  const [input, setInput] = useState(null);
  const [select, setSelect] = useState(null);
  const [textArea, setTextArea] = useState(null);
  const [radio0, setRadio0] = useState(null);
  const [radio1, setRadio1] = useState(null);
  const [radio2, setRadio2] = useState(null);
  const [checkbox, setCheckbox] = useState(null);

  const handleSubmit = (event) => {
    event.preventDefault();
    alert(
      JSON.stringify({
        input: input?.value,
        select: select?.value,
        radio: [radio0, radio1, radio2].find((radio) => radio?.checked)?.value,
        textArea: textArea?.value,
        checkbox: checkbox?.checked,
      })
    );
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        제목
        <input
          name="title"
          defaultValue=""
          ref={(element) => setInput(element)}
        />
      </label>

      <label>
        국가
        <select
          name="country"
          defaultValue="한국"
          ref={(element) => setSelect(element)}
        >
          <option>한국</option>
          <option>미국</option>
          <option>중국</option>
          <option>영국</option>
          <option>태국</option>
        </select>
      </label>

      <label>
        내용
        <textarea
          name="description"
          defaultValue="안녕하세요?"
          ref={(element) => setTextArea(element)}
        />
      </label>

      <fieldset>
        <legend>크기</legend>
        <label>
          <input
            type="radio"
            name="size"
            value=""
            ref={(element) => setRadio0(element)}
          /></label>
        <label>
          <input
            type="radio"
            name="size"
            value=""
            defaultChecked
            ref={(element) => setRadio1(element)}
          /></label>
        <label>
          <input
            type="radio"
            name="size"
            value=""
            ref={(element) => setRadio2(element)}
          /></label>
      </fieldset>

      <label>
        <input
          type="checkbox"
          name="terms"
          ref={(element) => setCheckbox(element)}
        />
        약관에 동의합니다.
      </label>

      <button type="submit">제출</button>
    </form>
  );
}

useRef() 후크

마지막으로 살펴볼 방법은 useState() 대신에 HTML 요소를 저장하는데 좀 더 최적화 된 useRef() 후크(hook) 함수를 사용하는 것인데요.

useRef 후크 함수에 대한 좀 더 자세한 내용은 관련 포스트를 참고 바랍니다.

useRef() 후크를 사용하면 굳이 상태 저장 변수와 상태 변경 함수가 짝으로 필요가 없으면 간편하게 하나의 변수만 ref prop에 넘겨줄 수 있습니다.

import { useRef } from "react";

function Form() {
  const inputRef = useRef(null);
  const selectRef = useRef(null);
  const textAreaRef = useRef(null);
  const radioRefs = [useRef(null), useRef(null), useRef(null)];
  const checkboxRef = useRef(null);

  const handleSubmit = (event) => {
    event.preventDefault();
    alert(
      JSON.stringify({
        input: inputRef.current?.value,
        select: selectRef.current?.value,
        textarea: textAreaRef.current?.value,
        radio: radioRefs
          .map(({ current }) => current)
          .find((current) => current?.checked)?.value,
        chekbox: checkboxRef.current?.checked,
      })
    );
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        제목
        <input name="title" defaultValue="" ref={inputRef} />
      </label>

      <label>
        국가
        <select name="country" defaultValue="한국" ref={selectRef}>
          <option>한국</option>
          <option>미국</option>
          <option>중국</option>
          <option>영국</option>
          <option>태국</option>
        </select>
      </label>

      <label>
        내용
        <textarea
          name="description"
          defaultValue="안녕하세요?"
          ref={textAreaRef}
        />
      </label>

      <fieldset>
        <legend>크기</legend>
        <label>
          <input type="radio" name="size" value="" ref={radioRefs[0]} /></label>
        <label>
          <input
            type="radio"
            name="size"
            value=""
            defaultChecked
            ref={radioRefs[1]}
          /></label>
        <label>
          <input type="radio" name="size" value="" ref={radioRefs[2]} /></label>
      </fieldset>

      <label>
        <input type="checkbox" name="terms" ref={checkboxRef} />
        약관에 동의합니다.
      </label>

      <button type="submit">제출</button>
    </form>
  );
}

마치면서

지금까지 React로 비제어 양식 UI를 구현할 때 자주 사용되는 3가지 패턴에 대해서 살펴보았습니다. 대부분의 프로젝트가 함수형 컴포넌트로 넘어가고 있기 때문에 굳이 클래스 기반 컴포넌트에 대해서는 다루지 않았습니다. Uncontrolled Components에 대한 React의 공식 문서를 보시면 클래스 기반 컴포넌트 기준으로 어떻게 비제어 양식을 만드는지에 대해서 나와있으니 참고하시면 좋을 것 같습니다.