ejyoo's 개발 노트

제어 컴포넌트와 비제어 컴포넌트의차이 본문

FrontEnd/React

제어 컴포넌트와 비제어 컴포넌트의차이

ejyoovV 2023. 8. 31. 04:18

React 폼 요소 다루는 방법

React 에서 폼 요소를 다루는 방법에는 제어 컴포넌트, 비제어 컴포넌트 두 가지 접근법이 있다.

제어/비제어 컴포넌트의 사전적 의미

이 둘의 사전적 의미는 아래와 같다.

1) 제어 컴포넌트 (Controlled Component) : React 에서 값을 제어하는 컴포넌트. 폼 요소의 상태를 React의 state로 관리하고, 값을 변경하기 위해서 이벤트 핸들러를 사용한다.

2) 비제어 컴포넌트 (UnControlled Component) : DOM 자체에 데이터를 저장하고 있고, 값을 직접 DOM 으로부터 가져옴. 'ref'를 사용하여 접근한다.

 

 

이 사전적 의미만으로 와닿지 않기에 각 컴포넌트의 자세한 내용과 그에 대한 예제를 살펴본다.

 

 

제어 컴포넌트

위에서 언급했듯이 

제어 컴포넌트는 React에서 폼 요소의 값을 React의 상태로 관리한다는 의미를 가진다.

 

보통 블로그 검색해보면 제어 컴포넌트 관련해서 'useStatus' React Hook 에 대한 예제를 많이 접할 수 있다.

하지만 리액트에서 제어 컴포넌트를 사용한다는 것은 useStatus만 있는 것이 아니다.

 

제어컴포넌트에서 이야기 하는 "상태" 는 'useState' 를 통해 생성된 state일 수도 있고, 'useReducer'나 Redux와 같은 외부 상태 라이브러리를 사용하여 관리되는 상태일 수 있다.

 

하지만 useState가 React에서 상태를 관리하는 가장 기본적이고 일반적인 방법 중 하나이기 때문에

제어 컴포넌트의 예제나 설명에서 자주 등장하는 것이다.

 

요약해서, 제어 컴포넌트는 React의 어떤 상태 관리 메커니즘을 사용하여 폼 요소의 값을 관리한다는 의미를 가질 수 있다.

 

제어 컴포넌트의 예시

그래서 아래의 예제는 useState 라는 상태 관리 메커니즘을 사용하여

제어 컴포넌트로 작성된 간단한 폼 예제이다.

import React, { useState } from 'react';

const ControlledExample = () => {
    const [value, setValue] = useState('');

    function handleChange(event) {
        setValue(event.target.value);
    }

    function handleSubmit() {
        alert('Submitted value: ' + value);
    }

	// JSX
    return ( 
        <>
            <input type="text" value={value} onChange={handleChange} />
            <button onClick={handleSubmit}>Submit</button>
        </>
    );
}

export default ControlledExample;

사용자가 Input에 값을 입력하면 그 값이 React의 상태로 관리되고, 제출 버튼을 클릭하면 현재 입력된 값을 알림으로 보여주는 예제이다.

 

코드를 조금 더 살펴보면,

Input 요소에 value 속성에 위에서 선언한 value 라는 useState 상태와 연결되어 있다.

이는 Input 의 값이 항상 value 상태와 동기화되도록 하는 제어컴포넌트의 특징이 고스란히 담겨있다.

 

이처럼 useState는

Input의 value 가 결정되기 때문에

'Single Source of Truth' 라는 원칙을 적용한 React Hook 중 하나라고 볼 수 있다.

 

제어 컴포넌트의 'Single Source of Truth' 원칙

Single Source of Truth 는 데이터 관리 원칙 중 하나로,

앱의 데이터나 상태에 대한 진실은 한 군데에서만 관리되어야 한다는 의미이다.

즉 해당 데이터는 한 곳에서만 변경되어야 하고, 다른 곳에서 참조될 때는 항상 동일한 값을 가져야 한다는 개념이다.

 

결국 Single Source of Truth 원칙은

데이터의 일관성을 유지하고, 앱의 복잡성을 관리하기 위한 중요한 개념이다.

그래서 리액트에서 useState를 포함한 제어 컴포넌트는 이 원칙을 준수하여 만들어 졌다고 볼 수 있다.

