Controlled Component vs Uncontrolled Component

Controlled Component(제어 컴포넌트)

HTML에서 form 요소들은 각자의 state를 가지고있고, 사용자의 입력에 의해 그것을 업데이트하는 방식이다.
리액트에서는 가변적인 상태들을 컴포넌트의 state가 관리하고 그것들은 setState()메소드에 의해서만 업데이트된다.(single source of truth)
폼 데이터를 다룰 때도 이처럼 폼에 발생하는 사용자의 입력값을 state와 이벤트 핸들러를 이용해 제어할 수 있다. 이러한 방식으로 React에 의해 값이 제어되는 폼 엘리먼트제어 컴포넌트라고 한다.

즉 제어 컴포넌트는 form 요소를 렌더링하는 컴포넌트 중에서 form 요소의 입력값을 DOM이 아닌 리액트 컴포넌트가 제어하는 컴포넌트를 의미한다. 제어 컴포넌트는 기본적인 HTML form 요소의 동작을 재정의한다.

controlled component image

다음 코드는 제어 컴포넌트로 구현된 폼 요소의 예시 코드이다.

const { useState } from 'react';

function Controlled () {
  const [email, setEmail] = useState();

  const handleInput = (e) => setEmail(e.target.value);

  return <input id="email" type="text" value={email} onChange={handleInput} />;
}


  1. email input이 가진 value속성이 email이라는 상태와 연결되어있다. 이는 컴포넌트의 state와 input의 입력값이 항상 동기화된다는 것을 뜻한다.
  2. email input에서 change event가 발생하면 handleInput콜백함수가 트리거되고 이 콜백함수 안에서는 입력값이 업데이트될 때마다 email state를 업데이트한다.

email input 요소의 입력값이 바뀔 때마다 onChange이벤트가 발생하고, 바뀐 값들은 useState()함수에 의해 업데이트되어 사용자에게 보여진다. 즉, 사용자의 입력값이 바뀔 때마다 리렌더링이 일어나게 된다.

Uncontrolled Component(비제어 컴포넌트)

비제어컴포넌트를 이용한 방식은 기존 HTML form 요소의 기본적인 동작과 유사한 방식으로 입력값을 실시간으로 업데이트하지 않는다. 대신 사용자의 입력 값을 Refs를 이용하여 접근하여 트리거될 때만 업데이트가 일어난다.

controlled component image

다음 코드는 비제어 컴포넌트로 구현된 폼 요소의 예시 코드이다.

const { useState } from 'react';

function Controlled () {
  const emailRef = useRef();

  return <input id="email" type="text" ref={emailRef} />;
}

emailRef를 만들어 email input요소에 접근했다. 한 눈에 봐도 로직이 간결해졌음을 알 수 있다. 이 방식에서는 form을 제출할 때 emailRef.current.value로 해당 input 요소의 입력값에 접근할 수 있을 것이다.

입력값이 변경될 때마다 state를 업데이트하거나 유효성 검사 규칙을 처리해 사용자가 입력한 값과 state를 동기화해야할 필요가 없는 경우 비제어 컴포넌트를 활용하면 코드를 많이 줄일 수 있을 것이다.

두 방식의 차이점

  • 제어컴포넌트에서 form 데이터는 React component에 의해 제어된다. 비제어컴포넌트는 DOM 요소 자체에서 그 데이터를 제어한다.
  • 제어컴포넌트 사용자의 입력값이 실시간으로 동기화된다. 그러나 비제어컴포넌트에서는 직접 트리거하지 않는 이상 리렌더링이 발생하지도 않고 실시간으로 값이 동기화되지 않는다.
왜 React에는 이 두 가지 방식이 모두 존재하는걸까?


다른 MVC 패턴의 프레임워크들과 달리, React는 “View Library”에 좀 더 치우친 라이브러리이다. 그래서 리액트는 Model-View 방식으로 접근하는 제어 컴포넌트와 View만을 이용한 방식으로 접근하는 비제어 컴포넌트를 모두를 유연하게 제공하는 것이다.
상황에 맞게 적절한 방식을 선택하여 사용하면 될 것 같다는 생각이 드는데, 다만 React의 공식적인 입장은 제어 컴포넌트를 더 권장하고있다.

언제 제어 컴포넌트를 사용해야 할까

  • 입력값에 대한 유효성 검사를 처리해야하는 경우
  • 사용자의 입력을 제한하는 경우(ex. 특정 문자열의 입력을 제한, 특정 길이 이내로 입력을 제한)
  • 사용자가 입력한 값을 즉시 포매팅해야하는 경우(ex. 전화번호, 신용카드번호 입력, 숫자에 단위 표시)

위와 같은 상황에서는 제어 컴포넌트를 사용해야 한다. 그래서 나도 제어 컴포넌트를 사용해 회원가입 폼을 구현해보았는데, 다음과 같은 문제점이 발생했다.

  1. 로직이 너무 길고 복잡하다. 제어컴포넌트는 데이터가 변경될 수 있는 모든 방법에 대해 이벤트 핸들러를 작성하고 useState()를 통해 모든 입력 값의 state를 form 요소와 연결해야하기 때문에 로직이 길어진다. 게다가 내가 구현해야했던 회원가입 폼 같은 경우 여러 개의 유효성 검사 규칙이 있었고 그 각각에 대한 처리 결과들도 state로 관리해야했다.
  2. 제어컴포넌트의 경우 회원가입 폼 내에 있는 모든 input 요소의 값이 변할 때마다 리렌더링이 발생하여 비효율적인 리렌더링이 일어난다.(이를 해결하기 위해 debouce나 throttle 등의 메소드를 사용하였지만 이 역시 코드를 복잡하게 만드는 요인이 되었다.)

하지만 React Hook Form이라는 라이브러리를 사용하면 비제어 컴포넌트를 이용해 다양한 경우에서의 유효성 검사를 처리할 수 있고, 명시적이고 간결한 로직 구현이 가능하다. 또한 제어 컴포넌트가 필요한 경우 특정 인풋 요소에 대해서는 제어컴포넌트로 구현할 수 있다.

그래서 나는 기존 제어 컴포넌트를 이용해 구현했던 회원가입 폼에 React Hook Form을 도입하였다. React Hook Form에 대한 설명과 이 라이브러리를 도입한 과정은 다음 포스팅에서 다루겠다.

Resources