2025-09-23 20:30

  • 리액트 비제어 컴포넌트는 DOM 자체가 폼 데이터의 상태를 관리하도록 하는 방식이다.

  • 상태 관리를 리액트에게 맡기지 않고 useRef를 통해 필요할 때만 DOM에서 직접 값을 가져온다.

  • 불필요한 리렌더링을 방지하여 성능을 최적화하고, 간단한 폼이나 외부 라이브러리 연동 시 유용하다.


리액트 비제어 컴포넌트 완벽 핸드북 DOM에게 자유를

리액트 컴포넌트

리액트(React)의 세계에서 폼(Form)을 다루는 방식은 크게 두 가지로 나뉜다. 바로 **제어 컴포넌트(Controlled Component)**와 **비제어 컴포넌트(Uncontrolled Component)**다. 대부분의 리액트 개발자는 상태(State)로 모든 것을 통제하는 제어 컴포넌트 방식에 익숙하다. 하지만 때로는 DOM에게 상태 관리의 자율성을 부여하는 비제어 컴포넌트가 더 효율적이고 현명한 선택일 수 있다.

이 핸드북은 비제어 컴포넌트의 개념이 왜 등장했는지부터 시작하여 그 구조와 사용법, 그리고 언제 사용해야 가장 큰 효과를 볼 수 있는지에 대한 심도 있는 통찰을 제공한다. 단순한 문법 나열을 넘어, 비제어 컴포넌트의 철학과 그 잠재력을 완전히 이해하는 것을 목표로 한다.

만들어진 이유 DOM의 원래 방식 존중하기

리액트가 탄생하기 전, 웹 개발에서 폼 데이터는 전통적으로 DOM 자체가 관리했다. 사용자가 <input> 필드에 텍스트를 입력하면, 해당 DOM 노드가 그 값을 내부적으로 유지하고 있었다. 개발자는 필요할 때 자바스크립트로 DOM에 접근하여 값을 읽어오는 방식에 익숙했다.

리액트는 ‘상태가 UI를 결정한다’는 선언적 패러다임을 도입하며 제어 컴포넌트라는 개념을 제시했다. 이는 모든 입력 값의 변화를 리액트의 state로 관리하고, onChange 이벤트가 발생할 때마다 setState를 호출하여 컴포넌트를 리렌더링하는 방식이다. 이로 인해 데이터의 흐름이 단방향으로 명확해지고, 상태와 UI가 항상 동기화되는 장점을 얻었다.

하지만 이 방식에는 몇 가지 트레이드오프가 존재한다.

  1. 과도한 리렌더링: 사용자가 키를 한 번 누를 때마다 state가 변경되고, 이는 곧 컴포넌트의 리렌더링으로 이어진다. 간단한 폼에서는 문제가 없지만, 복잡하고 큰 폼에서는 성능 저하의 원인이 될 수 있다.

  2. 보일러플레이트 코드 증가: 각 입력 필드마다 state 변수와 onChange 핸들러를 만들어 연결해야 하므로 코드의 양이 늘어난다.

  3. 외부 라이브러리 연동의 어려움: JQuery 플러그인처럼 DOM을 직접 조작하는 외부 라이브러리와 함께 사용할 때, 리액트의 제어 방식과 충돌을 일으킬 수 있다.

비제어 컴포넌트는 이러한 배경 속에서 “전통적인 HTML 폼의 방식을 리액트 생태계 안에서 다시 활용하자”는 아이디어에서 출발했다. 상태 관리를 리액트가 아닌, 원래 주인이었던 DOM에게 맡기는 것이다. 리액트는 상태 변화를 일일이 감시하는 대신, 최종적으로 값이 필요할 때만 DOM에 직접 물어보는 역할을 한다.

이를 비유하자면, 제어 컴포넌트는 모든 메뉴 주문을 중앙 카운터에서 직원이 일일이 받아 처리하는 방식이고, 비제어 컴포넌트는 고객이 키오스크에서 직접 주문하고 결제까지 마친 뒤, 결과(영수증)만 카운터에 전달하는 방식과 같다. 키오스크 방식은 중앙 카운터의 부담을 줄여 전체적인 효율을 높일 수 있다.


구조 DOM에 직접 연결하는 통로 useRef

비제어 컴포넌트의 핵심은 **ref**다. ref는 ‘reference(참조)‘의 줄임말로, 리액트에서 DOM 노드나 리액트 엘리먼트에 직접 접근할 수 있는 통로를 제공한다. 함수형 컴포넌트에서는 주로 useRef 훅을 사용해 ref 객체를 생성한다.

