controlled vs uncontrolled component: 무엇이 다르고 언제 무엇을 써야 할까

Frontend

React에서 form을 다루다 보면 자주 만나는 표현이 있습니다.

  • controlled component
  • uncontrolled component

처음 보면 단순히 input 사용법의 차이처럼 느껴지지만, 실제로는 입력값의 진짜 기준(source of truth)을 어디에 둘 것인가에 대한 이야기입니다.

이 글에서는 개념만 구분하지 않고, 실무에서 자주 부딪히는 질문까지 같이 보겠습니다.

  • 실시간 검증이 필요할 때는 무엇이 더 자연스러운가
  • 성능은 정말 uncontrolled가 더 유리한가
  • 파일 업로드는 왜 보통 uncontrolled로 보게 되는가
  • React Hook Form은 왜 uncontrolled 기반이라는 설명을 자주 듣는가

한눈에 보면

짧게 요약하면 이렇습니다.

  • controlled: 입력값의 기준이 React state에 있다
  • uncontrolled: 입력값의 기준이 DOM 자체에 있다

같은 input이라도 어디를 기준으로 삼느냐에 따라 코드 구조가 달라집니다.

// controlled
const [email, setEmail] = useState('');
<input value={email} onChange={(event) => setEmail(event.target.value)} />;
// uncontrolled
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} defaultValue="" />;

앞의 방식은 React가 값을 계속 알고 있고, 뒤의 방식은 필요할 때 DOM에서 값을 읽습니다.

controlled component란?

controlled component는 input 값이 React state에 의해 제어되는 방식입니다.

import { useState } from 'react';
 
export function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
 
  return (
    <form>
      <input
        type="email"
        value={email}
        onChange={(event) => setEmail(event.target.value)}
        placeholder="이메일"
      />
      <input
        type="password"
        value={password}
        onChange={(event) => setPassword(event.target.value)}
        placeholder="비밀번호"
      />
    </form>
  );
}

여기서 진짜 값은 DOM이 아니라 email, password state입니다. input은 그 state를 화면에 반영하는 역할을 합니다.

장점

  • 현재 값이 항상 React 안에 있다
  • 실시간 검증, 포맷팅, 마스킹이 쉽다
  • 다른 UI와 상태를 연결하기 쉽다
  • 디버깅 시 흐름을 추적하기 편하다

예를 들어 입력 즉시 에러 메시지를 보여주는 UX는 controlled가 자연스럽습니다.

const [email, setEmail] = useState('');
const isInvalid = !email.includes('@');
 
return (
  <>
    <input value={email} onChange={(event) => setEmail(event.target.value)} />
    {isInvalid && <p>올바른 이메일 형식을 입력해주세요.</p>}
  </>
);

uncontrolled component란?

uncontrolled component는 input 값이 DOM 내부에 있고, React는 필요할 때만 읽는 방식입니다.

import { useRef } from 'react';
 
export function SearchForm() {
  const keywordRef = useRef<HTMLInputElement>(null);
 
  const handleSubmit = (event: React.FormEvent) => {
    event.preventDefault();
    const keyword = keywordRef.current?.value ?? '';
    console.log('search:', keyword);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input ref={keywordRef} defaultValue="" placeholder="검색어" />
      <button type="submit">검색</button>
    </form>
  );
}

이 방식에서는 입력 중간 상태를 React가 계속 들고 있지 않습니다. 제출 시점처럼 필요할 때만 값을 읽습니다.

장점

  • 구조가 단순할 수 있다
  • 모든 입력마다 state 업데이트를 만들지 않아도 된다
  • 브라우저의 기본 폼 동작과 더 가깝다

즉, 입력 중간 상태를 굳이 React에서 관리할 이유가 없을 때는 꽤 실용적입니다.

둘의 차이를 실무 감각으로 보면

핵심 차이는 아래 질문 하나로 정리됩니다.

입력 중간 상태를 React가 계속 알아야 하는가?

이 질문에 라면 controlled 쪽이 자연스럽고, 아니오라면 uncontrolled로 단순하게 갈 수 있습니다.

로그인 폼은 왜 controlled가 자주 쓰일까?

로그인 폼은 보통 아래 요구가 붙습니다.

  • 실시간 validation
  • 버튼 disabled 제어
  • 에러 메시지 노출
  • 입력값 포맷팅

이 요구는 대부분 입력 중간 상태를 React가 알아야 구현하기 쉽습니다.

const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
 
const isSubmitDisabled = !email.includes('@') || password.length < 8;

이런 경우 uncontrolled도 불가능한 것은 아니지만, 오히려 더 우회적으로 풀게 됩니다.

검색창은 uncontrolled도 충분한 경우가 많다

반대로 검색창은 종종 제출 시점의 값만 중요합니다.

  • 사용자가 입력하는 매 타이핑마다 다른 UI가 바뀌지 않고
  • 제출 순간의 문자열만 읽으면 되고
  • 브라우저 기본 폼 흐름이 자연스럽다면

아래처럼 단순하게 둘 수 있습니다.

