제어 컴포넌트와 비제어 컴포넌트의 차이점
💡 제어 컴포넌트
제어 컴포넌트에 대한 공식문서의 설명은 아래와 같습니다.
제어 컴포넌트는 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트합니다. React에서는 변경할 수 있는 state가 일반적으로 컴포넌트의 state 속성에 유지되며 setState()에 의해 업데이트됩니다.
(중략)
이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트 (controlled component)“라고 합니다.
https://ko.reactjs.org/docs/forms.html#controlled-components
사용자 입력을 기반으로 자신의 state를 관리하고 업데이트한다라고 정의되어 있는데,,🤔 솔직히 어떤 말인지 잘 와닿지 않습니다. 한번 코드로 살펴보겠습니다.
export default function App() {
const [input, setInput] = useState("");
const onChange = (e) => {
setInput(e.target.value);
};
return (
<div className="App">
<input onChange={onChange} />
</div>
);
}
쉽게 설명하자면, 사용자의 입력을 받는 컴포넌트에 event 객체를 이용해 setState()로 값을 저장하는 방식을 제어 컴포넌트 방식이라 할 수 있겠습니다. (위에서도 말했다시피 React에 의해 값이 제어되므로 제어 컴포넌트라고 부릅니다.) 제어 컴포넌트는 사용자가 입력한 값과 저장되는 값이 실시간으로 동기화된다.
💡 비제어 컴포넌트
비제어 컴포넌트는 기존의 바닐라 자바스크립트와 크게 다르지 않은 방식으로 우리는 바닐라 자바스크립트를 사용하여 폼을 제출할 때 특정 이벤트가 발생 시(submit button을 클릭할 때) 요소 내부의 값을 가져왔습니다.
비제어 컴포넌트 또한 이와 유사한 방식으로 사용됩니다. 비제어 컴포넌트 방식을 사용할 땐, 제어 컴포넌트 방식에서 사용한 setState()를 쓰지 않고 ref(React에서 Dom을 직접 핸들링할 때 사용)를 사용해서 값을 얻습니다.
export default function App() {
const inputRef = useRef(); // ref 사용
const onClick = () => {
console.log(inputRef.current.value);
};
return (
<div className="App">
<input ref={inputRef} />
<button type="submit" onClick={onClick}>
전송
</button>
</div>
);
}
비제어 컴포넌트는 제어 컴포넌트와는 달리 값이 실시간으로 동기화되지 않습니다.
만약 a와 b라는 컴포넌트가 있을 때, a에 대한 변화를 즉각적으로 b가 영향을 받아야 할 때에는 비제어 컴포넌트를 사용하면 이런 방식에 대한 대응을 할 수 없습니다.
제어 컴포넌트가 사용자가 입력을 하는 액션이 발생될 때마다 리렌더링을 발생시키는 반면, 비제어 컴포넌트는 사용자가 직접 트리거하기 전까지는 리렌더링을 발생시키지도 않고 값을 동기화도 시키지 않습니다.
💬 약간의 보너스: useRef와 리렌더링에 관해
그럼 왜 비제어 컴포넌트를 사용할 땐 useRef를 사용하고, 이러한 useRef는 왜 리렌더링을 발생시키지 않는 걸까요?
useRef()는 heap영역에 저장되는 일반적인 자바스크립트 객체이다. 매번 렌더링 할 때 동일한 객체를 제공한다. heap에 저장되어 있기 때문에 애플리케이션이 종료되거나 가비지 컬렉팅 될 때까지, 참조할 때마다 같은 메모리 값을 가진다고 할 수 있다.
- 값이 변경되어도 리렌더링이 되지 않는다. 같은 메모리 주소를 갖고 있기 때문에 자바스크립트의 === 동치 연산이 항상 true를 반환한다. 즉 변경사항을 감지할 수 없어서 리렌더링을 하지 않는다는 뜻이다.
💡 비제어 컴포넌트와 제어 컴포넌트 - 이미지로 비교하기
1. 제어 컴포넌트
2. 비제어 컴포넌트
사진으로 확인하면 차이점이 보다 명확히 와닿습니다.
- 제어 컴포넌트
제어 컴포넌트의 값은 항상 최신값을 유지하며 새로운 입력 값이 생길 때마다 상태를 새롭게 갱신합니다. 이는 데이터와 UI에서 입력한 값이 항상 동기화됨을 알 수 있습니다. - 비제어 컴포넌트
필드에서 값을 트리거해야 값을 얻을 수 있습니다. 사진에선 [전송] 버튼을 클릭할 때 console에 값이 출력되게 되는데 이처럼 비제어 컴포넌트는 이벤트가 발생되기 전까지의 값은 변경되지 않는다.
💡 그럼 제어 컴포넌트는 언제 사용할까?
제어 컴포넌트를 사용하기 좋은 방식은 아래 3가지라고 합니다.
- 유효성 검사
- 유효한 데이터가 없는 경우 전송 버튼의 상태를 disabled로 표시
- 신용카드와 같은 특정 입력 방식 적용
하지만 항상 제어 컴포넌트의 방식이 사용하기 좋다는 건 아니기 때문에, 이는 개발자가 유연하게 사고해 방식을 선택하는 게 맞다고 생각합니다.
✔ 제어 컴포넌트를 사용할 때 문제점
제어 컴포넌트는 위해서도 언급했다시피 UI의 입력한 데이터 상태와 저장한 데이터의 상태가 항상 일치하는 것을 알 수 있다. 그러나 이 말을 다시 생각하자면, 사용자가 입력하는 모든 데이터가 동기화된다 라는 의미가 됩니다.
예를 들면, input 태그 안에 "안녕하세요"라는 값을 입력하려고 하면,
ㅇ
아
안
안ㄴ
안녀
안녕
안녕ㅎ
안녕하
안녕핫
안녕하
안녕하세
안녕하셍
안녕하세
안녕하세요
위와 같이 불필요한 단어 입력시에도 값이 갱신되어 버리게 되는데 이는 불필요한 리렌더링, 불필요한 api요청으로 인한 자원 낭비 문제로도 연결될 수 있습니다.
이러한 불필요한 방법을 막기 위해선 스로틀링이나 디바운싱 (throttle&debounce)을 사용할 수 있습니다.
쓰로틀링: 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것
디바운싱: 연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것
https://www.zerocho.com/category/JavaScript/post/59a8e9cb15ac0000182794fa
일반적으로 모든 form 요소에서 상태의 동기화가 필요한 건 아니고, form 요소가 증가할수록 모든 컴포넌트에 쓰로틀링이나 디바운싱을 걸기는 힘듭니다. 만약 값이 트리거 된 이후에만 갱신이 돼도 문제가 없다면, ref를 사용하는 방식이 불필요한 렌더링을 방지하는데 더욱 도움이 될 수 있다. 이러한 비제어 컴포넌트를 사용해 렌더링을 최적화하는 라이브러리가 react-hook-form입니다.
✔ 제어 컴포넌트는 되고 비제어 컴포넌트는 안 되는 것들
기능 : 제어 컴포넌트 vs 비제어 컴포넌트
일회성 정보 검색 (예: 제출) | O | O |
제출 시 값 검증 | O | O |
실시간으로 필드값의 유효성 검사 | O | X |
조건부로 제출 버튼 비활성화 (disabled) | O | X |
실시간으로 입력 형식 적용하기 (숫자만 가능하게 등) | O | X |
동적 입력 | O | X |
- 즉각적으로, 실시간으로 값에 대한 피드백이 필요하다 => 제어 컴포넌트 사용
- 즉각적인 피드백이 불필요하고 제출시에만 값이 필요하다, 불필요한 렌더링과 값 동기화가 싫다 => 비제어 컴포넌트 사용
으로 구분 지어 사용할 수 있을 거 같습니다.