1. State의 기본 개념 🧩
State란 무엇인가?
- 컴포넌트 내부에서 관리되는 변경 가능한 데이터 저장소
- 컴포넌트의 "메모리" 역할을 하는 객체
- props와 달리 컴포넌트가 직접 제어하고 수정 가능
- 상태가 변경되면 React는 해당 컴포넌트를 자동으로 다시 렌더링
State가 필요한 경우
- 시간이 지남에 따라 변경되는 데이터
- 사용자 입력에 반응해야 하는 값
- API 호출 결과를 저장할 때
- UI 상태(토글, 폼 입력값, 로딩 상태 등) 관리
State vs Props
- State: 컴포넌트 내부에서 관리, 변경 가능
- Props: 부모로부터 전달받음, 읽기 전용
- State는 종종 자식 컴포넌트에 props로 전달됨
2. 함수형 컴포넌트의 useState Hook 🪝
useState 기본 문법
import React, { useState } from 'react';
function Counter() {
// [현재 상태값, 상태 업데이트 함수] = useState(초기값)
const [count, setCount] = useState(0);
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
useState 특징
- 여러 개의 state 변수 선언 가능
- 다양한 데이터 타입 저장 가능 (숫자, 문자열, 불리언, 객체, 배열 등)
- 함수형 업데이트 지원 (이전 상태 기반 업데이트)
- 지연 초기화(lazy initialization) 지원
복잡한 상태 관리
// 객체 형태의 상태
const [user, setUser] = useState({ name: '', age: 0 });
// 객체 업데이트 (불변성 유지)
setUser(prevUser => ({ ...prevUser, name: '민영' }));
// 배열 형태의 상태
const [items, setItems] = useState([]);
// 배열에 항목 추가
setItems(prevItems => [...prevItems, newItem]);
지연 초기화
// 무거운 초기화 작업은 함수로 전달하여 최초 렌더링 시에만 실행
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation();
return initialState;
});
3. 클래스형 컴포넌트의 state 📦
초기화 및 사용법
class Counter extends React.Component {
constructor(props) {
super(props);
// state 초기화
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>현재 카운트: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
증가
</button>
</div>
);
}
}
setState 메서드 특징
- 상태 업데이트는 비동기적으로 처리됨
- 여러 setState 호출은 성능을 위해 배치 처리될 수 있음
- 이전 상태에 의존하는 업데이트는 함수 형태로 작성해야 함
// 잘못된 방식 (비동기적 업데이트로 인한 문제 발생 가능)
this.setState({ count: this.state.count + 1 });
// 올바른 방식 (이전 상태 기반 업데이트)
this.setState(prevState => ({ count: prevState.count + 1 }));
상태 업데이트 후 작업 수행
this.setState({ count: this.state.count + 1 }, () => {
// 상태 업데이트 후 실행될 콜백 함수
console.log('상태가 업데이트되었습니다:', this.state.count);
});
4. 상태 업데이트 주의사항 ⚠️
불변성(Immutability) 유지
- 상태를 직접 수정하지 말고, 항상 새 객체를 생성하여 업데이트
// 잘못된 방식 (직접 수정)
const [user, setUser] = useState({ name: '홍길동', age: 30 });
user.age = 31; // ❌ 직접 수정하면 리렌더링이 발생하지 않음
setUser(user);
// 올바른 방식 (새 객체 생성)
setUser({ ...user, age: 31 }); // ✅ 불변성 유지
객체와 배열 업데이트 패턴
// 객체 업데이트
const [person, setPerson] = useState({ name: '', address: { city: '', zipCode: '' } });
// 중첩 객체 업데이트
setPerson(prev => ({
...prev,
address: {
...prev.address,
city: '서울'
}
}));
// 배열 업데이트
const [todos, setTodos] = useState([]);
// 항목 추가
setTodos([...todos, newTodo]);
// 항목 제거
setTodos(todos.filter(todo => todo.id !== idToRemove));
// 항목 수정
setTodos(todos.map(todo =>
todo.id === idToUpdate ? { ...todo, completed: true } : todo
));
비동기적 업데이트 처리
// 문제가 될 수 있는 코드
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // count 값 참조
setCount(count + 1); // 여전히 같은 count 값 참조
// 결과적으로 count는 1만 증가
};
// 올바른 접근법
const handleClickCorrect = () => {
setCount(prevCount => prevCount + 1); // 이전 상태 기반 업데이트
setCount(prevCount => prevCount + 1); // 업데이트된 상태 기반
// 결과적으로 count는 2 증가
};
5. 복잡한 상태 관리 기법 🔄
useReducer를 통한 복잡한 상태 로직 분리
import { useReducer } from 'react';
// 리듀서 함수 정의
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
// [현재 상태, 디스패치 함수] = useReducer(리듀서 함수, 초기 상태)
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>카운트: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
상태 끌어올리기(Lifting State Up)
- 여러 컴포넌트가 동일한 상태를 공유해야 할 때 사용
- 공통 조상 컴포넌트에서 상태를 관리하고 props로 전달
function Parent() {
const [value, setValue] = useState('');
return (
<div>
<ChildA value={value} onChange={setValue} />
<ChildB value={value} />
</div>
);
}
커스텀 훅을 통한 상태 로직 재사용
// 커스텀 훅 정의
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
};
const reset = () => setValues(initialValues);
return { values, handleChange, reset };
}
// 커스텀 훅 사용
function SignupForm() {
const { values, handleChange, reset } = useForm({ username: '', email: '' });
const handleSubmit = (e) => {
e.preventDefault();
console.log(values);
reset();
};
return (
<form onSubmit={handleSubmit}>
<input
name="username"
value={values.username}
onChange={handleChange}
/>
<input
name="email"
value={values.email}
onChange={handleChange}
/>
<button type="submit">가입</button>
</form>
);
}