[React] 제어 컴포넌트 vs 비제어 컴포넌트


React에서 사용자 입력을 처리하는 방식은 제어 컴포넌트와 비제어 컴포넌트로 나뉩니다.
제어 컴포넌트는 입력 값을 상태로 관리해 실시간 피드백을 제공하고, 비제어 컴포넌트는 참조(ref)를 통해 값을 얻는 방식입니다.
이 글에서는 두 방식의 차이점과 장단점을 알아보고, 어떤 상황에서 적합한지에 대해 알아보려고 합니다.

제어 컴포넌트

제어 컴포넌트는 React의 상태(state)와 직접적으로 연결되어 있어, 입력 필드의 값이 항상 최신 상태로 유지됩니다.

<제어 컴포넌트 예시>
  • 상태와 동기화
    • 입력 필드의 값은 컴포넌트의 상태에 저장되며, 사용자가 입력할 때마다 setState()를 통해 상태가 업데이트됩니다.
    • 이로 인해 데이터와 UI가 항상 동기화되며, 입력된 값이 실시간으로 반영됩니다.
  • 즉각적인 상태 업데이트
    • 사용자가 새로운 값을 입력할 때마다 상태가 즉시 갱신되며, 이로 인해 컴포넌트가 리렌더링됩니다.
    • 예를 들어, 사용자가 “안녕하세요”를 입력할 때, 안녕하세요라는 최종 값뿐만 아니라, 중간 중간의 안녕, 안녕하, 안녕하세요와 같은 중간 상태들이 모두 React 상태로 기록됩니다.
  • 단방향 데이터 흐름 - 제어 컴포넌트에서는 데이터가 항상 React 상태에서 UI로 흘러가는 단방향 데이터 흐름이 유지됩니다. 이로 인해 데이터의 흐름이 예측 가능하고 관리하기 쉽습니다.

주로 사용하는 경우
  • 데이터의 검증이나 변환이 필요한 경우
  • 여러 입력 필드 간의 상태 동기화가 필요한 경우
  • 입력 값을 기반으로 다른 UI 요소를 동적으로 업데이트 해야 하는 경우


비제어 컴포넌트

비제어 컴포넌트는 참조(ref)를 통해 DOM 요소에 직접 접근하여 값을 얻는 방식으로, React의 상태와는 별개로 동작합니다.

<비제어 컴포넌트 예시>
  • 상태와의 독립성
    • 입력 필드의 값이 React 상태에 저장되지 않으며, 입력이 발생해도 상태가 즉시 갱신되지 않습니다.
    • 사용자가 값을 제출하거나 특정 이벤트가 발생했을 때 참조를 통해 값을 가져옵니다.
  • 값의 비동기적 접근 - 데이터와 UI가 실시간으로 동기화되지 않기 때문에, 필드의 값을 얻기 위해서는 특정 이벤트(ex: 폼 제출)가 트리거되어야 합니다. - 예를 들어, 사용자가 텍스트를 입력한 후 전송 버튼을 클릭했을 때만 그 값을 콘솔에 출력할 수 있습니다.


제어 컴포넌트와 비제어 컴포넌트의 장점과 단점

제어 컴포넌트 (Controlled Components)

  • 장점:
    • 예측 가능성: 상태와 UI가 항상 동기화되어, UI가 항상 최신 데이터를 반영합니다.
    • 유연성: 입력 값을 기반으로 즉시 다른 UI 요소를 업데이트하거나, 데이터 검증 및 변환이 가능합니다.
  • 단점:
    • 불필요한 리렌더링: 입력 시마다 리렌더링이 발생할 수 있으며, 이로 인해 성능이 저하될 가능성이 있습니다.
    • 복잡성: 작은 입력 필드라도 상태 관리를 위해 코드가 복잡해질 수 있습니다.

비제어 컴포넌트 (Uncontrolled Components)

  • 장점:
    • 간단한 구조: 입력 값이 상태로 관리되지 않기 때문에 코드가 간단해질 수 있습니다.
    • 성능 효율성: 입력 시 리렌더링이 발생하지 않아 성능 상의 이점이 있을 수 있습니다.
  • 단점:
    • 실시간 피드백 부족: 입력 값이 실시간으로 반영되지 않으므로, 사용자가 즉각적인 피드백을 받지 못합니다.
    • 상태 관리 어려움: 복잡한 폼 로직이나 입력 값을 기반으로 하는 동적 UI 처리가 어렵습니다.
요약
기능 제어 컴포넌트 비제어 컴포넌트
값 동기화 항상 최신 상태로 유지 실시간 동기화되지 않음
상태 관리 React 상태에 저장 참조(ref)를 통해 값 접근
즉각적인 리렌더링 입력 시마다 리렌더링 발생 리렌더링 없음
코드 복잡성 상태 관리로 인해 복잡 간단한 코드 구조
사용자 피드백 실시간 피드백 제공 이벤트 후 피드백 제공
성능 입력 시 성능 저하 가능성 높은 성능 유지 가능성

