React 컴포넌트는 생성되고, 변경되고, 제거되는 일련의 과정을 거칩니다. 이 과정을 컴포넌트 생명주기라고 합니다. 생명주기의 각 시점을 이해하면 데이터 패칭, 구독 설정, 리소스 해제 등을 올바른 시점에 처리할 수 있습니다.
+------------------+
| Mount | 컴포넌트가 DOM에 처음 추가됨
+------------------+
|
v
+------------------+
| Update | props 또는 state 변경으로 리렌더링됨
+------------------+
|
v
+------------------+
| Unmount | 컴포넌트가 DOM에서 제거됨
+------------------+
Mount(마운트): 컴포넌트가 처음으로 화면에 나타나는 단계입니다. 초기 렌더링이 완료되고 DOM이 준비된 시점으로, 외부 데이터 패칭이나 이벤트 리스너 등록을 시작하기에 적합합니다.
Update(업데이트): props나 state가 변경되어 컴포넌트가 다시 렌더링되는 단계입니다. 변경된 값에 따라 추가 작업이 필요할 때 처리합니다.
Unmount(언마운트): 컴포넌트가 화면에서 사라지는 단계입니다. 설정해 둔 타이머, 이벤트 리스너, 서버 구독 등 외부 리소스를 정리해야 합니다.
클래스 컴포넌트에서는 componentDidMount, componentDidUpdate, componentWillUnmount 메서드로 각 단계를 처리했습니다. 함수형 컴포넌트에서는 useEffect 하나로 세 단계를 모두 다룹니다.
useEffect(() => {
// Mount 시 실행 (의존성 배열이 빈 배열이면)
// Update 시 실행 (의존성 배열 값이 변경되면)
return () => {
// Unmount 시 실행 (Cleanup)
// 다음 Effect 실행 직전에도 실행
};
}, [의존성]);
마운트 시 한 번 실행:
useEffect(() => {
console.log("컴포넌트가 마운트됨");
fetchData();
}, []); // 빈 배열 = 마운트 시 한 번만
특정 값이 변경될 때마다 실행:
useEffect(() => {
console.log(`userId가 ${userId}로 변경됨`);
fetchUserData(userId);
}, [userId]); // userId 변경 시마다 실행
언마운트 시 정리:
useEffect(() => {
const subscription = subscribe(channel);
return () => {
subscription.unsubscribe(); // 언마운트 시 구독 해제
};
}, [channel]);
Cleanup 함수를 빠뜨리면 컴포넌트가 언마운트된 이후에도 타이머나 구독이 계속 실행됩니다. 이는 메모리 누수로 이어지고, 언마운트된 컴포넌트의 state를 업데이트하려 해서 오류가 발생할 수도 있습니다.
메모리 누수(Memory Leak): 더 이상 필요하지 않은 리소스가 해제되지 않고 메모리를 계속 점유하는 현상. 장시간 실행되는 앱에서 성능 저하를 유발함.
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
return () => {
clearInterval(intervalId); // Cleanup 필수
};
}, []);
return <p>{seconds}초</p>;
}
Cleanup 함수는 언마운트 시뿐만 아니라, 의존성이 변경되어 Effect가 다시 실행되기 직전에도 호출됩니다. 이를 통해 이전 Effect의 부작용이 새 Effect와 충돌하지 않도록 보장합니다.
Effect 실행 순서 (의존성 변경 시):
1. 이전 Effect의 Cleanup 실행
2. 새로운 Effect 실행
생명주기의 각 단계에 맞는 처리를 useEffect로 명확하게 작성하면, 컴포넌트의 동작을 예측하고 유지보수하기가 훨씬 쉬워집니다.