상태 끌어올리기(Lifting State Up)
여러 컴포넌트가 동일한 데이터를 함께 사용해야 할 때 상태값을 어디서 관리하는 게 적절할까요?
React에서는 이를 상태 끌어올리기(Lifting State Up)이라고 명명하는데요.
React는 기본적으로 단방향 흐름을 가져 부모에서 자식으로 props가 전달되고
자식 컴포넌트에서는 props를 직접적으로 수정하지 못합니다.
따라서 형제 컴포넌트 간 데이터를 공유하게 될 경우, 부모가 상태를 소유하는 패턴으로 진행해야 합니다.
부모 컴포넌트가 상태의 단일 소유자가 되어 모든 자식이 동일 기준을 바라보게 됩니다.
예를 들어 input의 체크박스와 그 내용간의 동기화 문제를 어떻게 해결해야 하는지 알아보겠습니다.
다음과 같은 코드를 작성해주세요.
function App() {
const [checked, setChecked] = useState(false);
return (
<div>
<Checkbox checked={checked} setChecked={setChecked} />
<Summary checked={checked} />
</div>
);
}
function Checkbox({ checked, setChecked }) {
return (
<label>
<input type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
알림 받기
</label>
);
}
function Summary({ checked }) {
return (
<p>{checked ? "알림을 받습니다" : "알림을 받지 않습니다."}</p>
);
}여기서 위에서 배운 원칙인 상태 끌어올리기를 통해 checked 상태의 단일 소유자로 App 컴포넌트로 지정했습니다.
이제 Checkbox는 상태를 가지지 않고, 부모가 내려준 checked를 값으로 쓰며, 변경도 Setter 함수를 통해 부모를 거칩니다.
이 흐름 덕분에 Checkbox에서 상태값이 변해도 부모의 상태가 바뀌고, 그 값이 자식 컴포넌트에 동시에 반영됩니다.
입력 요소가 제어 컴포넌트가 되려면 value or checked와 onChange가 쌍으로 존재해야 하는데
지금은 이 쌍이 부모 상태값을 기준으로 묶였습니다.
이를 통해 useState Hook 만으로 상태의 흐름 명확성이 유지되어 최적화됩니다. 이 패턴이 상태 최적화의 기본입니다.
Form 다루기
html에서는 form 태그 안에 input을 배치하고, 제출 버튼을 넣어 submit 이벤트를 처리하는 방식으로 동작합니다.
React에서는 input과 State를 연결해 제어 컴포넌트 방식을 따르는 것이 핵심입니다.
제어 컴포넌트란?
React가 input, textarea, select 같은 form 요소들의 값이 DOM이 아니라 State가 직접 관리하는 구조입니다.
사용자가 입력한 입력값이 React State로 반영되고, 그 State 값이 input value에 반영되는 순환 구조를 갖습니다.
즉 값의 진짜 원천이 React State가 됩니다.
먼저, 입력값을 저장할 State를 생성하는데 초기값을 빈 문자열로 생성하며, 이것이 제어 컴포넌트의 중심 저장소가 됩니다.
const [name, setName] = useState("");그리고 input에서 value 속성과 State를 연결하여, DOM이 아닌 React State만이 유일한 데이터 소스가 됩니다.
<input type="text" value={name} />그리고 Change Event를 통해 e.target.value를 꺼내 state 내용을 갱신합니다.
function handleChange(e) {
setName(e.target.value);
}
<input type="text" value={name} onChange={handleChange} />최종적으로, 결과를 Submit 할 때, React에서는 새로고침이 필요하지 않으므로 기본 동작을 차단합니다.
function handleSubmit(e) {
e.preventDefault();
......
}
<form onSubmit={handleSubmit}>....</form>여러 개의 input을 동시에 관리하는 방법
① 회원가입 화면처럼 이름, 이메일, 나이 같은 서로 연결된 여러 필드를 하나의 객체로 묶어 관리해야 합니다.
const [user, setUser] = useState({
name: "",
email: "",
age: ""
})② handleChange 로직에서 구조 분해 할당을 활용해, e.target에서 name, value를 꺼내서 씁니다.
input의 name 속성을 name, email, age로 설정했기 때문에 어떤 Field에서 Event가 발생했는지 쉽게 구분합니다.
그리고 스프레드 연산자를 이용해 기존 객체를 복사한 뒤, name 속성과 value 속성으로 해당 속성만 덮어씁니다.
여기서 앞서 배웠듯이 항상 새로운 객체를 만들어 불변성을 유지했다는 것입니다.
function handleChange(e) {
const { name, value } = e.target;
setUser({ ...user, [name]: value });
}③ 제어 컴포넌트 완성
각 input의 value를 user.name, user.email, user.age로 State를 연결했습니다.
이 부분이 제어 컴포넌트의 정의에 해당하는데 값의 진짜 원천은 React State에 있다는 것입니다.
모든 입력은 onChange를 통해 수정되어 State가 갱신되고 value에 반영됩니다.
따라서, UI와 State가 완전히 동기화됩니다.
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={user.name}
onChange={handleChange}
/>
<input
type="text"
name="email"
value={user.email}
onChange={handleChange}
/>
<input
type="number"
name="age"
value={user.age}
onChange={handleChange}
/>
<button>제출</button>
</form>
)④ 제어 컴포넌트와 유효성 검사
여러 input을 객체로 관리하면 handleChange 내부에서 즉시 검증할 수 있습니다.
여기서 잘못된 값이 입력되자마자 즉시 반영을 막을 수 있고, 올바른 값만 State에 저장됩니다.
결국 State와 UI가 항상 일관성이 유지되어 사용자 경험(UX)가 개선됩니다.
function handleChange(e) {
const { name, value } = e.target
if (name === 'age' && isNaN(Number(value)) {
alert('나이는 숫자만 입력할 수 있습니다!');
return;
}
if (name === 'email' && !value.includes('@')) {
alert('올바른 이메일 주소를 입력하세요.');
return;
}
....
}