Controllered & Uncontrolled Component

Jinseong Park·2025년 6월 25일
post-thumbnail

React를 공부하면서 컴포넌트만 만들줄 알았지, 컴포넌트 패턴이 있는지 몰랐다. 그래서 이번 기회에 제어(controlled) 컴포넌트와 비제어(uncontrolled) 컴포넌트와 추가로 관련된 개념에 대해서 공부해봤다.

참고로 이 개념은 React 18버전 공식문서에 다루는 내용이다.

제어??

제어라는 말을 들으니 컴포넌트에 어딘가에 의존되어있다는 생각이 먼저 들었다. 상위 컴포넌트가 하위 컴포넌트에 영향을 준다는 것을 단어에서 언뜻 느낄 수 있다.

신뢰할 수 있는 단일 출처(A single source of truth)

제어 & 비제어 컴포넌트를 설명하기에 앞서, 이 개념부터 설명해야 한다. 리액트에서는 컴포넌트마다 정말 다양한 상태를 가진다. 그래서 이 각각의 고유한 상태를 어떤 컴포넌트가 “소유”할지 정할 수 있다. 이 원칙은 “신뢰할 수 있는 단일 출처”를 가진다고도 말할 수 있다.

신뢰할 수 있는 단일 출처(A single source of truth)
정보시스템 설계 및 이론에서, 관련된 모든 데이터 요소를 한 곳에서만 제어 또는 편집하도록 조직하는 관례를 말한다. 이는 데이터 복제본이 존재할 때 원본 데이터가 수정된다면, 복제본과 이를 조회하는 모든 작업에서 잘못된 이전 데이터를 조회하는 위험성이 생긴다. 그렇기 때문에 항상 원본 데이터를 조회하게 설계해야 한다. (출처: Wikipedia 단일 진실 공급원

이를 통해 컴포넌트마다 중복된 상태를 가지게 하는 것이 아니라, 공통 부모로 상태를 끌어올리고, 이를 필요한 자식에게 전달하게 설계할 수 있다.

Form Element 먼저 이해하기

form element는 자체적으로 내부 상태(입력 값, 대표적으로 input value)를 가지기 때문에 React의 다른 DOM element와 다르게 동작한다. 사용자의 입력을 받는 방식은 input, textarea, select(여기서는 해당 태그의 속성까지는 다루지 않겠다)가 있다. 그리고 이 태그로 입력받는 값을 React에 의해 값을 제어하는 방법기존처럼 DOM에 직접 접근해서 값을 제어하는 방식이 존재한다.

제어 컴포넌트(Controlled Component)

제어 컴포넌트는 리액트의 상태(state)를 통해 입력 값을 제어하는 컴포넌트를 말한다. 그래서 입력값(value)을 리액트 상태와 동기화하고, 사용자가 입력할 때마다 onChange 이벤트 핸들러를 통해 상태를 업데이트한다.

그래서, 제어 컴포넌트는 입력 값을 상태로 관리하기 때문에, 입력 할 때마다 값을 검증하거나, 값을 자유롭게 변경할 수 있으며, 복잡한 폼 로직을 처리하는 데 유용하다.

단, 입력 값을 상태로 관리하기 때문에 입력 값이 변경 될 때마다 리렌더링이 발생한다.

아래에 코드를 보며 이해해보자.

// 제어 컴포넌트 예시 코드
function ControlledComponent() {
  const [value, setValue] = useState("");

  return <input type="text" value={value} onChange={(e)=>setValue(e.target.value)} />;
}

이 코드는 보다시피 input 태그의 value 속성을 상태로 관리하고 있다.

여기서 주의할 점은 onChange 이벤트 핸들러로 input 태그의 이벤트를 감지해 사용자가 입력한 값으로 상태를 갱신해줘야 한다는 것이다. 이렇게 상태를 갱신하지 않으면 입력 값을 상태에 전달하지 못해 화면에서도 보이지 않고, 실제 상태도 빈 상태가 된다.

비제어 컴포넌트(Uncontrolled Component)

비제어 컴포넌트는 입력 값을 리액트의 상태로 관리하지 않고, DOM을 통해 입력 값을 제어하는 방식이다. 그래서 입력 값은 DOM에서 직접 관리되고, 리액트는 이에 관여하지 않는다. 그래서 useRef 훅을 사용해 참조 객체인 ref를 사용해 입력 태그의 DOM 요소에 직접 접근해서 값을 읽거나 조작한다.

그래서, 비제어 컴포넌트는 리액트 상태 관리에 따른 성능 비용(리렌더링)이 없다.

하지만 입력 값을 동적으로 할당하는 등의 조작을 불가능해서 보통 간단한 폼에서 사용한다.

예시로 아래 코드를 보자.

// 비제어 컴포넌트 예시 코드
function UncontrolledComponent() {
  const ref = useRef('');

  const handleSubmit = (e) => {
    e.preventDefault(); // 해당 이벤트에 대해 기본적으로 구현된 동작을 정지시킴.

    console.log(ret.current.value); // 출력: (사용자 입력값)
  }

  return (
    <form onSubmit={() => {}}>
      <input type='text' ref={ref} />
    </form>
  );
}

위 코드에서는 input 태그의 DOM 요소를 ref 객체로 받는다. ref.current는 DOM 요소를 가리키게 되고, 최종적으로 value 값에 접근할 수 있다.

여기서 입력 값은 React 상태로 제어되지 않고 DOM 요소에 의해 알아서 변경된다.

유효성 검사

비제어 컴포넌트의 경우 submit 이벤트 발생 시 유효성 검사를 실행한다. 그래서 유저가 즉각적인 입력에 대한 피드백을 받을 수 없다. 위의 비제어 컴포넌트 예시 코드를 보면 ref는 값의 변경이 일어나도 리렌더링이 일어나지 않기 때문에 어떤 피드백을 화면에 띄워 유저에게 알려줄 수 없다.

하지만, 제어 컴포넌트의 경우 입력 값이 바뀔 때마다 유효성 검사를 실시해 유저에게 즉각적인 피드백을 줄수 있다. 입력 값에 따라 상태가 변경되어 리렌더링이 발생하기 때문에 이 상태를 가지고 유효성 검사를 실시한 결과를 화면에 바로 띄울 수 있다.

React 19버전에서의 제어 컴포넌트와 비제어 컴포넌트

React 19버전 공식문서에 따르면, 지역 상태만으로 중요한 로직을 처리하는 컴포넌트를 “비제어 컴포넌트”라고 부른다. 말 그대로 비제어 컴포넌트는 부모 컴포넌트에서 “제어할 수 없는” 컴포넌트를 가리킨다.

지역 상태(Local State)
props를 통해 만들어지는 상태가 아닌 컴포넌트 자체에서 생성되는 상태를 일컫는다.

반대로, “제어 컴포넌트”는 부모 컴포넌트에서 “제어할 수 있는” 컴포넌트다. 대표적인 예로 props를 통해 상태를 전달해 컴포넌트의 중요한 상태에 영향을 주는 경우가 있다.

설명에서 느껴지는 바와 같이, React 19버전 공식문서에서는 제어 & 비제어 컴포넌트가 엄격한 기술 용어가 아니다. 하지만 이런 구분을 통해 컴포넌트의 설계를 명확히하고 설명하는 데 유용한 방법이라고 소개되어 있다.

이는 18버전에서 form element에만 적용되는 제어 & 비제어 개념을 전체 컴포넌트로 확장한 것 같다.

마무리

오늘은 입력을 처리하는 컴포넌트 패턴을 이해하고 18버전에서와 19버전에서 어떻게 다른지도 짚어봤다. 설명에서 언급하지 않았지만 React는 제어 컴포넌트로 입력을 처리하는 것을 적극 권장한다. 다만, 상태로 굳이 관리하고 싶지 않거나 적은 코드를 유지하고 싶은 등 특수한 목적을 위해 비제어 컴포넌트를 사용하는 방법도 명시해놓았다.

개인적으로 이 개념을 공부하면서, 컴포넌트에만 맞춰 상태를 설계해야한다는 강박을 없애고, 설계된 컴포넌트에서 상태에 맞춰 단일 상태를 설정하고 필요하면 컴포넌트를 추가한다는 시각을 가지게 되었다.

오늘도 느낀거지만 글로 정리하기 전과 후의 이해도는 하늘과 땅 차이다. 매번 미뤄왔는데 귀찮더라도 꼭 시간을 내서 정리해야겠다.

[참고]

React 18 Docs - Forms

React 18 Docs - Uncontrolled-Component

[10분 테코톡] 세인의 제어 컴포넌트와 비제어 컴포넌트

React 19 Docs - Controlled and uncontrolled components

profile
헤맨 만큼 내 땅이다

0개의 댓글