제어 컴포넌트에 대한 공식문서의 설명은 아래와 같다.
제어 컴포넌트는 사용자의 입력을 기반으로 자신의 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
를 사용해서 값을 얻는다.
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() 는 heap영역에 저장되는 일반적인 자바스크립트 객체이다.
- 매번 렌더링할 때 동일한 객체를 제공한다. heap에 저장되어 있기 때문에 어플리케이션이 종료되거나 가비지 컬렉팅될 때 까지, 참조할때마다 같은 메모리 값을 가진다고 할 수 있다.
- 값이 변경되어도 리렌더링이 되지 않는다. 같은 메모리 주소를 갖고있기 때문에 자바스크립트의 === 연산이 항상 true 를 반환한다. 즉 변경사항을 감지할 수 없어서 리렌더링을 하지 않는다는 뜻이다.
제어 컴포넌트
비제어 컴포넌트
사진으로 확인하니 차이점이 와닿는다.
제어 컴포넌트
제어 컴포넌트의 값은 항상 최신값을 유지한다. 새로운 입력 값이 생길때 마다 상태를 새롭게 갱신한다. 이는 데이터와 UI에서 입력한 값이 항상 동기화됨을 알 수 있다.
비제어 컴포넌트
필드에서 값을 트리거 해야 값을 얻을 수 있다. 사진에선 [전송] 버튼을 클릭하면 console에 값이 찍힌다. [전송]버튼을 클릭해 트리거 하기 전까지의 값은 변경되지 않는다.
제어 컴포넌트를 사용하기 좋은 방식은 아래 세가지라고 한다.
하지만 항상 제어 컴포넌트의 방식이 사용하기 좋다는건 아니기 때문에, 이는 개발자가 유연하게 사고해 방식을 선택하는게 맞다고 생각한다.
제어 컴포넌트는 위해서도 언급했다 싶이 UI의 입력한 데이터 상태와 저장한 데이터의 상태가 항상 일치하는 것을 알 수 있다. 그러나 이 말을 다시 생각하자면, 사용자가 입력하는 모든 데이터가 동기화 된다 라는 의미가 된다.
예를들자면, input태그 안에 "안녕하세요"라는 값을 입력하나고 하면,
ㅇ
아
안
안ㄴ
안녀
안녕
안녕ㅎ
안녕하
안녕핫
안녕하
안녕하세
안녕하셍
안녕하세
안녕하세요
이런식으로 불필요한 단어 입력시에도 값이 갱신되어 버린다. 이는 불필요한 리렌더링, 불필요한 api요청으로 인한 자원 낭비 문제로도 연결 될 수 있다.
이러한 불필요한 방법을 막기 위해선 스로틀링이나 디바운싱 (throttle&debounce)을 사용할 수 있다.
쓰로틀링: 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것
디바운싱: 연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것
https://www.zerocho.com/category/JavaScript/post/59a8e9cb15ac0000182794fa
자세한 예시는 위의 링크를 참고하는 걸 추천한다. lodash에서도 해당 기능을 제공하므로 필요할 경우 사용하는 것도 좋을 것 같다. (직접 사용해본 경험은 없으므로 lodash에 대한 자세한 예시는 검색을 추천한다..)
하지만 일반적으로 모든 form 요소에서 상태의 동기화가 필요한 건 아니고, form 요소가 증가할 수록 모든 컴포넌트에 쓰로틀링이나 디바운싱을 걸기는 힘들다. 만약 값이 트리거 된 이후에만 갱신이 되도 문제가 없다면, ref를 사용하는 방식이 불필요한 렌더링을 방지하는데 더욱 도움이 될 수 있다. 이러한 비제어 컴포넌트를 사용해 렌더링을 최적화 하는 라이브러리가 react-hook-form 이다.
기능 | 제어 컴포넌트 | 비제어 컴포넌트 |
---|---|---|
일회성 정보 검색 (예: 제출) | O | O |
제출 시 값 검증 | O | O |
실시간으로 필드값의 유효성 검사 | O | X |
조건부로 제출 버튼 비활성화 (disabled) | O | X |
실시간으로 입력 형식 적용하기 (숫자만 가능하게 등) | O | X |
동적 입력 | O | X |
으로 구분지어 사용할 수 있겠다. 위에서도 언급했지만 특정 방법이 옳고 그르다고 하기엔 각각의 용도가 있기 때문에 상황에 맞춰 쓰는게 가장 무난한 방법인듯 싶다.(라고 나는 생각한다.)
https://whal.eu/l/B7G1J1eE
https://goshakkk.name/controlled-vs-uncontrolled-inputs-react/
혹시라도 글에 잘못된 내용이 있다면 댓글로 알려주세요!
좋은글 감사합니다