React

React useState 사용법: state와 객체 배열 업데이트 기준

2025.12.04·수정 2026.05.11·약 11분

이 글에서 정리하는 내용

React에서 state가 왜 필요한지부터 일반 변수와의 차이, 기본 문법, 객체와 배열 업데이트 방식이전 값 기반 업데이트가 필요한 이유까지 한 흐름으로 정리합니다. 화면이 바뀌어야 하는 값은 왜 state로 관리해야 하는지 기준을 잡는 데 집중합니다.

state가 왜 필요한가

React useState 사용법: state와 객체 배열 업데이트 기준 핵심 개념을 설명하는 첫 번째 본문 이미지

React를 처음 배울 때 가장 많이 헷갈리는 지점은 변수와 state를 같은 종류의 값처럼 느끼는 부분입니다. 하지만 React에서 둘은 역할이 다릅니다. 일반 변수는 단순히 자바스크립트 안에서 값이 바뀌는 것에 가깝고, state는 화면을 다시 그려야 하는 값이라는 의미를 함께 가집니다.

기준은 단순합니다. 화면에 보이는 값이 바뀌어야 한다면 state로 관리합니다. 클릭 횟수, 입력창 값, 펼침 여부, 목록 데이터처럼 UI에 직접 연결된 값은 state여야 React가 다시 렌더링하면서 바뀐 화면을 보여줄 수 있습니다.

일반 변수로는 화면이 바뀌지 않는 이유

import { useState } from 'react'; export default function WrongCounter() { // 일반 변수는 렌더링마다 다시 만들어집니다. let count = 0; const handleClick = () => { // 값은 바뀌는 것처럼 보여도 React에 다시 그리라고 알리지 못합니다. count += 1; console.log(count); }; return ( <div> <p>현재 값: {count}</p> <button onClick={handleClick}>증가</button> </div> );
}

위 코드는 콘솔 숫자는 변하는 것처럼 보여도 화면은 기대대로 바뀌지 않습니다. React는 일반 변수 변경만으로는 다시 렌더링하지 않기 때문입니다. 이 차이를 이해해야 왜 state가 필요한지 자연스럽게 이어집니다.

기본 사용법

state는 로 만듭니다. 는 현재 값과 값을 바꾸는 setter 함수를 한 쌍으로 돌려줍니다. 보통 배열 구조 분해 문법으로 받아서 사용합니다.

가장 기본적인 카운터 예시

import { useState } from 'react'; export default function Counter() { // count는 현재 상태 값, setCount는 상태 변경 함수입니다. const [count, setCount] = useState(0); const handleClick = () => { // setter를 호출하면 다음 렌더링에 반영할 값을 예약합니다. setCount(count + 1); }; return ( <div> <p>현재 값: {count}</p> <button onClick={handleClick}>증가</button> </div> );
}

여기서는 값만 바꾸는 것이 아니라 React에게 화면을 다시 계산할 이유를 알려줍니다. 그래서 버튼을 누를 때마다 count가 바뀌고, 바뀐 값이 화면에 반영됩니다. 이때 중요한 점은 state는 바로 덮어쓰는 변수라기보다 다음 렌더를 위한 값으로 다뤄야 한다는 것입니다.

state는 snapshot처럼 동작합니다

