React 개발을 할 땐 컴포넌트가 언마운트될 때의 메모리 누수나 성능 문제를 고려해야 한다. 특히 타이머나 이벤트 리스너를 사용할 때 이런 문제들이 자주 발생한다. 이런 상황에서 useEffect의 클린업 함수는 정말 중요한 역할을 한다. 오늘은 이 클린업 함수가 무엇인지, 왜 필요한지, 그리고 어떻게 효과적으로 사용할 수 있는지 알아보려고 한다.
❓ useEffect의 클린업 함수
- useEffect의 클린업 함수는 컴포넌트가 언마운트되거나 의존성 배열의 값이 변경되기 직전에 실행되는 함수다.
- 이 함수는 useEffect 내에서 return 문을 통해 정의되며, 이전에 설정한 구독, 타이머, 이벤트 리스너 등을 정리하는 역할을 담당한다.
- 요약하면 side effect를 정리하여, 메모리 누수를 방지하고 애플리케이션의 성능을 향상시키는 핵심적인 기능을 하는 함수이다.
▶ 기본 사용 예시
useEffect(() => {
// 이펙트 함수: 컴포넌트가 마운트되거나 의존성이 변경될 때 실행
console.log("이펙트 함수 실행 = 새로운 Effect 시작");
// 클린업 함수: 컴포넌트가 언마운트되거나 의존성이 변경되기 직전에 실행
return () => {
console.log("클린업 함수 실행 = 이전 Effect 종료");
};
}, [의존성배열]);
- 위 코드에서 클린업 함수는 useEffect의 반환값으로 정의되며, 컴포넌트의 생명주기에 따라 적절한 시점에 실행된다. 이를 통해 리소스를 안전하게 해제할 수 있다.
❗ 사용이유
✅ 메모리 누수 방지
- 타이머, 이벤트 리스너, 구독 등과 같은 비동기 작업은 컴포넌트가 언마운트된 후에도 계속 실행될 수 있어 메모리 누수를 발생시킬 수 있다.
- 클린업 함수를 통해 이러한 리소스를 적절히 해제함으로써 메모리 누수를 효과적으로 방지할 수 있다. 특히 SPA(Single Page Application)에서는 페이지 이동 시에도 이전 컴포넌트의 리소스가 남아있을 수 있기 때문에 더욱 중요하다.
✅ 성능 최적화 및 안정성 향상
- 불필요한 리소스 사용을 줄이고 애플리케이션의 성능을 향상시킨다.
- 컴포넌트가 리렌더링될 때마다 새로운 이벤트 리스너나 타이머가 중복 생성되는 것을 방지하여 시스템의 안정성을 높인다. 이는 특히 복잡한 애플리케이션에서 중요한 역할을 한다.
💥 주의
1️⃣ 클린업 함수의 실행 시점 정확히 이해하기
- 클린업 함수는 컴포넌트가 언마운트될 때뿐만 아니라, 의존성 배열의 값이 변경되어 useEffect가 다시 실행되기 직전에도 호출된다.
- 의존성 배열이 빈 배열([])일 경우, 클린업 함수는 컴포넌트가 언마운트될 때만 실행된다. 이 차이점을 명확히 이해하고 의존성 배열을 적절히 설정해야 한다.
2️⃣ 클로저 문제와 최신 상태 참조 주의하기
- 클린업 함수는 이전 렌더링의 값을 참조하므로, 최신 상태를 참조하려면 의존성 배열에 해당 값을 추가해야 한다.
- 클린업 함수가 바라보는 값은 해당 함수가 생성된 시점의 값이므로, 이를 고려하여 코드를 작성해야 예상치 못한 버그를 방지할 수 있다.
💠 예시
1️⃣ 타이머(setInterval) 정리
useEffect(() => {
const timer = setInterval(() => {
console.log('타이머 실행 중...');
}, 1000);
return () => {
clearInterval(timer); // 타이머 정리
console.log('타이머 정리 완료');
};
}, []);
- 이 예시에서는 컴포넌트가 마운트될 때 1초마다 실행되는 타이머를 설정하고, 언마운트될 때 클린업 함수를 통해 타이머를 정리한다. 만약 클린업 함수가 없다면 컴포넌트가 사라진 후에도 타이머가 계속 실행되어 메모리 누수가 발생할 수 있다.
2️⃣ 이벤트 리스너 제거
useEffect(() => {
const handleResize = () => {
console.log('창 크기가 변경되었습니다.');
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize); // 이벤트 리스너 제거
};
}, []);
- 이 예시에서는 컴포넌트가 마운트될 때 창 크기 변경 이벤트 리스너를 등록하고, 언마운트될 때 클린업 함수를 통해 이벤트 리스너를 제거한다. 이를 통해 불필요한 이벤트 리스너가 누적되는 것을 방지할 수 있다.
3️⃣ API 구독 취소
useEffect(() => {
const subscription = someAPI.subscribe();
return () => {
subscription.unsubscribe(); // API 구독 취소
};
}, [someAPI]);
- 이 예시에서는 컴포넌트가 마운트될 때 API를 구독하고, 언마운트될 때 클린업 함수를 통해 구독을 취소한다. 실시간 데이터를 다루는 애플리케이션에서 특히 중요한 패턴이다.
4️⃣ 비동기 작업 취소 플래그
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
if (isMounted) {
// 컴포넌트가 마운트된 상태일 때만 상태 업데이트
setData(data);
}
};
fetchData();
return () => {
isMounted = false; // 언마운트 시 플래그 변경
};
}, []);
- 이 예시에서는 컴포넌트가 마운트될 때 데이터를 가져오고, 언마운트된 후에는 상태 업데이트를 방지하기 위해 플래그 변수를 사용한다. 이는 비동기 작업이 완료되기 전에 컴포넌트가 언마운트되는 경우를 대비한 안전장치다. 모달을 열어서 데이터를 불러오는 중에 사용자가 모달을 닫거나 ESC 키를 누르는 경우가 대표적이다. 이때 모달 컴포넌트는 언마운트되지만 API 호출은 계속 진행된다.
🚩 실제 프로젝트 사례
const useDebounce = <T>(value: T, delay: number): T => {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
};
export default useDebounce;
디바운스 훅을 만들 때 TimeOut 함수를 정리하는 코드를 작성해 메모리 누수 및 성능 문제를 방지했다.
✅ setInterval과 setTimeout의 차이
- JavaScript의 setInterval과 setTimeout은 모두 특정 시간 후에 함수를 실행하는 타이밍 이벤트 함수이지만, 실행 방식에서 중요한 차이가 있다.
- setTimeout은 지정된 시간이 경과한 후에 한 번만 특정 함수를 실행한다. 반면 setInterval은 지정된 시간 간격으로 함수를 무한 반복 실행한다.
+ setInterval의 한계
- setInterval로 호출한 함수는 지연 시간 속에 함수 실행 시간도 포함된다.
- 함수 실행 시간이 명시한 지연 시간보다 길면, 함수 실행이 끝나자마자 바로 다음 호출이 시작된다.
- 실제 지연 간격이 예상보다 짧아질 수 있다.
- 중첩 setTimeout의 장점은 함수 실행이 끝난 후 다음 함수 호출 계획이 세워지므로 지연 시간이 보장된다.
- 따라서 더 정확한 시간 간격을 유지할 수 있다는 장점이 있다.
- 이렇듯 중첩 setTimeout은 setInterval보다 더 유연한 처리가 가능하다. 예를 들어, 서버 상태에 따라 요청 간격을 동적으로 조절할 수 있다.
// 중첩 setTimeout으로 setInterval과 같은 효과
let timerId = setTimeout(function tick() {
console.log("째깍");
timerId = setTimeout(tick, 2000);
}, 2000);
let delay = 5000;
let timer = setTimeout(function request() {
// 요청 보내기
if(서버_과부하_상태) {
delay *= 2; // 간격을 늘림
}
timerId = setTimeout(request, delay);
}, delay);
[참고]
https://jongminfire.dev/react-use-effect-%ED%9B%85-%EC%9D%B4%ED%8E%99%ED%8A%B8-%ED%95%A8%EC%88%98-%ED%81%B4%EB%A6%B0%EC%97%85-%ED%95%A8%EC%88%98
https://velog.io/@skyoffly/%EA%B0%9C%EB%B0%9C-%EC%A7%80%EC%8B%9D-useEffect%EC%99%80-%ED%81%B4%EB%A6%B0%EC%97%85clean-up-%ED%95%A8%EC%88%98
https://jjang-j.tistory.com/82
https://inseong1204.tistory.com/138
'React' 카테고리의 다른 글
[React] Redux와 Redux Toolkit (3) | 2025.07.04 |
---|---|
[React] 얕은 복사와 불변성 (2) | 2025.05.12 |
[React] 외부 클릭 감지 훅 만들기 (0) | 2025.05.04 |
[React] 이벤트 핸들러에 함수를 전달하는 방식 (0) | 2025.04.28 |
[React] 리액트의 폴더구조 (0) | 2025.03.31 |