React의 useState 훅: 상태 관리, 불변성 유지, 그리고 prevState 활용법
useState와 일반 변수(let)의 차이점
React에서 상태 관리를 위해 사용하는 useState 훅은 컴포넌트의 상태 변화를 관리하고 화면에 자동으로 반영해 주는 중요한 도구입니다. 반면, 일반 자바스크립트 변수(let)는 상태를 저장할 수 있지만, 그 값이 변경되더라도 React는 이를 감지하지 않으므로 화면에 업데이트가 이루어지지 않습니다. 이 글에서는 useState와 일반 변수의 차이점을 비교하며, 버튼 클릭 시 카운트가 증가하는 예제를 통해 그 차이를 살펴보겠습니다.
useState와 let 변수의 기본 개념
- useState: React에서 제공하는 훅(Hook)으로, 컴포넌트의 상태를 선언하고 그 상태가 변경될 때마다 컴포넌트를 다시 렌더링합니다. 이 상태는 컴포넌트가 다시 렌더링될 때 유지됩니다.
- let 변수: 자바스크립트의 변수 선언 방식 중 하나로, 블록 스코프를 가지며 값이 변경될 수 있습니다. 하지만 이 변수는 React의 상태 관리와는 무관하며, 값이 변경되더라도 컴포넌트가 다시 렌더링되지 않습니다.
예제 코드
아래는 두 가지 방법으로 버튼 클릭 시 카운트를 증가시키는 예제입니다. 하나는 let 변수를 사용하고, 다른 하나는 useState를 사용합니다.
1. let 변수를 사용한 예제
import React from 'react';
function CounterWithLet() {
let num = 0;
const incrementNum = () => {
num += 1;
console.log(num); // 콘솔에선 업데이트된 num이 보이지만, 화면에는 반영되지 않음
};
return (
<div>
<h2>Counter with let: {num}</h2>
<button onClick={incrementNum}>Increment</button>
</div>
);
}
export default CounterWithLet;
이 예제에서 num 변수는 버튼 클릭 시 값이 증가하지만, 화면에는 변경 사항이 반영되지 않습니다. 그 이유는 React가 num 변수의 변화를 감지하지 못하기 때문입니다.
2. useState를 사용한 예제
import React, { useState } from 'react';
function CounterWithState() {
const [count, setCount] = useState(0);
const incrementCount = () => {
setCount(count + 1);
};
return (
<div>
<h2>Counter with useState: {count}</h2>
<button onClick={incrementCount}>Increment</button>
</div>
);
}
export default CounterWithState;
이 예제에서는 useState를 사용하여 count 상태를 관리합니다. 버튼을 클릭할 때마다 setCount를 통해 상태가 업데이트되며, React는 이 상태 변화를 감지하여 컴포넌트를 다시 렌더링합니다. 그 결과, 화면에 새로운 count 값이 반영됩니다.
setCount를 연속으로 호출할 때의 동작
만약 setCount를 연속으로 5번 호출한다고 가정해 보겠습니다. 예를 들어, 아래와 같이 코드를 작성하면:
const incrementCount = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
이 경우, count가 5번 증가하는 것이 아니라 단지 1번 증가하는 결과를 보게 됩니다. 그 이유는 setCount가 비동기적으로 동작하며, 상태가 즉시 업데이트되지 않기 때문입니다. setCount는 현재의 count 값을 참조하여 증가시킨 후, 리렌더링이 발생하지만, 연속된 호출에서는 첫 번째 setCount 이후의 상태가 반영되지 않은 채로 동일한 값을 사용하게 됩니다.
prevState를 사용한 해결 방법
이 문제를 해결하기 위해, setCount는 이전 상태 값을 기반으로 업데이트할 수 있는 콜백 함수를 지원합니다. 이 방법을 사용하면, prevState라는 변수를 이용해 정확한 상태 변경을 할 수 있습니다.
const incrementCount = () => {
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);
};
이 코드에서는 prevCount라는 이전 상태 값을 이용해 상태를 업데이트합니다. 이렇게 하면, setCount가 호출될 때마다 항상 최신의 상태 값이 반영되어 5번 호출할 경우 count가 5 증가하게 됩니다.
상태 불변성 유지와 객체 상태 관리
React에서 useState를 사용할 때, 특히 객체 상태를 관리하는 경우 상태의 불변성을 유지하는 것이 중요합니다. 불변성이란 상태를 직접 수정하지 않고, 기존 상태를 복사한 뒤 변경 사항을 적용한 새로운 상태를 생성하는 것을 의미합니다. 이는 React가 상태 변화를 감지하고, 컴포넌트를 효율적으로 업데이트할 수 있게 해줍니다.
예를 들어, 상태로 객체를 관리할 때 다음과 같이 불변성을 유지해야 합니다:
const [person, setPerson] = useState({
name: 'John',
age: 30,
mentor: {
name: 'Doe',
age: 40,
}
});
const updateMentorName = (newName) => {
setPerson(prevPerson => ({
...prevPerson,
mentor: {
...prevPerson.mentor,
name: newName,
},
}));
};
위 코드에서 setPerson은 이전 상태(prevPerson)를 복사한 뒤, mentor 객체의 name만 업데이트합니다. 이렇게 하면 React는 상태가 변경된 것을 감지하고, 컴포넌트를 다시 렌더링합니다.
React의 useEffect 훅: 데이터 페칭, 무한 루프 방지, 의존성 배열, 그리고 클린업 처리
React의 useEffect 훅은 컴포넌트의 생명주기에 따라 특정 작업을 수행할 수 있게 해주는 중요한 도구입니다. 주로 API 호출, 구독 설정, 타이머 시작과 같은 부수 효과(side effects)를 처리할 때 사용됩니다. 이 글에서는 useEffect의 기본 개념과 함께, API를 fetch로 호출하는 예제, 그리고 useEffect를 잘못 사용했을 때 발생할 수 있는 무한 루프 상황을 다루겠습니다. 또한, useEffect의 두 번째 인자와 return을 사용하여 클린업을 처리하는 방법, 그리고 의존성 배열에 상태 변수를 포함시킬 때의 동작을 설명하겠습니다.
useEffect의 기본 개념
useEffect는 함수 컴포넌트에서 부수 효과를 수행할 수 있도록 하는 React 훅입니다. 다음과 같은 상황에서 유용합니다:
- 컴포넌트가 마운트되거나 업데이트될 때 데이터를 가져오기
- 구독(subscribe) 설정
- 타이머 시작 및 정지
- 컴포넌트가 언마운트될 때 리소스를 정리(clean up)
useEffect의 기본 사용법
useEffect는 다음과 같이 사용할 수 있습니다:
import React, { useState, useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// 여기에 부수 효과 코드를 작성합니다.
}, []); // 두 번째 인자: 의존성 배열
}
위의 예제에서, useEffect는 컴포넌트가 마운트될 때 한 번 실행됩니다. 두 번째 인자로 빈 배열([])을 전달하면, 이 효과는 컴포넌트가 처음 렌더링될 때 한 번만 실행됩니다.
fetch로 데이터 가져오기 예제
다음은 fetch를 사용하여 데이터를 가져오는 간단한 예제입니다. 이 예제에서는 useEffect를 사용하여 컴포넌트가 마운트될 때 API 호출을 수행합니다.
import React, { useState, useEffect } from 'react';
function FetchData() {
const [data, setData] = useState(null);
useEffect(() => {
// 컴포넌트가 마운트될 때 데이터 페칭
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(json => setData(json));
}, []); // 빈 배열로 의존성을 설정하여 마운트 시 한 번만 실행
return (
<div>
{data ? (
<div>
<h1>{data.title}</h1>
<p>{data.body}</p>
</div>
) : (
<p>Loading...</p>
)}
</div>
);
}
export default FetchData;
useEffect를 사용하지 않고 데이터 페칭 시 발생하는 문제
만약 useEffect를 사용하지 않고 fetch를 함수 컴포넌트 내부에서 직접 호출하면, 다음과 같이 무한 루프에 빠질 수 있습니다:
import React, { useState } from 'react';
function FetchDataWithoutEffect() {
const [data, setData] = useState(null);
// 잘못된 방법: 매번 렌더링할 때마다 fetch가 호출됨
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(json => setData(json));
return (
<div>
{data ? (
<div>
<h1>{data.title}</h1>
<p>{data.body}</p>
</div>
) : (
<p>Loading...</p>
)}
</div>
);
}
export default FetchDataWithoutEffect;
위 예제는 fetch가 컴포넌트가 렌더링될 때마다 호출됩니다. setData가 상태를 변경하면 컴포넌트가 다시 렌더링되고, 다시 fetch가 호출되어 상태가 업데이트되는 무한 루프가 발생합니다. 이 문제는 useEffect를 사용하여 데이터 페칭을 한 번만 수행하도록 설정함으로써 해결할 수 있습니다.
useEffect의 두 번째 인자: 의존성 배열
useEffect의 두 번째 인자는 의존성 배열로, 이 배열에 포함된 값이 변경될 때마다 useEffect가 실행됩니다. 예를 들어, 아래 코드는 count 값이 변경될 때마다 useEffect가 실행됩니다.
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Count has changed:', count);
}, [count]); // count가 변경될 때마다 실행
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Counter;
이 경우, count가 업데이트될 때마다 useEffect가 다시 실행되며, 이로 인해 console.log가 호출됩니다. 즉, 의존성 배열에 포함된 값이 변경될 때마다 해당 useEffect가 실행되는 것입니다. 만약 의존성 배열을 빈 배열([])로 설정하면, useEffect는 컴포넌트가 처음 마운트될 때 한 번만 실행됩니다.
useEffect의 반환값: 클린업 함수
useEffect는 선택적으로 클린업 함수를 반환할 수 있습니다. 이 함수는 컴포넌트가 언마운트될 때 또는 의존성이 변경될 때 실행됩니다. 이 기능은 구독을 해제하거나 타이머를 정리하는 등의 작업에 유용합니다.
import React, { useState, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// 클린업 함수: 컴포넌트 언마운트 시 타이머 해제
return () => clearInterval(interval);
}, []); // 빈 배열로 한 번만 실행
return (
<div>
<p>Count: {count}</p>
</div>
);
}
export default Timer;
이 예제에서 setInterval로 타이머를 시작하지만, 컴포넌트가 언마운트될 때 clearInterval을 호출하여 타이머를 해제합니다. 이렇게 하면 메모리 누수나 불필요한 동작을 방지할 수 있습니다.
useEffect의 마운트와 언마운트 개념
- 마운트(Mount): 컴포넌트가 처음으로 DOM에 삽입될 때를 의미합니다. 이때 useEffect는 의존성 배열에 관계없이 한 번 실행됩니다.
- 언마운트(Unmount): 컴포넌트가 DOM에서 제거될 때를 의미합니다. useEffect의 클린업 함수는 이 시점에 실행되어 리소스를 정리합니다.
결론
React에서 상태 관리의 핵심은 useState 훅을 사용하여 컴포넌트의 상태 변화를 효과적으로 처리하는 것입니다. 일반 변수(let)와 달리 useState는 상태가 변경될 때마다 UI를 자동으로 업데이트해 주므로, 상태를 관리하는 가장 기본적인 방법입니다. 또한, 상태를 연속으로 업데이트할 때는 prevState를 활용하여 정확한 상태 변화를 반영할 수 있습니다.
useState를 올바르게 사용하면 React 컴포넌트에서 상태 관리를 보다 쉽게 처리할 수 있으며, 이를 통해 동적인 UI를 구현할 수 있습니다.
useEffect는 React 컴포넌트에서 부수 효과를 관리하는 데 필수적인 훅입니다. useEffect를 올바르게 사용하면, 컴포넌트의 생명주기에 따라 데이터를 가져오고, 구독을 설정하고, 리소스를 정리하는 작업을 효과적으로 처리할 수 있습니다. 특히, fetch와 같은 비동기 작업을 수행할 때는 useEffect를 사용하여 무한 루프를 방지하고, 컴포넌트의 상태를 적절하게 관리할 수 있습니다.
'React > 정리' 카테고리의 다른 글
리액트 전역 상태 관리: useContext 훅 (0) | 2024.08.12 |
---|---|
리액트 상태 관리: useReducer, useImmer 알아보기 (0) | 2024.08.12 |
JSX 문법 (0) | 2024.08.11 |
Create React App (0) | 2024.08.11 |
React 란? (0) | 2024.08.11 |