리액트에서 form을 다루기 위해 input 태그를 쓸 때, 흔히 볼 수 있는 에러가 있다. 바로 uncontrolled input이 controlled input으로 바뀌었다는 내용의 에러다.
콘솔에서 친절(?)하게 알려주고 있듯이, 초기값이 undefined 였다가 렌더링 후에 값이 들어와 defined value로 바뀌면서 에러가 발생하게 된다. 이 가이드에 따르면, input 태그를 일반적으로 사용하기 위해서는 value에 undefined가 들어가면 안 된다. 따라서 초기값이 존재하지 않을 경우, undefined가 아닌 공백(””)을 줘서 controlled input으로만 사용하도록 바꿔주면 에러를 방지할 수 있다. 그렇다면 여기서 말하는 controlled input이란 무엇일까?
Controlled? Uncontrolled?
React에서 사용되는 Controlled를 직역하면 “제어된다” 라는 말로 풀어쓸 수 있다. 제어되는 컴포넌트란 무엇이고 제어되지 않는 컴포넌트란 무엇인가?
제어되는 컴포넌트
제어된다는 것은 폼의 상태 또는 값이 컴포넌트의 상태를 통해 관리된다는 것을 말한다. 이 경우, 우리는 input의 value prop을 컴포넌트에서 관리하는 상태값으로 세팅하게 된다. 또한 해당 상태값을 onChange 이벤트 핸들러로 업데이트할 수 있다. 따라서 input의 값과 업데이트를 React 컴포넌트 자체에서 관리할 수 있게 된다.
사실 React 컴포넌트를 만들어 본 적이 있다면 이는 당연한 내용처럼 보인다. 컴포넌트에서 변경 가능한 state를 관리하는 것은 매우 자연스러운 설계다. 그러나 후술할 “제어되지 않는 컴포넌트"의 반대되는 개념인 “제어되는 컴포넌트"가 등장하는 이유는, HTML의 form 요소는 다른 엘리먼트들과는 조금 다르게 동작하기 때문이다.
<input>, <textarea>, <select> 같은 form 요소들은 자체적으로 내부 상태를 가지고, 유저 입력에 따라 업데이트된다. 즉, input 엘리먼트 자체가 스스로 state를 가지고, 유저 입력에 따라 해당 상태가 업데이트 된다는 것이다. 순수 HTML로 작성된 다음 코드는 정상적으로 동작할 것이며, HTML 동작 원리에 따라 다음 페이지로 이동할 것이다.
<form>
<label>
Name:
<input type="text" name="name" />
</label>
<input type="submit" value="Submit" />
</form>
하지만 React에서는 컴포넌트가 자체적으로 상태를 관리하고, 필요할 경우 자식에 props를 내려주기도 한다. 따라서 위와 같은 form를 React에서 사용한다는 것은 React 컴포넌트에서도 상태를 가지고 있고, HTML form 요소에서도 상태를 가지게 된다는 것을 의미한다. 이는 single source of truth (참고 링크) 원칙을 위반하게 된다.
따라서 React의 상태를 신뢰 가능한 단일 소스로 만들어 두 요소를 결합할 수 있다. 이 React 컴포넌트는 이후 form에서 발생하는 유저 입력을 컨트롤하게 된다. 이러한 방식으로 React에 의해 제어되는 form 요소를 제어되는 컴포넌트라고 부른다.
import { useState } from "react";
const ControlledForm = () => {
const [sample1, setSample1] = useState("this is sample1");
const [sample2, setSample2] = useState("this is sample2");
const onChangeValue1 = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setSample1(val);
};
const onChangeValue2 = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setSample2(val);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSample1("");
setSample2("");
};
return (
<form onSubmit={handleSubmit}>
<input type={"text"} value={sample1} onChange={onChangeValue1} />
<input type={"text"} value={sample2} onChange={onChangeValue2} />
<button type="submit">Submit</button>
</form>
);
};
유저가 입력한 form 데이터에 직접 접근할 수 있는 자바스크립트 Handler를 가지는 이 방식은 React에서 폼을 다루는 일반적인 방식이다. 딱 봐도 리액트스럽고 마음이 편하다.
제어되지 않는 컴포넌트
제어되지 않는다는 것은 폼의 상태 또는 값이 React가 아닌 DOM에서 처리된다는 것을 말한다. 이 말은 자바스크립트로 쓰여진 리액트 코드로 input의 상태값을 관리하는 것이 아닌, DOM 자체에서 상태를 관리한다는 말로 이해해볼 수 있다. 이 경우, 우리는 input의 value prop으로 값을 세팅하지 않는다. 대신 다음 코드와 같이 defaultValue prop을 통해 초기값이 필요한 경우 세팅할 수 있겠다.
const UncontrolledForm = () => {
const submitValue = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
e.target.reset();
};
return (
<form onSubmit={submitValue}>
<input name={"sample1"} type={"text"} defaultValue="enter something" />
<input name={"sample2"} type={"text"} defaultValue="enter something" />
<button type="submit">Submit</button>
</form>
);
};
form 요소의 value 속성은 DOM의 값보다 우선시된다. 따라서 제어되지 않는 컴포넌트를 사용하는 경우, defaultValue로 React가 초기값을 지정하고, 후속 업데이트는 제어하지 않는 것이 좋다.
만약 input의 value에 접근하고자 할 경우, 우리는 일반적으로 ref를 사용할 수 있다. 상태를 위한 이벤트 핸들러를 작성하는 대신 말이다. 다음은 useRef를 사용한 예시다.
import { useRef } from "react";
const UncontrolledForm = () => {
const sample1 = useRef<HTMLInputElement>(null);
const sample2 = useRef<HTMLInputElement>(null);
const submitValue = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const enteredSample1 = sample1.current.value;
const enteredSample2 = sample2.current.value;
/** do something with values... */
e.target.reset();
};
return (
<form onSubmit={submitValue}>
<input ref={sample1} type={"text"} defaultValue="enter something" />
<input ref={sample2} type={"text"} defaultValue="enter something" />
<button type="submit">Submit</button>
</form>
);
};
제어되지 않는 컴포넌트는 신뢰 가능한 상태를 DOM에 유지하므로, React와 React가 아닌 코드를 통합하는 데 더 쉬울 수 있다고 한다. 예를 들면 바닐라 자바스크립트로 작성된 소스를 React로 마이그레이션할 때를 들 수 있겠다. 그러나 일반적으로는 Refs를 남용하는 것을 추천하지 않는 React 철학에 따라, 제어되는 컴포넌트로 form을 작성하는 것이 더 권장된다.
에러는 언제 발생하는가
그렇다면 첫 문단에서 언급한, uncontrolled input이 controlled input으로 바뀌었다 는 내용의 에러는 과연 언제 발생하는 것일까? 유저의 이름과 나이를 입력받는 다음 React 컴포넌트를 보자.
import { useState } from "react";
const UserAddForm = () => {
const [userName, setUsername] = useState(undefined);
const [age, setAge] = useState(undefined);
const onChangeUsername = (event: React.ChangeEvent<HTMLInputElement>) => {
setUsername(event.target.value);
};
const onChangeAge = (event: React.ChangeEvent<HTMLInputElement>) => {
setAge(event.target.value);
};
const onSubmitUser = (event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
// save something...
clear();
};
const clear = () => {
setUsername(undefined);
setAge(undefined);
};
return (
<form onSubmit={onSubmitUser}>
<label>Username</label>
<input type="text" value={userName} onChange={onChangeUsername}></input>
<label>Age</label>
<input type="number" value={age} onChange={onChangeAge}></input>
<button type="submit">Add User</button>
</form>
);
};
input 태그의 value를 통해 상태를 관리하는 일반적인 controlled component 같아 보인다. 그러나 useState 선언 시 input 태그의 초기값은 undefined로 전달된다. React 입장에서는 undefined를 보고 이 input에 대해서 controlled라고 인지하게 된다. 그 후 유저가 이름과 나이를 입력할 때마다 onChange 핸들러를 통해 state는 string 값으로 할당되어 다시 input에 전달되게 된다. 따라서 React는 이 컴포넌트가 uncontrolled 라고 인지하고 있었는데, 갑자기 controlled로 변경하려는 시도가 감지되어 위와 같은 경고 메세지를 띄우는 것이다. 이는 null을 할당하는 경우에도 동일하다.
Warning: A component is changing an uncontrolled input to be controlled.
This is likely caused by the value changing from undefined to a defined value,
which should not happen. Decide between using a controlled or uncontrolled input
element for the lifetime of the component.
More info: <https://reactjs.org/link/controlled-components>
React 에서는 controlled와 uncontrolled 중 하나만을 선택하도록 유도하며, 컴포넌트의 수명 중간에 다른 방식으로 바뀌는 것은 있어서는 안 되는 일이라고(should not happen) 말하고 있다. 따라서 위의 경우, 다음과 같이 이름과 나이 상태값의 초기화를 undefined가 아닌 빈 값(””)로 바꿔주면 해결된다.
const [userName, setUsername] = useState("");
const [age, setAge] = useState("");
const clear = () => {
setUsername("");
setAge("");
};
결론
리액트에서 form을 다룰 땐 제어되는 컴포넌트의 형태로 구현하는 것이 편리할 수 있다. 이때 상태값으로 undefined 또는 null을 할당하는 것에 유의해야 한다. 불가피한 경우 제어되지 않는 컴포넌트를 사용할 수도 있겠다. 이때 ref를 적절하게 사용하여 상태값을 읽어올 수 있다.
Reference
'Web > React' 카테고리의 다른 글
리액트로 만들어보는 마우스 드래그 좌우스크롤 컴포넌트 (1) | 2024.01.21 |
---|---|
[React] 배열 랜더링과 Key (0) | 2022.11.04 |