import { useState } from 'react'; export default function SnapshotExample() { const [count, setCount] = useState(0); const handleClick = () => { // 현재 이벤트 안에서는 지금 렌더의 count를 읽습니다. setCount(count + 1); console.log(count); }; return ( <button onClick={handleClick}>현재 값 {count}</button> );
}

이 코드는 버튼을 눌렀을 때 를 호출해도 바로 아래의 console.log에는 이전 count가 찍힐 수 있습니다. React 공식 문서에서는 이 동작을 state가 snapshot처럼 읽힌다고 설명합니다. 지금 실행 중인 이벤트 핸들러는 현재 렌더에서 받은 state를 기준으로 동작하고, setter는 다음 렌더에 반영할 값을 예약하는 역할에 가깝습니다.

구분 의미
count 현재 렌더에서 읽는 상태 값
다음 렌더에 사용할 값을 설정하는 함수

객체와 배열 state는 어떻게 업데이트할까

숫자나 문자열은 비교적 단순하지만, 실무에서는 객체와 배열을 state로 다루는 경우가 더 많습니다. 폼 입력값 묶음, 체크리스트, 장바구니 목록, 댓글 배열 같은 데이터가 여기에 해당합니다.

이때 가장 먼저 잡아야 할 기준은 기존 state를 직접 수정하지 않는다는 점입니다. React에서는 객체와 배열도 읽기 전용처럼 다루고, 바뀐 내용을 반영한 새 객체나 새 배열을 만들어 교체해야 합니다.

객체 state 업데이트

import { useState } from 'react'; export default function ProfileForm() { const [form, setForm] = useState({ name: '', job: ''}); const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { // 기존 객체를 펼친 뒤 필요한 속성만 새 값으로 교체합니다. setForm({ ...form, name: e.target.value}); }; return ( <div> <input value={form.name} onChange={handleNameChange} /> <p>이름: {form.name}</p> <p>직무: {form.job}</p> </div> );
}

객체에서 특정 속성만 바꾸고 싶더라도 원본 객체를 직접 수정하면 안 됩니다. 기존 값을 복사하고 필요한 부분만 바꿔 새 객체를 만드는 방식이 안전합니다.

배열 state 업데이트

import { useState } from 'react'; export default function TodoList() { const [todos, setTodos] = useState(['React 복습']); const handleAdd = () => { // push 대신 새 배열을 만들어 교체합니다. setTodos([...todos, 'state 정리']); }; const handleRemove = (target: string) => { // filter로 제거된 새 배열을 만듭니다. setTodos(todos.filter((todo) => todo !== target)); }; return ( <div> <button onClick={handleAdd}>추가</button> <ul> {todos.map((todo) => ( <li key={todo}> {todo} <button onClick={() => handleRemove(todo)}>삭제</button> </li> ))} </ul> </div> );
}

배열도 동일합니다. push, pop, splice처럼 원본을 바꾸는 방식보다, 전개 연산자, map, filter처럼 새 배열을 만드는 방식으로 접근해야 state 업데이트 흐름이 안정적입니다.

한 가지를 더 주의해야 합니다. 전개 연산자는 얕은 복사이기 때문에, 중첩된 객체까지 자동으로 모두 새로 복사해주지는 않습니다. 예를 들어 state 안에 user.address.city 같은 구조가 있다면 바뀌는 단계마다 새 객체를 만들어 교체해야 합니다. 이 지점을 놓치면 분명 새 객체를 만든 것 같은데도 내부 데이터 수정 방식이 섞이면서 코드가 빠르게 복잡해집니다.

이전 값 기반 업데이트는 언제 필요할까

React useState 사용법: state와 객체 배열 업데이트 기준 적용 흐름을 설명하는 두 번째 본문 이미지

state를 다룰 때 한 번 더 헷갈리는 부분은 setter에 바로 값을 넣는 방식과 이전 값을 받아 계산하는 방식의 차이입니다. 둘 다 사용할 수 있지만이전 상태를 기준으로 다음 상태를 계산해야 한다면 함수 형태가 더 안전합니다.

이전 값 기반 업데이트 예시

import { useState } from 'react'; export default function TripleCounter() { const [count, setCount] = useState(0); const handleTriple = () => { // 이전 상태를 기준으로 차례대로 계산합니다. setCount((prev) => prev + 1); setCount((prev) => prev + 1); setCount((prev) => prev + 1); }; return ( <div> <p>현재 값: {count}</p> <button onClick={handleTriple}>3 증가</button> </div> );
}

이 방식은 특히 같은 이벤트 안에서 여러 번 업데이트할 때 중요합니다. state는 현재 렌더의 고정된 값처럼 동작하기 때문에, 단순히 count를 반복해서 사용하면 기대와 다른 결과가 나올 수 있습니다. 이전 값을 인자로 받아 계산하면 그 문제를 피할 수 있습니다.

정리하면, 단순히 새 값을 넣는 상황이라면 일반 형태로도 충분하지만이전 값에 의존하는 순간부터는 함수형 업데이트를 우선 기준으로 잡는 편이 더 안전합니다.

결론

state는 단순히 값을 저장하는 문법이 아니라 React가 화면을 다시 그릴 근거가 되는 값입니다. 그래서 화면에 영향을 주는 데이터는 state로 관리하고, 객체와 배열은 직접 수정하지 않고 새 값으로 교체해야 합니다. 또한 이전 값에 의존하는 업데이트라면 updater 함수를 사용해야 의도한 결과를 안정적으로 얻을 수 있습니다.

state로 관리한 값을 자식 컴포넌트에 넘기는 흐름은 React props 데이터 전달 글에서 이어서 확인할 수 있습니다.

많이 받는 질문

Q. 일반 변수와 state의 가장 큰 차이는 무엇인가요?
일반 변수는 값만 바뀌고 React 화면 갱신과 직접 연결되지 않습니다. state는 값이 바뀔 때 React가 다시 렌더링할 수 있도록 연결된 값입니다.

Q. 객체나 배열을 왜 직접 수정하면 안 되나요?
React에서는 state를 읽기 전용처럼 다루는 것이 기본입니다. 원본을 직접 수정하면 변경 흐름을 추적하기 어려워지고, 예상과 다른 렌더링 문제가 생길 수 있어서 새 객체나 새 배열을 만들어 교체하는 방식을 사용합니다.

Q. 언제 이전 값 기반 업데이트를 써야 하나요?
다음 값이 이전 state에 의존하면 사용하는 것이 맞습니다. 특히 같은 이벤트 안에서 여러 번 업데이트하거나 비동기 흐름에서 안전하게 계산해야 할 때 유용합니다.

Q. 를 호출했는데 바로 아래 console.log에는 왜 이전 값이 보이나요?
현재 실행 중인 이벤트 핸들러는 지금 렌더의 state 값을 보고 있기 때문입니다. setter는 값을 즉시 덮어쓰는 함수라기보다 다음 렌더에 반영할 업데이트를 예약하는 함수로 이해하는 편이 맞습니다.

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

“React useState 사용법: state와 객체 배열 업데이트 기준”에 대한 3개의 생각

댓글 남기기