✨ 클로저 트랩이란(Closure Trap)?
"stale closure" (오래된 클로저) 문제라고도 하며, 함수형 컴포넌트에서 useEffect, useCallback, useMemo 등의 훅을 사용할 때, 이전의 상태(state)나 props가 의도치 않게 캡처되는 문제를 의미한다. 특히 useEffect 내부에서 이벤트 리스너를 추가할 때, 오래된 상태를 참조하는 경우가 많다.
✨ 클로저 트랩이 발생하는 이유
자바스크립트의 클로저(Closure)는 외부 변수의 상태를 기억하는 함수이다. React에서는 렌더링될 때마다 함수형 컴포넌트가 다시 실행되는데, 이 과정에서 클로저가 이전의 상태(state)나 props를 캡처하면 예상과 다른 동작이 발생할 수 있다.
⚠️ 문제 발생 예제 1 - useEffect에서 상태 갱신이 안되는 경우
import { useState, useEffect } from "react";
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
console.log("Count:", count); // 항상 0만 출력됨
setCount(count + 1); // 의도대로 증가하지 않음
}, 1000);
return () => clearInterval(interval); // unMount시 이벤트 정리
}, []); // 빈 의존성 배열로 인해 초기 count만 사용됨
return <h1>{count}</h1>;
}
export default Counter;
🔍 문제점 분석
- useEffect는 초기 마운트 시에만 실행되며 내부의 setInterval에서 count 값을 참조한다.
- setInterval 내의 count는 초기값(0)만을 캡처하고, 이후 상태가 업데이트되지 않는다.
- 따라서 setCount(count + 1)이 항상 setCount(0 + 1)이 되므로 항상 1만 반복된다.
setInterval()
각 호출 사이에 고정된 시간 지연으로 함수를 반복적으로 호출하거나 코드 스니펫을 실행
🔗 https://developer.mozilla.org/ko/docs/Web/API/Window/setInterval
👍 해결방법 1 - 최신 상태를 가져오도록 useEffect 의존성 배열 수정
useEffect(() => {
const interval = setInterval(() => {
console.log("Count:", count); // 최신 count를 사용
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, [count]); // ✅ count가 변경될 때마다 새로운 useEffect 실행
하지만 이 방법은 useEffect가 매번 실행되면서 setInterval이 재설정되는 문제가 있다.
👍 해결방법 2 - setState의 함수형 업데이트 사용
setState는 콜백 함수 형태로 값을 설정할 수 있으며, 이때 이전 상태를 안전하게 가져올 수 있다.
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1); // ✅ 최신 상태 사용
}, 1000);
return () => clearInterval(interval);
}, []); // ✅ 빈 배열로 두어도 최신 count를 가져올 수 있음
🔍 왜 해결될까?
- setCount(prevCount => prevCount + 1)에서 prevCount는 React가 제공하는 최신 상태 값을 받는다.
- 따라서 setInterval이 오래된 count 값을 캡처하더라도 항상 최신 상태가 보장된다.
⚠️ 문제 발생 예제 2 - useCallback이 오래된 props를 캡처하는 경우
useCallback이란?
- useCallback은 리렌더링 간에 함수 정의를 캐싱해 주는 React Hook이다.
- 불필요한 함수 재생성을 방지하고, props로 전달되는 함수가 변경되지 않도록 보장하는 것이 주된 목적이다.
- 가능한 경우 기존 함수를 재사용하여 불필요한 렌더링을 방지하는 것이다.
🔗 https://ko.react.dev/reference/react/useCallback
import { useState, useCallback } from "react";
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("Hello");
const handleClick = useCallback(() => {
console.log("Text:", text); // 항상 초기 text("Hello")만 출력됨
}, []); // 빈 의존성 배열 -> 최초 text만 캡처됨
return (
<div>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
<button onClick={handleClick}>Show Text</button>
</div>
);
}
🔍 문제점
- handleClick 함수는 text의 최초 상태만을 캡처한다.
- setText를 변경해도 useCallback의 의존성 배열이 []이므로 최신 text가 반영되지 않음.
👍 해결 방법: useCallback의 의존성 배열에 text 추가
const handleClick = useCallback(() => {
console.log("Text:", text); // ✅ 최신 text 값 사용
}, [text]); // ✅ text가 변경될 때마다 handleClick 갱신
이렇게 하면 text가 변경될 때마다 새로운 handleClick 함수가 생성되어 최신 상태를 유지할 수 있다.
❓ useCallback 메모이제이션에 대한 의문
- handleClick 함수의 의존성에 추가된 text가 바뀔 때마다 새로운 함수가 생성되므로 handleClick이 자주 변경될 수 있기에 useCallback의 메모이제이션 목적과 일부 상충된다.
- 하지만 아래와 같은 경우 여전히 도움이 되며, 메모이제이션 자체에도 비용이 있기 때문에 필요한 경우에만 사용하는 것이 좋다.
- 함수를 자식 컴포넌트에 props로 전달하는 경우 (특히 React.memo 사용 시)
- 함수가 의존성 변경이 적은 상태에서 최적화가 필요한 경우
🔰 결론 - 클로저 트랩 피하기
- useEffect의 의존성 배열을 신중하게 관리
- 의존성을 명확하게 추가하지 않으면 stale closure 문제가 발생할 수 있음.
- useEffect 내부에서 최신 상태를 유지하려면 의존성을 추가하거나, 함수형 업데이트 (setState(prev => ...))를 사용해야 한다.
- setState의 함수형 업데이트 활용
- setState(prevState => newState) 형태를 사용하면 항상 최신 상태를 반영할 수 있다.
- useCallback, useMemo의 의존성 배열 관리
- 빈 배열 ([])을 사용하면 최초 상태만 캡처되므로, 최신 값이 필요하면 적절한 의존성을 추가해야 한다.
발생 원인 | 해결 방법 |
useEffect 내부에서 초기 상태만 캡처 | 의존성 배열에 상태 추가 or 함수형 업데이트 사용 |
useCallback이 최신 props/state를 반영하지 못함 | 필요한 의존성을 추가 |
setInterval, setTimeout 내부에서 오래된 상태 캡처 | setState(prev => prev + 1) 형태 사용 |
참고
'React' 카테고리의 다른 글
[React] Vite에서 font 설정하기 (with. Styled-Components) (0) | 2025.03.05 |
---|---|
[Vite] 절대경로 설정하기 (0) | 2025.01.20 |
[React] Stackflow로 모바일 웹뷰 Routing 구현하기 (1) | 2024.12.20 |
[React] input 태그의 왼쪽 0 제거하기 (0) | 2024.12.05 |
[React] React Mobile Picker로 날짜 선택하기 (0) | 2024.12.05 |