const ControlledInput = () => {
  const [text, setText] = useState("");

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

export default ControlledInput;

위의 간단한 예제로 Input 요소의 값을 React의 state로 관리하는 경우를 생각해본다.

사용자가 Input에 값을 입력하면 그 값이 React의 state에 저장되고, 이 state는 다시 Input의 value 속성에 전달되어 화면에 표시된다

이러한 구조에서 Input의 value는 항상 React의 state에 결정되는 것을 확인할 수 있다.

이로인해 input 값의 진실은 state 로 결정되고

이것이 Single Source of Truth의 개념이다.

 

 

이러한 개념으로 인해 제어 컴포넌트의 장점을 정리할 수 있다.

 

제어 컴포넌트의 장점

제어 컴포넌트의 장점은 Single Source of Truth의 장점과 거의 동일하다.

1) 예측 가능함

상태의 변화가 한 곳에서만 발생하여 어플리케이션의 동작을 예측하기 쉽다.

2) 데이터 동기화

여러 위치에서 같은 데이터를 참조할 때, 그 데이터는 항상 동기화된다.

3) 오류감소

데이터가 한 곳에서만 변경되므로, 데이터 불일치나 관련 오류가 줄어들게 된다.

 

 

제어 컴포넌트는 장점만 있는것이 아니다. 단점도 있다.

제어 컴포넌트의 단점

1) 코드의 복잡성 증가

- 상태관리

각 제어 컴포넌트마다 상태를 정의해야 하므로,

여러 입력 요소가 있으면 각각의 상태관리를 해야하기 때문에, 코드가 복잡해진다.

- 이벤트 핸들러

입력 요소의 값이 변경될 때마다 이를 처리하기 위한 이벤트 핸들러를 구현해야 한다.

여러 입력 요소에 대해 개별 핸들러 구현이 필요하면,

작업이 반복되고 추가적인 로직이 포함되면 복잡도가 더욱 증가된다.

2) 리렌더링 비용 발생

- 상태의 변경

React에서 상태가 변경될 때마다 해당 컴포넌트와 그 자식 컴포넌트들은 재 랜더링이 된다.

제어 컴포넌트에서 사용자의 입력마다 상태가 변경될 수 있어서,

그 결과로 컴포넌트의 불필요한 리렌더링이 발생할 수 있다.

- 성능 문제

상태 변경으로 인한 잦은 리렌더링은 큰 어플리케이션에서 성능 문제를 야기할 수 있다.

아무리 React 의 Virtual DOM 을 사용하여 빠른 리렌더링 , 최적화 기법들도 존재한다지만,

제어 컴포넌트를 사용할 때 이 문제는 꼭 고려해야 하는 사항이다.

 

제어 컴포넌트의 문제점을 해결하기 위해, Throttling 이나 Debouncing 방법이 있지만, 이 글에서는 다루지 않겠다.

.

.

.

.

 

폼을 개발을 하다보면 무조건 제어 컴포넌트를 사용할 수 없는 상황이 오기 마련이다.

그래서 비제어 컴포넌트를 사용하는 방식을 고려해볼 수 있게된다.

 

 

비제어 컴포넌트

비제어 컴포넌트는 React에서 제공하는 상태 관리 메커니즘이나 이벤트핸들러를 사용하지 않고

DOM 에서 직접 값을 얻어오는 방식의 컴포넌트를 의미한다.

 

 

그래서 두가지 개념을 가진다.

비제어 컴포넌트의기본 개념

1) DOM 에서 직접적인 값의 접근

- 'ref' 를 사용하여 DOM 요소에 직접 접근하고, 그 값을 얻어온다.

2) React 상태에서 독립적

- React의 'state' 를 사용하지 않아도 된다. 값의 변경이 React 상태와 연동되지 않고 DOM 요소 자체가 값을 가지고 있다.

 

비제어 컴포넌트의 예시

다음은 input 태그에 대한 비제어 컴포넌트의 예시이다.

import React, { useRef } from 'react';

const UncontrolledComponent = () => {
  const inputRef = useRef(null);

  function handleSubmit(event) {
    alert('A name was submitted: ' + inputRef.current.value);
    event.preventDefault();
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={inputRef} />
      <button type="submit">Submit</button>
    </form>
  );
}

export default UncontrolledComponent;

위 코드는

useRef React Hook 을 사용해서 input 요소에 대한 참조(ref)를 생성하고

제출 버튼을 클릭했을 때 해당 참조를 통해 입력값을 직접 얻어오는 코드이다.