위 표를 살펴봤을 때 결론은 아래와 같습니다.

제어 컴포넌트: 복잡한 상태 관리나 실시간 피드백이 필요한 경우에 적합합니다.
비제어 컴포넌트: 간단한 폼 처리나 불필요한 리렌더링을 피하고자 할 때 적합합니다.

주로 사용하는 것?

대부분 경우에 폼을 구현하는데 제어 컴포넌트를 사용하는 것이 좋습니다.
제어 컴포넌트에서 폼 데이터는 React 컴포넌트에서 다루어집니다. 대안인 비제어 컴포넌트는 DOM 자체에서 폼 데이터가 다루어집니다.

  • 더 나은 제어와 유연성: 모든 입력 상태를 컴포넌트 상태로 관리하므로, 입력 값에 대한 더 많은 제어와 유연성을 제공합니다.
  • 예측 가능성: 상태와 UI가 항상 동기화 되어, 예측 가능한 동작을 보장합니다.
  • 검증과 변환: 입력 값을 쉽게 검증하고 변환할 수 있어, 복잡한 폼 로직을 구현하기 용이합니다.

하지만, 항상 제어 컴포넌트가 좋은 것이라고 할 수는 없기 때문에 상황에 따라 적절한 방식을 선택하는 것이 좋습니다.

그렇다면 제어 컴포넌트는 언제 사용하는 걸까?

  • 제어 컴포넌트를 사용하기 좋은 방식 3가지
    • 유효성 검사
    • 유효한 데이터가 없는 경우 전송 버튼의 상태를 disabled로 표시
    • 신용카드와 같은 특정 입력 방식을 적용할 때


제어 컴포넌트를 사용할 때 문제점

제어 컴포넌트는 UI의 입력된 데이터 상태와 저장된 데이터의 상태가 항상 일치합니다다.
사용자가 입력하는 모든 데이터가 동기화 된다는 것과 같습니다.
예를 들어 input태그 안에 블로그 주인장의 이름인 “이도건”이라는 값을 입력하면

ㅇ;
이;
읻;
이도;
이독;
이도거;
이도건;

위와 같은 방식으로 입력하는 모든 순간이 동기화 되기 때문에 불필요한 리렌더링, api 요청으로 인한 자원 낭비로 연결되기도 합니다.

이러한 문제를 막기 위해서 throttle & debounce를 사용해 불필요한 리렌더링, api 요청을 줄여 불필요한 리렌더링을 줄일 수 있습니다.

throttle

이벤트를 일정 주기마다 처리함으로써 이벤트를 제어하는 방식입니다.
단순히 사용자의 움직임이 멈추었을 때만 동작하는 것이 아니라, 마지막 함수가 호출된 후 일정 시간 이내에 재호출되지 않음으로써, 처리량은 조절되면서 사용자가 서비스의 연속성도 느낄 수 있습니다.

<throttle 적용 예시>

위 예시 이미지는 lodash 라이브러리를 이용하여 throttle을 구현한 예시 이미지입니다.
이미지에서 보면 값을 입력하는 중간중간 타이핑하는 글씨가 입력 값에 업데이트 되는 것을 볼 수 있습니다.
그 이유는 throttle의 지연 시간을 1초로 설정하여 1초마다 한 번씩 값이 업데이트 되기 때문입니다.

쓰로틀 방식은 자동완성이나 무한스크롤링 기능에 적용할 수 있습니다.
계속해서 타이핑하거나 스크롤링하더라도 일정 주기마다 이벤트 콜백이 발생하기 때문에 사용자는 해당 기능이 계속해서 동작하고 있다고 느끼게 됩니다.

debounce

연속적으로 발생하는 이벤트들을 그룹화하여, 해당 이벤트 그룹이 일시정지가 되었다고 판단되는 시점이 지나면 최초 또는 최후의 이벤트에 대해서만 처리하는 방식입니다.

<debounce 적용 예시>

위 예시 이미지는 lodash 라이브러리를 이용하여 debounce을 구현한 예시 이미지입니다.
이미지에서 보면 값을 입력하난 이후 타이핑한 글씨가 입력 값에 업데이트 되는 것을 볼 수 있습니다.
그 이유는 debounce의 지연 시간을 1초로 설정하여 마지막 키보드 입력 이후 1초동안 추가 이벤트가 발생하지 않으면 입력 값이 업데이트 되기 때문입니다.

디바운스 방식은 컴포넌트 리사이징 이벤트와 같이 마지막 액션에 대한 처리가 중요한 경우에 활용할 수 있습니다.
자동완성이나 무한스크롤링 같이 계속해서 타이핑하거나 스크롤링하는 경우 이벤트 콜백이 발생하지 않아 사용자가 동작하지 않는다고 느낄 수 있기 때문에 부적합할 수 있습니다.