React 함수 state Closure 해결

Darcy Daeseok YU ·2026년 1월 16일

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]());
}
profile
React, React-Native https://darcyu83.netlify.app/

0개의 댓글