리액트 state 관리: useState 기본 사용법과 동작 원리
리액트(React)는 컴포넌트의 동적인 데이터를 다루기 위해 state 개념을 제공합니다. state는 UI와 데이터의 일관성을 유지하는 핵심 도구이며, 함수형 컴포넌트에서는 useState 훅을 사용해 관리합니다. 이 글에서는 state 변경 규칙(불변성)과 함께, 카운터 예제, 입력 폼 예제를 통해 useState의 기본 원리를 알아봅니다.
1. useState 기본 사용법
useState는 배열을 반환하며, 첫 번째 값은 현재 상태(state), 두 번째 값은 상태를 업데이트하는 함수(setter)입니다.
import React, { useState } from 'react';
export default function CounterBasic() {
const [count, setCount] = useState(0); // 초기값 0
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
</div>
);
}✅ 주의: setter 함수(setCount)를 직접 호출해야 UI가 다시 렌더링됩니다. count++ 같은 방식으로 직접 변경하면 동작하지 않습니다.
2. state 변경 규칙 (불변성)
리액트 state는 불변성(immutability)을 유지해야 합니다. 기존 값을 직접 수정하는 것이 아니라, 새로운 값을 만들어 setState 함수로 전달해야 합니다.
import React, { useState } from 'react';
export default function TodoList() {
const [todos, setTodos] = useState(['공부하기', '운동하기']);
const addTodo = () => {
// 잘못된 방법 ❌ (원본 배열 수정)
// todos.push('독서하기');
// setTodos(todos);
// 올바른 방법 ✅ (새 배열 생성)
setTodos([...todos, '독서하기']);
};
return (
<div>
<h3>할 일 목록</h3>
<ul>
{todos.map((todo, idx) => (
<li key={idx}>{todo}</li>
))}
</ul>
<button onClick={addTodo}>할 일 추가</button>
</div>
);
}✅ 배열/객체 state는 spread 연산자(...)나 map, filter 등을 활용해 새로운 값을 생성하는 방식으로 갱신해야 합니다.
3. 입력 폼 예제
폼 입력값을 관리할 때도 state를 활용합니다. 입력 이벤트(onChange)를 감지하여 상태를 업데이트하고, 그 값이 즉시 UI에 반영됩니다.
import React, { useState } from 'react';
export default function FormExample() {
const [name, setName] = useState('');
const [age, setAge] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
alert(`이름: ${name}, 나이: ${age}`);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>이름: </label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label>나이: </label>
<input
type="number"
value={age}
onChange={(e) => setAge(e.target.value)}
/>
</div>
<button type="submit">제출</button>
</form>
);
}✅ 입력 폼 예제는 state가 UI와 데이터의 양방향 연결을 어떻게 구현하는지 보여줍니다. React에서는 이런 패턴을 Controlled Component라고 부릅니다.
4. 통합 예제
아래 코드는 카운터 + 할 일 목록 + 입력 폼을 한 화면에서 확인할 수 있도록 만든 통합 예제입니다.
import React, { useState } from 'react';
export default function StateDemo() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState(['React 공부하기']);
const [input, setInput] = useState('');
const addTodo = () => {
if (!input) return;
setTodos([...todos, input]);
setInput('');
};
return (
<div style={{ padding: '16px' }}>
<h2>state 통합 예제</h2>
{/* 카운터 */}
<div>
<p>카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
</div>
{/* 할 일 목록 */}
<div>
<h3>할 일 목록</h3>
<ul>
{todos.map((todo, idx) => (
<li key={idx}>{todo}</li>
))}
</ul>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={addTodo}>추가</button>
</div>
</div>
);
}▼ 실행 결과 화면

마무리
state는 리액트 컴포넌트의 동적 데이터를 관리하는 핵심 도구입니다. useState 훅으로 간단히 정의할 수 있으며, 반드시 setter 함수를 통해 값을 변경해야 합니다. 또한 배열/객체 상태는 불변성을 지켜 새로운 값을 만들어 갱신해야 합니다. 이를 숙지하면 카운터, 입력 폼, 리스트 같은 기본 기능을 쉽게 구현할 수 있습니다.
참고 링크
함께 보면 좋은 게시글
- React 개념 정리: 가상 DOM·컴포넌트·장단점 분석
- React 시작하기: 환경 구축 & 첫 컴포넌트 만들기
- React JSX 기초: 표현식·조건부 렌더링·리스트 출력
- 리액트 JSX 문법 규칙: 인라인 스타일링·className·닫는 태그·주석
- 리액트 props 사용법: 부모에서 자식으로 데이터 전달하기
이 글이 도움이 되셨다면 공유 부탁 드립니다.



