React State의 클로저 현상
리액트의 useState에서 발생하는 문제는 "함수가 생성되는 시점의 환경(스코프)을 기억한다"는 클로저의 기본 원리에서 정확히 일치합니다.
1.부모 컴포넌트의 함수를 자식에게 넘겼을 때 발생하는 클로저 현상은 "함수가 생성될 당시의 State 스냅샷을 들고 자식에게 내려간다"는 점 때문에 발생
예제
searchText를 [searchText]에 추가하면 텍스트변경시 함수가 새로 생성되면서 문제가 해결되긴한다. 하지만 자식컴포넌트가 렌더링되는 코스트가 크다면 성능문제로 이어질 수 있다. 또한 렌더링 타이밍에 따라 handleApplyFilter()함수가 제때 재생성되지 않아 stale searchText값을 가지는 경우도 존재한다.
import React, { useState, useCallback } from 'react';
// 자식 컴포넌트 (필터 버튼)
const FilterButton = React.memo(({ onApply }) => {
console.log("자식 버튼 렌더링됨");
return (
<TouchableOpacity onPress={onApply}>
<Text>현재 검색어로 필터 적용</Text>
</TouchableOpacity>
);
});
// 부모 컴포넌트
const Parent = () => {
const [searchText, setSearchText] = useState("");
// 문제의 부분: 의존성 배열이 비어있음 []
// 이 함수는 '최초 렌더링 시점'의 searchText(빈값)를 클로저로 가둡니다.
const handleApplyFilter = useCallback(() => {
console.log("API 호출 검색어:", searchText);
// searchText가 바뀌어도 이 콘솔에는 계속 ""만 찍힙니다.
}, []);
return (
<View>
<TextInput
value={searchText}
onChangeText={setSearchText}
placeholder="검색어를 입력하세요"
/>
<FilterButton onApply={handleApplyFilter} />
</View>
);
};
해결 useRef
1.불필요한 리렌더링 방지: 만약 의존성 배열을 쓰면 사용자가 글자를 한 글자 칠 때마다 handleApplyFilter가 새로 만들어지고, 이를 받는 자식(FilterIndicator 등)도 계속 다시 그려집니다. useRef를 쓰면 타이핑 중에는 자식이 꿈쩍도 안 하죠.
2.동기적 참조: useCallback 의존성 방식은 결국 "렌더링이 완료되어야" 새 함수가 나옵니다. 하지만 useRef는 렌더링과 상관없이 값이 할당되는 즉시 반영되므로, 사용자가 엄청나게 빠르게 조작할 때 발생하는 미세한 엇박자를 완벽히 방어합니다.
const Parent = () => {
const [searchText, setSearchText] = useState("");
const searchTextRef = useRef(""); // 최신 값을 담을 박스
const onChangeText = (text) => {
setSearchText(text);
searchTextRef.current = text; // 입력 즉시 Ref 업데이트
};
// 이제 의존성 배열이 비어있어도 안전합니다.
const handleApplyFilter = useCallback(() => {
// 함수가 만들어진 시점은 과거지만,
// searchTextRef라는 '박스'는 동일하므로 그 안의 최신값을 꺼내올 수 있습니다.
console.log("API 호출 최신 검색어:", searchTextRef.current);
}, []); // 함수 주소값이 변하지 않아 자식 리렌더링도 방지됨
return (
<View>
<TextInput value={searchText} onChangeText={onChangeText} />
<FilterButton onApply={handleApplyFilter} />
</View>
);
};
2.setTimeout, useEffect내에서 과거의 state를 참조할 때 발생합니다

클로저 정의
자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는데 이러한 함수를 클로저(Closure)라고 부르는 것이다.
function outerFunc() {
var x = 10;
var innerFunc = function () { console.log(x); };
innerFunc();
}
outerFunc(); // 10
function outerFunc() {
var x = 10;
var innerFunc = function () { console.log(x); };
return innerFunc;
}
/**
* 함수 outerFunc를 호출하면 내부 함수 innerFunc가 반환된다.
* 그리고 함수 outerFunc의 실행 컨텍스트는 소멸한다.
*/
var inner = outerFunc();
inner(); // 10
전역변수의 위험성을 해결하기 위한
function increase() {
// 카운트 상태를 유지하기 위한 지역 변수
var counter = 0;
return ++counter;
}
Button.onclick = function () {
count.innerHTML = increase(); // 무조건 1이 반환 된다
};
var increase = (function() { // (function() { ... })(); 즉시실행함수
var counter = 0;
return function(){ // 클로저
return ++counter;
}
})();
Button.onclick = function () {
count.innerHTML = increase(); // 1 .. 2 .. 3
};
클로저 실수
for 문에서 사용한 변수 i는 전역 변수이기 때문이다.
var arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = function () { return i; };
}
for (var j = 0; j < arr.length; j++) {
console.log(arr[j]()); // 5 5 5 5 5
}
해결
ar arr = [];
for (var i = 0; i < 5; i++){
arr[i] = (function (id) { // ②
return function () {
return id; // ③
};
}(i)); // ①
}
for (var j = 0; j < arr.length; j++) {
console.log(arr[j]());
}