[React]useState와 useEffect 가이드(Hook) 및 useState와 useEffect의 유의사항
![[React]useState와 useEffect 가이드(Hook) 및 useState와 useEffect의 유의사항](/content/images/size/w1920/2025/01/react_png_logo-1.png)
1. React 훅(Hook)이란?
React는 함수형 컴포넌트에서 상태(state)와 생명주기(lifecycle) 기능을 활용하기 위해 Hook이라는 개념을 도입했다.
useState
와 useEffect
는 React의 대표적인 훅으로, 각각 상태 관리와 부수 효과 관리를 담당한다. 최신 React(버전 18 이상)에서도 훅은 여전히 핵심이며, 컴포넌트 기반 개발에서 필수적으로 사용된다.
2. useState: 상태 관리의 핵심 🔄
2.1 useState란?
useState
는 React에서 컴포넌트 상태를 관리하기 위해 사용된다.
컴포넌트의 상태는 렌더링 중에 변경될 수 있는 데이터를 의미하며, React는 상태가 변경될 때마다 UI를 다시 렌더링한다.
2.2 기본 사용법
문법
const [state, setState] = useState(initialValue);
- state: 현재 상태 값.
- setState: 상태 값을 업데이트하는 함수.
- initialValue: 상태의 초기값.
예제
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // 초기값은 0
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
export default Counter;
2.3 상태 업데이트의 특징
- 비동기적 업데이트
setState
는 비동기적으로 동작한다. 즉, 업데이트는 즉시 반영되지 않을 수 있다.
const increment = () => {
setCount(count + 1);
console.log(count); // 이전 값 출력 (동기적 기대와 다름)
};
- 함수형 업데이트상태 변경에 이전 값을 활용해야 할 경우, 함수형 업데이트를 권장한다.
const increment = () => setCount((prevCount) => prevCount + 1);
- 병합되지 않음상태는 객체로 관리할 때 자동 병합되지 않는다. 직접 병합해야 한다.
const [state, setState] = useState({ name: '', age: 0 });
setState({ name: 'Austin' }); // age는 undefined로 덮어씌워짐
setState((prevState) => ({ ...prevState, name: 'Austin' })); // 해결
3. useEffect: 부수 효과 관리의 핵심 🌐
3.1 useEffect란?
useEffect
는 컴포넌트가 렌더링된 이후 또는 상태나 props가 변경된 이후에 실행되는 작업을 정의할 수 있다.
대표적으로 데이터 fetching, DOM 조작, 구독 설정/해제에 사용된다.
3.2 기본 사용법
문법
useEffect(() => {
// 실행할 작업
return () => {
// 정리(cleanup) 작업
};
}, [dependencies]);
- 첫 번째 매개변수: 실행할 함수.
- 두 번째 매개변수 (dependencies): 배열로 지정하며, 의존성이 변경될 때만 실행. 생략 시 매번 렌더링 후 실행.
예제 1: 의존성 없이 실행
import React, { useEffect } from 'react';
function App() {
useEffect(() => {
console.log('컴포넌트가 처음 렌더링되었습니다.');
}, []); // 빈 배열 -> 처음 한 번만 실행
return <h1>Hello, World!</h1>;
}
export default App;
예제 2: 의존성을 활용한 실행
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`현재 카운트는 ${count}입니다.`);
}, [count]); // count가 변경될 때만 실행
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
export default Counter;
예제 3: 정리 작업
컴포넌트가 언마운트될 때 실행할 작업(예: 구독 해제)을 정의할 수 있다.
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(interval); // 컴포넌트 언마운트 시 정리
};
}, []);
return <h1>{seconds}초 경과</h1>;
}
export default Timer;
3.3 의존성(dependencies) 배열
- 빈 배열 (
[]
)- 컴포넌트가 처음 렌더링될 때만 실행.
- 의존성 배열 생략
- 상태나 props가 변경될 때마다 실행.
- 특정 값 포함
- 배열에 포함된 값이 변경될 때만 실행.
4. useState와 useEffect를 함께 사용하기 🎯
실제 예제: 데이터 Fetching
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchUsers = async () => {
const response = await fetch('<https://jsonplaceholder.typicode.com/users>');
const data = await response.json();
setUsers(data);
setIsLoading(false);
};
fetchUsers();
}, []); // 의존성 없음 -> 처음 렌더링 시 실행
return (
<div>
{isLoading ? (
<h1>Loading...</h1>
) : (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}
export default UserList;
5.useEffect에서 useState를 사용하면 발생할 수 있는 문제점🪲
5.1 useState
로 상태를 업데이트하면 컴포넌트가 리렌더링됨
useEffect
내부에서 setState
를 호출하면 React는 해당 상태 업데이트를 반영하기 위해 컴포넌트를 리렌더링한다. 리렌더링 시 다시 useEffect
가 실행될 수 있으므로 무한 루프의 가능성이 있다.
예시
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('useEffect 실행');
setCount((prevCount) => prevCount + 1); // 상태 업데이트
}, []); // 의존성 배열에 아무 것도 없으므로 한 번만 실행
return <div>Count: {count}</div>;
}
결과
- 컴포넌트가 처음 렌더링될 때
useEffect
가 실행되고,setCount
를 호출한다. setCount
가 상태를 업데이트하면 리렌더링이 발생한다.useEffect
는 의존성 배열이 비어 있으므로 처음 렌더링 이후 다시 실행되지 않는다.- 무한 루프는 발생하지 않지만, 의도치 않게 상태가 업데이트될 수 있다.
5.2 의존성 배열을 잘못 설정하면 무한 루프 가능성🔁
useEffect
의 의존성 배열에 useState
로 설정된 상태를 포함하면, 해당 상태가 업데이트될 때마다 useEffect
가 실행된다. 만약 useEffect
내부에서 그 상태를 업데이트하면 무한 루프에 빠질 수 있다.
문제 예시
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Count 업데이트:', count);
setCount(count + 1); // 상태 업데이트
}, [count]); // count가 업데이트될 때마다 실행
return <div>Count: {count}</div>;
}
결과
count
가 변경되면useEffect
가 실행되고,setCount
를 호출한다.setCount
가 상태를 변경하면 다시useEffect
가 실행된다.- 이 과정이 반복되며 브라우저가 멈추거나, 메모리 초과로 크래시가 발생한다.
해결 방법
무한 루프를 방지하려면 의존성 배열을 신중히 설정하거나, 특정 조건에 따라 상태를 업데이트해야 한다.
useEffect(() => {
if (count < 5) { // 특정 조건
setCount(count + 1);
}
}, [count]);
5.3 상태 업데이트가 비동기적이므로 의도치 않은 동작 가능🚫
React의 상태 업데이트는 비동기적으로 처리된다. useEffect
가 실행되는 동안 상태 업데이트가 완료되지 않을 수 있다. 이를 고려하지 않으면 이전 상태값에 의존하는 문제가 생길 수 있다.
문제 예시
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('현재 카운트:', count); // 상태가 즉시 반영되지 않음
setCount(count + 1); // 의도한 값보다 더 커질 수 있음
}, []);
return <div>Count: {count}</div>;
}
해결 방법
상태 업데이트 시 이전 상태를 기반으로 업데이트하려면 콜백 형식을 사용하는 것이 안전하다.
setCount((prevCount) => prevCount + 1);
5.4 useEffect
의 의존성 배열이 올바르지 않으면 버그 발생 가능
useState
로 관리되는 상태를 useEffect
에서 사용하는데 의존성 배열에 포함하지 않으면, 오래된 상태를 참조하거나 예기치 않은 동작이 발생할 수 있다.
문제 예시
useEffect(() => {
console.log('현재 카운트:', count); // 오래된 값 참조 가능
}, []); // count가 의존성 배열에 없어서 업데이트되지 않음
해결 방법
의존성 배열에 상태를 포함하여 최신 값을 참조하도록 설정해야 한다.
useEffect(() => {
console.log('현재 카운트:', count);
}, [count]); // count가 변경될 때마다 최신 값으로 실행
5.5 비동기 작업과 상태 업데이트의 조합 문제
useEffect
에서 비동기 작업(예: API 호출)을 수행하고 그 결과로 상태를 업데이트할 때, 컴포넌트가 언마운트되면 상태 업데이트가 경고를 발생시킬 수 있다.
문제 예시
useEffect(() => {
fetch('/api/data')
.then((response) => response.json())
.then((data) => setState(data)); // 컴포넌트가 언마운트된 상태에서 업데이트 시 문제
}, []);
해결 방법
비동기 작업을 취소하거나, 상태 업데이트 여부를 컴포넌트의 마운트 상태에 따라 결정해야 한다.
useEffect(() => {
let isMounted = true;
fetch('/api/data')
.then((response) => response.json())
.then((data) => {
if (isMounted) setState(data);
});
return () => {
isMounted = false; // 언마운트 시 상태 플래그 변경
};
}, []);
6. 주요 차이점과 유의 사항
useState vs useEffect
useState
: 상태를 저장하고 업데이트하는 역할.useEffect
: 상태 변경이나 렌더링 후의 부수 효과를 처리.
유의사항
- 의존성 배열 관리: 불필요한 재실행 방지.
- 정리 작업 명시: 메모리 누수 방지를 위해 정리(cleanup) 필수.
- 비동기 작업:
useEffect
에서 비동기 함수는 내부에 정의하고 호출. - 무한루프:
useEffect
에서useState
를 사용할 때 리렌더링이 발생하며, 의존성 배열을 잘못 설정하면 무한 루프 문제가 생길 수 있다. - 의존성: 상태 업데이트는 비동기적으로 작동하므로 이전 상태에 의존하는 경우 주의해야 한다.
- 추가적인 처리 필요: 비동기 작업이나 외부 API 호출과 상태 업데이트를 결합할 때는 메모리 누수나 경고를 방지하기 위해 추가 처리가 필요하다.
7. 결론
useState
와 useEffect
는 React 컴포넌트의 상태와 생명주기를 효율적으로 관리하기 위한 핵심 훅이다.
useState
는 상태 관리를 단순화하고,useEffect
는 컴포넌트의 부수 효과를 선언적으로 정의할 수 있다.
댓글