Controlled vs Uncontrolled

Controlled vs Uncontrolled (제어 vs 비제어)

모든 Mantine 입력 컴포넌트는 제어(controlled)와 비제어(uncontrolled) 두 모드를 모두 지원해요. 이 가이드는 두 모드의 차이와 언제 무엇을 사용해야 하는지 이해를 돕기 위한 문서예요.

출처: 문서

본문

제어 컴포넌트 (Controlled components)

제어 컴포넌트는 값이 React state에 의해 제어되는 폼 요소예요. 컴포넌트의 값은 state로 설정되고, 변경은 그 state를 갱신하는 이벤트 핸들러로 처리돼요. React가 폼 데이터의 단일 진실 공급원(single source of truth)이 되어요.

제어 TextInput 컴포넌트 예시:

import { useState } from 'react';
import { TextInput } from '@mantine/core';

function Demo() {
  const [value, setValue] = useState('');

  return (
    <TextInput
      label="Controlled TextInput"
      value={value}
      onChange={(event) => setValue(event.currentTarget.value)}
    />
  );
}

이 예시에서 입력 값은 항상 컴포넌트 state와 동기화돼요. 키 입력마다 state 갱신이 발생하고, 새 값으로 다시 렌더링돼요.

비제어 컴포넌트 (Uncontrolled components)

비제어 컴포넌트는 전통적인 HTML 폼 요소처럼 DOM(또는 내부 state)을 통해 내부적으로 자신의 state를 관리해요. React가 값을 직접 제어하지 않아요. 대신 ref나 DOM 메서드를 사용해 필요한 시점(보통 폼 제출 시)에 현재 값에 접근해요.

비제어 TextInput 컴포넌트 예시:

import { useRef } from 'react';
import { TextInput, Button } from '@mantine/core';

function Demo() {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    if (inputRef.current) {
      alert(`Input value: ${inputRef.current.value}`);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <TextInput label="Uncontrolled TextInput" ref={inputRef} />
      <Button type="submit">Submit</Button>
    </form>
  );
}

여기서 입력은 자신의 state를 유지해요. React는 ref를 통해 명시적으로 요청할 때만 값을 읽어요.

주요 차이점 (Key differences)

주요 차이는 state가 어디에 사는지에 있어요. 제어 컴포넌트는 state를 React에 저장하고, 비제어 컴포넌트는 DOM에 저장해요. 이 근본적인 차이는 컴포넌트의 수명 주기에서 어떻게 상호작용하는지에 영향을 줘요.

제어 컴포넌트에서는 value prop을 명시적으로 정의하고 모든 변경을 처리해요. 비제어 컴포넌트에서는 defaultValue를 설정하고 DOM이 갱신을 처리하게 두며, 필요할 때만 값에 접근해요.

제어 컴포넌트는 상호작용을 유지하기 위해 onChange 핸들러가 필요하지만, 비제어 컴포넌트는 표준 HTML 입력처럼 어떤 변경 핸들러도 없이 동작해요.

무엇을 언제 사용할까 (When to use which)

다음 같은 경우 제어 컴포넌트를 사용하세요:

  • 입력 값을 실시간으로 검증하거나 조작해야 할 때
  • 사용자 입력에 특정 형식이나 제약을 강제하고 싶을 때
  • 입력 변경에 따른 즉각적인 피드백이나 동적 UI 갱신이 필요할 때

다음 같은 경우 비제어 컴포넌트를 사용하세요:

  • 코드를 단순화하고 간단한 폼의 보일러플레이트를 줄이고 싶을 때
  • 폼 제출 전까지 입력 값을 검증하거나 조작할 필요가 없을 때
  • 성능이 우려되고 다시 렌더링을 최소화하고 싶은 대형 폼을 작업할 때

FormData와 비제어 컴포넌트

비제어 폼은 종종 FormData API와 함께 사용돼요. 이를 통해 각 입력의 state를 관리하지 않고도 폼 값을 쉽게 수집할 수 있어요. 모든 Mantine 컴포넌트는 FormData와 함께 비제어 사용을 지원해요.

FormData와 함께 비제어 Checkbox를 사용하는 예시:

import { Checkbox } from '@mantine/core';

function Demo() {
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        const formData = new FormData(event.currentTarget);
        console.log('Checkbox value:', !!formData.get('terms'));
      }}
    >
      <Checkbox label="Accept terms and conditions" name="terms" defaultChecked />
      <button type="submit">Submit</button>
    </form>
  );
}

비제어 use-form

@mantine/form은 성능이 좋은 대형 폼을 만들 수 있는 비제어 모드를 지원해요. 필드가 많은 복잡한 폼을 작업한다면, 비제어 모드의 useForm 훅이 훌륭한 선택이에요.

useForm과 함께 비제어 모드를 사용하는 예시:

import { useState } from 'react';
import { Button, Code, Text, TextInput } from '@mantine/core';
import { hasLength, isEmail, useForm } from '@mantine/form';

function Demo() {
  const form = useForm({
    mode: 'uncontrolled',
    initialValues: { name: '', email: '' },
    validate: {
      name: hasLength({ min: 3 }, 'Must be at least 3 characters'),
      email: isEmail('Invalid email'),
    },
  });

  const [submittedValues, setSubmittedValues] = useState<typeof form.values | null>(null);

  return (
    <form onSubmit={form.onSubmit(setSubmittedValues)}>
      <TextInput
        {...form.getInputProps('name')}
        key={form.key('name')}
        label="Name"
        placeholder="Name"
      />
      <TextInput
        {...form.getInputProps('email')}
        key={form.key('email')}
        mt="md"
        label="Email"
        placeholder="Email"
      />
      <Button type="submit" mt="md">
        Submit
      </Button>

      <Text mt="md">Form values:</Text>
      <Code block>{JSON.stringify(form.values, null, 2)}</Code>

      <Text mt="md">Submitted values:</Text>
      <Code block>{submittedValues ? JSON.stringify(submittedValues, null, 2) : '–'}</Code>
    </form>
  );
}

더 알아보기 (Learn more)