| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 31 |
- 테스트자동화
- CSS
- list
- 자바
- 문자열
- junit5
- Eclipse
- vscode
- 스프링부트
- SpringBoot
- math
- string
- javascript
- input
- 배열
- 자바문법
- ArrayList
- html
- js
- Java
- 정규식
- 자바스크립트
- 인텔리제이
- 단위테스트
- IntelliJ
- HashMap
- junit
- java테스트
- Array
- Visual Studio Code
- Today
- Total
어제 오늘 내일
[React] useState 사용법 (개념, 흔히 하는 실수, 주의할 점) 본문
React로 웹 애플리케이션을 만들 때 가장 핵심이 되는 개념은 '상태(State)'입니다.
일반 자바스크립트 변수는 값이 바뀌어도 화면에 즉시 반영되지 않지만, React의 State 값이 변경되면 컴포넌트가 리렌더링(Re-render)되어 화면이 자동으로 업데이트됩니다.
이번 포스팅에서는 함수형 컴포넌트에서 상태를 관리하게 해주는 useState Hook의 기초 사용법과 실무에서 꼭 알고 있어야 할 주의사항 3가지를 알아보겠습니다.
1. useState 기본 사용법
useState는 호출 시 [현재 상태 값, 상태를 변경하는 함수] 형태의 배열을 반환합니다. 이를 구조 분해 할당(Destructuring)을 통해 가져와 사용합니다.
import React, { useState } from 'react';
function Counter() {
// useState(초기값): count 변수의 초기값을 0으로 설정합니다.
// count: 현재 상태 값
// setCount: count 상태를 변경하는 함수
const [count, setCount] = useState(0);
// 카운트 1 증가 핸들러
const handleIncrease = () => {
// setCount를 호출하여 상태를 변경하면 React가 컴포넌트를 리렌더링합니다.
setCount(count + 1);
};
// 카운트 1 감소 핸들러
const handleDecrease = () => {
setCount(count - 1);
};
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>현재 카운트: {count}</h2>
{/* 버튼 클릭 시 각각의 이벤트 핸들러가 실행됩니다. */}
<button onClick={handleIncrease}>+1 증가</button>
<button onClick={handleDecrease}>-1 감소</button>
</div>
);
}
export default Counter;
2. useState 사용 시 꼭 주의해야 할 점 3가지
useState는 단순해 보이지만, 동작 원리를 제대로 이해하지 못하면 예측하지 못한 버그가 발생할 수 있습니다.
① 상태 업데이트는 비동기적으로 처리됩니다 (함수형 업데이트)
React는 성능 최적화를 위해 여러 상태 변경 요청을 모아서 한 번에 처리(Batching)합니다.
따라서 상태 변경 함수를 호출한 직후, 동일한 상태를 연속해서 변경하려고 하면 기대했던 결과가 나오지 않을 수 있습니다.
❌ 잘못된 예시 (값 직접 전달)
import React, { useState } from 'react';
function BadCounter() {
const [count, setCount] = useState(0);
const handleTripleAdd = () => {
// count가 0인 상태에서 실행될 경우:
setCount(count + 1); // setCount(0 + 1) -> 1로 변경 요청
setCount(count + 1); // setCount(0 + 1) -> 여전히 1로 변경 요청
setCount(count + 1); // setCount(0 + 1) -> 여전히 1로 변경 요청
// 결과적으로 3이 더해지는 것이 아니라 1만 증가합니다.
// 비동기 처리 특성상 이 함수가 실행되는 동안 count는 여전히 0이기 때문입니다.
};
return <button onClick={handleTripleAdd}>+3 증가 (잘못된 예시)</button>;
}
✅ 올바른 작성법 (함수형 업데이트)
이전 상태 값을 기반으로 다음 상태를 계산할 때는 값이 아닌 콜백 함수를 넘겨주어야 합니다.
import React, { useState } from 'react';
function GoodCounter() {
const [count, setCount] = useState(0);
const handleTripleAdd = () => {
// 이전 상태(prev)를 인자로 받는 함수를 전달합니다.
// React는 이 함수들을 큐에 넣고 순서대로 최신 값을 적용합니다.
setCount((prev) => prev + 1); // prev = 0 -> 반환값 1
setCount((prev) => prev + 1); // prev = 1 -> 반환값 2
setCount((prev) => prev + 1); // prev = 2 -> 반환값 3
// 한 번의 클릭으로 정확히 3이 증가합니다!
};
return <button onClick={handleTripleAdd}>+3 증가 (올바른 예시)</button>;
}
② 객체와 배열은 '불변성(Immutability)'을 유지해야 합니다
React는 이전 상태와 다음 상태의 참조 주소(Reference)를 비교하여 변경 여부를 감지합니다.
객체나 배열 내부의 값을 직접 수정하면, 메모리 주소가 바뀌지 않아 React가 상태 변경을 감지하지 못하고 리렌더링을 건너뜁니다.
❌ 잘못된 예시 (원본 직접 수정)
const [user, setUser] = useState({ name: '홍길동', age: 20 });
const handleUpdateAge = () => {
// ❌ 객체의 속성을 직접 변경 (메모리 참조 주소는 동일함)
user.age = 21;
// ❌ 동일한 객체 주소를 전달했으므로 React는 변경 사항이 없다고 판단해 리렌더링하지 않음!
setUser(user);
};
✅ 올바른 작성법 (새로운 객체/배열 생성)
import React, { useState } from 'react';
function UserProfile() {
const [user, setUser] = useState({ name: '홍길동', age: 20 });
const handleUpdateAge = () => {
// ✅ 전개 연산자(...)를 사용해 기존 값을 복사하고, 새로운 객체를 만들어 전달합니다.
setUser((prevUser) => ({
...prevUser, // 기존 객체의 속성들을 복사 (name: '홍길동')
age: prevUser.age + 1, // age 속성만 새로운 값으로 덮어쓰기
}));
};
return (
<div>
<p>이름: {user.name}, 나이: {user.age}</p>
<button onClick={handleUpdateAge}>나이 +1</button>
</div>
);
}
배열의 경우에도 push, splice 같은 원본을 변형하는 메서드 대신, concat, filter, map, 또는 [...] 전개 연산자를 사용해야 합니다.
const [items, setItems] = useState(['사과', '바나나']);
const handleAddItem = () => {
// ✅ 불변성을 유지하며 새 요소 추가
setItems((prevItems) => [...prevItems, '포도']);
};
③ 무거운 연산이 포함된 초기값은 '게으른 초기화(Lazy Initialization)'를 사용하세요
useState(초기값) 형태는 컴포넌트가 리렌더링될 때마다 초기값 인자 계산식을 다시 호출합니다.
초기값 계산에 연산량이 많거나 localStorage 조회 같은 작업이 포함된다면 낭비가 발생합니다.
❌ 매 리렌더링마다 불필요한 연산 수행
// ❌ 컴포넌트가 리렌더링될 때마다 getExpensiveCalculatedValue() 함수가 계속 재실행됩니다.
// (실제 초기값은 첫 번째 렌더링에만 사용됨에도 불구하고 연산이 반복됨)
const [data, setData] = useState(getExpensiveCalculatedValue());
✅ 올바른 작성법 (게으른 초기화)
import React, { useState } from 'react';
// 연산 비용이 많이 드는 함수 예시
function getExpensiveCalculatedValue() {
console.log('복잡하고 무거운 계산 수행 중...');
return 10000;
}
function LazyInitExample() {
// ✅ 함수 자체를 콜백 형태로 전달하면 첫 렌더링 시에만 단 한 번 실행됩니다.
const [data, setData] = useState(() => getExpensiveCalculatedValue());
const [count, setCount] = useState(0);
return (
<div>
<p>계산된 초기값: {data}</p>
{/* 이 버튼을 눌러 리렌더링이 발생해도 무거운 계산 함수는 재실행되지 않습니다. */}
<button onClick={() => setCount(count + 1)}>리렌더링 유발 ({count})</button>
</div>
);
}
React 개발에서 useState의 이 세 가지 주의사항만 잘 지켜도 상태 관련 버그의 대부분을 예방할 수 있습니다!
'IT > React' 카테고리의 다른 글
| [React] props와 state 차이 이해하기, 상태 끌어올리기 (0) | 2026.08.02 |
|---|---|
| [React] useEffect 사용법 (개념, 의존성 배열, Clean-up 함수) (0) | 2026.08.02 |
| [React] AboutController로 비동기 처리하기 (0) | 2026.08.02 |
| React StrictMode 완벽 가이드: 왜 내 코드는 두 번 실행될까? (0) | 2026.08.02 |
| [React] Create React App (CRA) 으로 개발환경 구성하기 (1) | 2021.11.04 |