비제어 컴포넌트는 다음과 같은 구조로 동작한다.

  1. useRef로 참조 생성: useRef()를 호출하여 ref 객체를 만든다. 이 객체는 .current라는 프로퍼티를 가지며, 초기값은 null이다.

    JavaScript

    const inputRef = useRef(null);
    
  2. DOM 요소에 ref 연결: 렌더링할 JSX의 <input> 같은 DOM 요소에 ref 속성으로 생성한 ref 객체를 연결한다.

    JavaScript

    <input type="text" ref={inputRef} />
    

    이제 리액트는 이 <input> DOM 노드가 생성되면 inputRef.current에 해당 노드를 할당한다.

  3. 필요한 시점에 값 접근: 이벤트 핸들러(예: 폼 제출 핸들러) 내에서 inputRef.current.value를 통해 사용자가 입력한 현재 값을 직접 읽어온다.

    JavaScript

    const handleSubmit = (event) => {
      event.preventDefault();
      alert(`입력된 값: ${inputRef.current.value}`);
    };
    

제어 컴포넌트와의 구조적 차이

특징제어 컴포넌트 (Controlled Component)비제어 컴포넌트 (Uncontrolled Component)
상태 관리 주체리액트 컴포넌트 (useState)DOM 자체
데이터 흐름state value 속성, onChange setState (양방향 바인딩처럼 보임)DOM ref를 통한 값 조회 (단방향 조회)
값 동기화실시간 (모든 키 입력마다)특정 시점 (주로 제출 시)
주요 기술useState, value prop, onChange propuseRef, ref prop
초기값 설정useState의 초기값defaultValue prop

defaultValue 프로퍼티는 비제어 컴포넌트에서 초기 렌더링 시에만 값을 설정하고, 이후 사용자의 입력에는 관여하지 않는 중요한 속성이다. 만약 value를 사용하면 리액트는 해당 컴포넌트를 제어 컴포넌트로 인식하고, onChange 핸들러가 없으면 사용자가 입력을 해도 값이 바뀌지 않는 읽기 전용 필드가 되어버린다.


사용법 언제, 어떻게 활용하는가

비제어 컴포넌트는 특정 시나리오에서 매우 강력한 도구가 될 수 있다. 사용법을 구체적인 예시와 함께 살펴보자.

1. 간단한 폼 및 로그인 폼

실시간 유효성 검사나 입력 값에 따른 동적인 UI 변경이 필요 없는 단순한 폼은 비제어 컴포넌트의 가장 대표적인 사용 사례다.

JavaScript

import React, { useRef } from 'react';

function SimpleForm() {
  const usernameRef = useRef(null);
  const passwordRef = useRef(null);

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('Username:', usernameRef.current.value);
    console.log('Password:', passwordRef.current.value);
    // 여기서 API 호출 등을 수행
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        사용자명:
        <input type="text" ref={usernameRef} defaultValue="guest" />
      </label>
      <br />
      <label>
        비밀번호:
        <input type="password" ref={passwordRef} />
      </label>
      <br />
      <button type="submit">제출</button>
    </form>
  );
}

이 코드는 사용자가 입력할 때마다 리렌더링을 발생시키지 않는다. 오직 ‘제출’ 버튼을 눌렀을 때만 ref를 통해 DOM에 접근하여 값을 가져온다. 이는 불필요한 렌더링을 최소화하여 성능상 이점을 제공한다.

2. 파일 입력 (<input type="file">)

파일 입력 태그는 본질적으로 비제어적이다. 보안상의 이유로 개발자가 코드를 통해 파일 경로를 임의로 설정할 수 없기 때문이다. 사용자가 직접 파일을 선택해야만 값이 설정된다. 따라서 파일 입력은 ref를 사용해 접근하는 것이 가장 자연스럽다.

JavaScript

import React, { useRef } from 'react';

