React 교재 · 기초 6편

React useState — 컴포넌트에 상태 박기

처음 만나는 Hook. 클릭하면 +1 되는 카운터부터 객체/배열 state 함정까지.

UI 카운터 배지와 상태 셀이 화살표로 연결된 일러스트 — useState 반응성 컨셉

5편까지의 컴포넌트는 부모가 준 props 만 보여줬다. 사용자가 클릭·입력해도 화면이 안 바뀜. useState 가 그 벽을 깨는 첫 도구다.

이번 6편은 useState 의 기본 + 한국 개발자가 가장 많이 헤매는 두 함정 (setter 비동기성·객체/배열 업데이트) + 함수형 업데이트 패턴까지. 7편 이벤트 핸들링과 합치면 인터랙티브 UI 가 완성된다.

1. 첫 Hook — 카운터 5줄

가장 단순한 useState 예. 버튼 클릭하면 숫자 +1.

import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}> 카운트: {count} </button> ); }

한 줄씩 — useState(0) 초기값 0 으로 state 만들기. 반환은 배열 [현재값, setter 함수]. 구조분해로 받아 사용. setter 호출하면 React 가 state 갱신 + 컴포넌트 자동 재렌더. 새 값으로 다시 함수 실행되어 count 가 새 숫자.

왜 배열 반환인가 — 객체였으면 매번 const counter = useState(0); counter.value 식인데 길다. 배열이라 구조분해로 이름을 자유롭게 지을 수 있다 (const [count, setCount] 또는 const [name, setName]). 같은 컴포넌트에 useState 여러 개 박을 때 이름 충돌 없음.

2. setter 는 비동기 — 호출 직후 값 안 바뀜

한국 개발자가 가장 자주 묻는 질문 — "setState 한 다음 줄에서 console.log 하면 왜 옛 값이 나오나요?"

function Counter() { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); console.log(count); // ❌ 여전히 0 (다음 렌더에서야 1) }; return <button onClick={handleClick}>클릭: {count}</button>; }

이유 — React 의 state 는 다음 렌더 사이클에서 갱신. setter 는 "다음 렌더 때 이 값으로 해줘" 라는 예약일 뿐, 현재 함수 실행 중에는 count 변수가 옛 값 그대로. JavaScript 클로저의 자연스러운 결과.

새 값이 필요하면 setter 에 넘긴 값을 변수로:

const newCount = count + 1; setCount(newCount); console.log(newCount); // ✅ 1

3. 같은 setter 두 번 → 마지막만 적용? 아니다

비동기성의 두 번째 얼굴 — 한 핸들러 안에서 setter 두 번 호출하면 두 번 다 옛 값 기준.

const handleClick = () => { setCount(count + 1); // count=0 → 1 예약 setCount(count + 1); // count=0 → 1 예약 (또!) // 결과: count=1 (2 가 아니다) };

해결책 — 함수형 업데이트. setter 에 값 대신 "옛 값을 받아 새 값을 반환하는 함수" 전달:

const handleClick = () => { setCount(prev => prev + 1); // prev=0 → 1 예약 setCount(prev => prev + 1); // prev=1 (직전 예약 결과) → 2 // 결과: count=2 ✅ };

"같은 state 를 여러 번 업데이트" 가 필요하면 무조건 함수형. 카운터 외에도 토글 (setOn(prev => !prev)) · 누적 작업 (setLog(prev => [...prev, newItem])) 모두 같은 패턴.

4. 객체·배열 state 의 함정 — 항상 새 참조

state 가 객체/배열이면 한 가지 규칙 — 절대 직접 수정하지 말 것.

const [user, setUser] = useState({ name: '박준성', age: 42 }); // ❌ 직접 수정 (React 가 감지 못 함) user.age = 43; setUser(user); // ✅ 새 객체 만들어 전달 (spread 패턴) setUser({ ...user, age: 43 }); // ✅ 함수형 + spread setUser(prev => ({ ...prev, age: prev.age + 1 }));

이유 — React 는 state 가 바뀌었는지 판단할 때 참조 비교 (oldState === newState) 만 한다. 같은 객체를 수정해 다시 넣으면 참조가 같아서 "안 바뀜" 판단 → 재렌더 스킵.

배열도 같은 규칙arr.push(item) 금지. setArr([...arr, item]) 또는 setArr(prev => [...prev, item]). 깊은 중첩 객체면 structuredClone 또는 Immer 라이브러리 (회사 코드베이스 표준이면). 9편 리스트 렌더링에서 다시 등장.

useState 가 손에 잡히면 React 의 절반은 끝난 셈. 7편에서 이벤트 핸들링 패턴 (onChange·onSubmit·합성 이벤트), 8·9편 조건부·리스트 렌더링까지 합치면 실전 폼 컴포넌트 작성 준비 완료.

다음 글

React 교재 7편 — 이벤트 핸들링 패턴. onClick·onChange·onSubmit, 합성 이벤트, preventDefault, 인자 전달.

© 2026 주나이테크(주) @JUNAITECH