function SearchBar() {
  const inputRef = useRef<HTMLInputElement>(null);
 
  const handleSubmit = (event: React.FormEvent) => {
    event.preventDefault();
    const keyword = inputRef.current?.value.trim() ?? '';
 
    if (!keyword) return;
 
    console.log('submit keyword:', keyword);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} placeholder="검색어를 입력하세요" />
      <button type="submit">검색</button>
    </form>
  );
}

파일 업로드는 왜 보통 uncontrolled로 보게 될까?

파일 input은 대표적인 예외처럼 느껴질 수 있지만, 사실 브라우저 제약상 controlled로 다루지 않는 편이 자연스럽습니다.

function UploadForm() {
  const fileRef = useRef<HTMLInputElement>(null);
 
  const handleUpload = () => {
    const file = fileRef.current?.files?.[0];
 
    if (!file) return;
 
    console.log(file.name);
  };
 
  return <input ref={fileRef} type="file" />;
}

파일 목록 자체는 브라우저가 관리하고, React는 선택 결과를 읽어서 후속 로직만 수행하는 구조가 일반적입니다.

controlled가 성능에 불리하다는 말은 항상 맞을까?

자주 나오는 오해 중 하나입니다.

입력마다 setState가 일어나니 무조건 느릴 것처럼 느껴질 수 있습니다. 하지만 실제 체감 성능은 아래 요소에 더 크게 좌우됩니다.

  • 상태가 어디까지 전파되는가
  • 어떤 컴포넌트가 같이 리렌더되는가
  • 무거운 계산이 입력과 같이 묶여 있는가

즉, 문제가 "controlled라서"라기보다 state 위치와 렌더링 범위 설계가 적절한가인 경우가 많습니다.

예를 들어 입력 state를 큰 페이지 최상단에 두고 많은 하위 컴포넌트를 같이 다시 그리게 만들면 느릴 수 있습니다. 하지만 입력 컴포넌트 근처로 상태를 좁히면 controlled여도 충분히 빠르게 동작하는 경우가 많습니다.

React Hook Form은 왜 uncontrolled 기반이라고 할까?

실무에서 많이 쓰는 React Hook Form은 입력값을 매 타이핑마다 전부 React state로 올리는 방식보다, ref와 브라우저 input 동작을 적극 활용하는 방향에 가깝습니다.

이 접근이 주는 장점은 분명합니다.

  • 많은 필드를 가진 폼에서도 렌더링 부담을 줄이기 쉽고
  • 브라우저 입력 흐름과 잘 맞고
  • 필요한 시점에만 값을 수집하고 검증하기 좋습니다

물론 Controller를 써서 controlled 컴포넌트와도 함께 쓸 수 있습니다. 그래서 실제로는 "무조건 uncontrolled"라기보다, 폼 라이브러리 입장에서 uncontrolled 스타일이 유리한 부분이 크다고 이해하는 편이 더 정확합니다.

둘을 섞어서 쓰는 것도 자연스럽다

실무에서는 둘 중 하나만 고집할 필요가 없습니다.

예를 들어:

  • 검색창 input 자체는 uncontrolled
  • 제출된 검색어 state는 controlled하게 보관

혹은:

  • 기본 필드는 uncontrolled
  • 실시간 formatting이 필요한 카드 번호 입력만 controlled

이런 혼합 전략이 오히려 가장 현실적일 때가 많습니다.

그럼 어떤 기준으로 선택하면 될까?

아래 기준으로 보면 비교적 정리가 됩니다.

  1. 입력 중간 상태가 다른 UI와 실시간으로 연결되는가
    controlled를 먼저 봅니다.

  2. 제출 시점의 값만 중요하고 중간 상태는 크게 중요하지 않은가
    uncontrolled도 충분히 좋습니다.

  3. 필드가 많고 폼 라이브러리를 적극 활용하는가
    uncontrolled 기반 전략을 같이 검토합니다.

  4. 실시간 검증, 마스킹, 포맷팅이 중요한가
    대체로 controlled가 더 자연스럽습니다.

  5. 성능이 걱정되는가
    패턴 이름보다 상태 위치와 렌더링 범위를 먼저 점검합니다.

정리하면

controlled componentuncontrolled component의 차이는 결국 이 한 줄로 정리할 수 있습니다.

입력값의 기준이 React state에 있으면 controlled, DOM 자체에 있으면 uncontrolled입니다.

실무 기준으로 다시 줄이면:

  • 입력 중간 상태를 React가 계속 알아야 하면 controlled
  • 제출 시점 값만 중요하면 uncontrolled
  • 파일 업로드나 폼 라이브러리는 uncontrolled가 더 자연스러울 때가 많고
  • 둘을 상황에 맞게 섞는 것이 오히려 가장 현실적입니다

중요한 것은 어떤 방식이 더 "React스럽다"가 아니라, 이 입력을 실제로 어떤 상호작용과 검증 흐름으로 다뤄야 하는가입니다. 그 질문에 답하면 선택은 생각보다 분명해집니다.

같이 보면 좋은 글