위의 그림을 보면 useRef라는 React Hook은

React 내에서 DOM 요소에 직접 접근하는 참조를 생성하는 것으로 볼 수 있다.

 

useRef 에 의해 반환된 객체는 current 프로퍼티를 가지고 있고,

이 프로퍼티는 참조하고 있는 DOM 요소를 가리킨다.

 

따라서 위의 예제 코드에서,

useRef(null);

<input ref={inputRef} />

useRef(null); 로 초기화할 때 inputRef 객체는 { current: null } 형태를 가지고

<input ref={inputRef} /> 와 같이 ref 를 입력 요소에 연결하면 

inputRef.current는 이 <input> 요소를 직접 참조하게 된다.

그래서 inputRef.current.value 를 통해 입력된 값을 직접 접근할 수 있는 것이다.

 

 

이러한 사례를 바탕으로 비제어 컴포넌트는 Single Source of Truth를 준수하지 않다 라고 볼 수 있다.

비제어 컴포넌트와 Single Source of Truth

비제어 컴포넌트에서는 DOM 요소 자체가 값을 가지고 있기 때문에 

State와 달리 React 외부에서 값이 관리된다.

 

이로 인해 React 상태와 UI 사이에 동기화 작업이 필요할 수 있다.

예를 들면, 

입력 필드의 초기값을 설정한 뒤 사용자가 그 값을 변경하면, DOM 에는 저장되지만 React 의 상태에는 반영되지 않는다.

 

 

 

따라서 비제어 컴포넌트는 React의 상태를 사용하지 않고 DOM의 상태를 그대로 활용한다.

아래의 예제를 추가적으로 살펴본다.

비제어 컴포넌트 기준으로 제어/비제어 컴포넌트 혼합 사용 예제

다음 예제는 아래의 특징을 가지고 있다.

1) 비제어 컴포넌트를 사용하여 사용자 입력을 받는 컴포넌트를 생성

2) 동일 컴포넌트 내에서 React 상태 생성과 사용자가 입력한 값을 상태 저장하여 상태를 동기화

import React, { useRef, useState } from 'react';

const UncontrolledWithState = () => {
  const inputRef = useRef(null);
  const [text, setText] = useState("This won't change!");

  function handleShowValue() {
    alert('Current input value: ' + inputRef.current.value);
  }

  function handleSetState() {
    setText(inputRef.current.value);
  }

  return (
    <div>
      <input type="text" ref={inputRef} defaultValue="Type here..." />
      <button onClick={handleShowValue}>Show Input Value</button>
      <p>React State Value: {text}</p>
      <button onClick={handleSetState}>Try Setting React State</button>
    </div>
  );
}

export default UncontrolledWithState;

위의 예제를 보았을 때,

Show Input Value 버튼을 클릭하면 현재의 입력 값이 경고창으로 표시된다.

이는 Ref 로 인해 DOM 을 직접 접근하여 값을 가져온 결과다.

 

리액트의 state 상태에서 input 에 입력된 값을 가져오려면 

별도의 이벤트 핸들러를 구현하여 거기에서 inputRef.current.value를 가져와서 state 변수에 등록해주어야한다.

 

 

비제어 컴포넌트의 장점

1) 제어컴포넌트에 비해 코드가 간결

- 값의 변경을 추적하기위해 상태 선언이나 이벤트 핸들러를 작성할 필요가 없다.

비제어 컴포넌트의 단점

1) 직관성 떨어짐

- ref 를 사용해서 DOM 요소에 접근하기때문에, 직관적이지 않다. 눈으로 확인해야 한다.

2) 값 변경 시 로직 추가

- 값을 변경하거나 검증하고자 할 시 추가적 로직이 필요할 수 있다. (타입스크립트로 제어할 수 없음)

 

 

 

 

이러한 특징을 가진 제어 컴포넌트와 비제어 컴포넌트를 활용한 예시는 아래와 같다.

제어 컴포넌트와 비제어 컴포넌트의 활용 예시

제어 컴포넌트 비제어 컴포넌트
- 실시간 유효성 검사 폼
- 조건부 필드 렌더링 (예: 체크박스 선택 시 추가 필드 표시)
- 사용자 입력에 따라 다른 UI 렌더링
- 폼 상태 관리 필요없는 간단한 입력 폼 또는 데이터 제출 폼
- FileInput 태그와 같이 제어하기 힘든 DOM 요소 상태 관리