function FileUploader() {
  const fileInputRef = useRef(null);

  const handleSubmit = (event) => {
    event.preventDefault();
    if (fileInputRef.current.files.length > 0) {
      console.log(`선택된 파일: ${fileInputRef.current.files[0].name}`);
      // 파일 업로드 로직 처리
    } else {
      console.log('파일을 선택해주세요.');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        파일 업로드:
        <input type="file" ref={fileInputRef} />
      </label>
      <br />
      <button type="submit">업로드</button>
    </form>
  );
}

3. 외부 라이브러리와의 통합

DOM을 직접 조작하는 JQuery 플러그인이나 D3.js 같은 시각화 라이브러리를 리액트 컴포넌트 내에 통합해야 할 때 비제어 방식이 유용하다. ref를 통해 실제 DOM 노드를 가져와서 외부 라이브러리의 초기화 함수에 인자로 넘겨줄 수 있다.

JavaScript

import React, { useRef, useEffect } from 'react';
import $ from 'jquery';
import 'select2'; // 예시 라이브러리

function Select2Component({ data }) {
  const selectRef = useRef(null);

  useEffect(() => {
    // 컴포넌트가 마운트된 후 select2를 초기화
    $(selectRef.current).select2({ data: data });

    // 컴포넌트가 언마운트될 때 select2 인스턴스를 파괴
    return () => {
      $(selectRef.current).select2('destroy');
    };
  }, [data]); // data가 변경될 때 라이브러리를 다시 초기화할 수 있음

  return (
    <select ref={selectRef} style={{ width: '200px' }}>
      {/* 옵션은 select2가 동적으로 생성 */}
    </select>
  );
}

리액트가 ref를 통해 렌더링한 <select> DOM 노드에 대한 제어권을 select2 라이브러리에 넘겨주는 방식이다.


심화 내용 장단점과 주의사항

비제어 컴포넌트는 강력하지만 만병통치약은 아니다. 그 장단점을 명확히 이해하고 적재적소에 사용하는 지혜가 필요하다.

장점 (Pros)

  • 성능 최적화: 입력 값이 바뀔 때마다 리렌더링이 발생하지 않으므로 복잡한 폼에서 성능상 유리하다.

  • 코드의 간결함: 상태 관리 로직과 이벤트 핸들러가 줄어들어 코드가 더 짧고 직관적일 수 있다.

  • 높은 통합성: 전통적인 DOM 조작 방식과 유사하여, 리액트 생태계 외부의 라이브러리와 통합하기 용이하다.

  • 구현의 용이성: 간단한 경우에는 제어 컴포넌트보다 빠르게 구현할 수 있다.

단점 (Cons)

  • 제한적인 실시간 반응: 입력 값에 즉시 반응해야 하는 기능(예: 실시간 유효성 검사, 입력 값에 따른 버튼 활성화/비활성화)을 구현하기 번거롭다.

  • 낮은 예측 가능성: 상태가 DOM에 의해 관리되므로, 리액트의 선언적 패러다임에서 벗어난다. 데이터의 ‘단일 진실 공급원(Single Source of Truth)’ 원칙을 위배할 수 있다.

  • 복잡한 상태 로직 구현의 어려움: 여러 입력 값이 서로에게 영향을 주거나, 동적으로 폼 필드가 추가/삭제되는 등 복잡한 로직을 구현하기 어렵다. 이런 경우에는 제어 컴포넌트가 훨씬 유리하다.

언제 비제어 컴포넌트를 선택해야 하는가?

다음과 같은 상황을 마주했을 때 비제어 컴포넌트 사용을 적극적으로 고려해볼 수 있다.

  • “제출하고 잊어버리는(Fire-and-forget)” 형태의 간단한 폼 (예: 검색창, 뉴스레터 구독 폼)

  • 성능이 매우 중요한 대규모의 복잡한 폼에서 리렌더링을 최소화하고 싶을 때 (단, react-hook-form 같은 라이브러리는 내부적으로 비제어 방식을 활용해 성능과 제어의 장점을 모두 취하기도 한다)

  • <input type="file">을 다룰 때

  • 리액트로 완전히 제어되지 않는 외부 라이브러리나 레거시 코드와 연동해야 할 때

  • 포커스 관리, 미디어 재생 제어 등 특정 DOM API를 직접 호출해야 할 때

결론적으로, 비제어 컴포넌트는 리액트의 제어 철학에서 한 걸음 물러나 DOM의 자율성을 존중하는 실용적인 접근법이다. 제어 컴포넌트가 “리액트다운” 방식의 표준으로 여겨지지만, 비제어 컴포넌트라는 도구를 함께 갖춤으로써 개발자는 더 유연하고 효율적인 솔루션을 구축할 수 있다. 상황에 맞는 최적의 도구를 선택하는 것이 바로 뛰어난 개발자의 역량일 것이다.


다음 동영상은 리액트의 제어 및 비제어 컴포넌트에 대한 개념과 차이점을 설명하여, 이 주제에 대한 이해를 돕습니다.

비디오: 제어 및 비제어 컴포넌트 폼