블로그 이미지
프론트엔드 개발도 하고 뛰기도 하고 이챙(leechaeng)
🐻 전체 : 오늘 : 어제 :

리액트에서 클로저가 어떻게 사용되고 있는가?

2025. 7. 17. 15:34react

클로저

함수와 그 함수가 선언될 당시의 렉시컬 환경(선언 됬을때의 렉시컬 환경)의 조합.
함수가 외부 스코프 변수를 참조 할 수 있고 외부 함수의 실행이 끝나도 변수에 접근 할 수 있다.

 

바닐라 JavaScript에서 클로저 이슈

for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}

반복문을 돌며 setTimeout 호출 시 콘솔에 i를 출력하는 예시 코드다.
결과가 어떻게 나올까?
클로저를 모르면 0,1,2가 출력된다고 볼 수 있다.
하지만 클로저는 외부 스코프의 변수를 참조하기 때문에 setTimeout 함수가 1초 후 실행됬을때는 i는 이미 3가 되어 콘솔에는 3이 출력된다.
var는 함수 스코프이기 때문에 반복문을 돌며 새로운 i를 생성하지 않고 하나의 i로 공유가 되기 때문이다.

 

리액트에서의 클로저

자바스크립트에서는 var를 사용할수도 있고 비동기 함수도 직관적으로 쓰기 때문에 코드 동작 결과를 보며 클로저다! 라고 느끼는 경우가 많았다. 그런데 리액트를 사용하다 보면 클로저 라는 개념 자체가 생각이 잘 안들었다.
그래서 리액트에서는 클로저가 안일어나는가?? 라는 생각을 해봤지만 말이 안되는 경우이고 (js로 사용하기 때문) 대부분 리액트는 훅을 사용하기 때문에 구조 자체에서 숨어서 일어나고 있었다.

1. useState의 클로저 캡처

const [count, setCount] = useState(0);

  const countIncrement = () => {
    setTimeout(() => {
      console.log(count); // 여전히 0
      setCount(count + 1); // 0 + 1 = 1
    }, 1000);
  };
  
  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={countIncrement}>증가</button>
    </div>
  );

useState는 리액트의 상태 매커니즘이다. 상태를 리액트가 관리하고 상태가 변경되면 컴포넌트는 리렌더링 된다.
이때 컴포넌트 전체가 다시 실행이 되지만 state는 이미 리액트가 기억하고 있어 리렌더링 이후에도 최신 상태값이 유지 된다.

하지만 함수 내부에서 상태값을 클로저를 캡쳐했을때 이슈가 있다.
countIncrement 함수가 생성 될때, 그 시점의 count 값을 클로저로 캡처한다. 이후 리렌더링이 되더라도 함수 내부에서는
이미 캡쳐된 0으로 사용하기 때문에 증가 버튼을 클릭해도 1로 변경된다

const [count, setCount] = useState(0);

  const countIncrement = () => {
    setTimeout(() => {
      setCount(prev => {
        return prev + 1; // 항상 최신 상태 기반
      });
    }, 1000);
  };
  
  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={countIncrement}>증가</button>
    </div>
  );

리액트가 setCount 함수의 콜백 호출시 내부적으로 현재 상태를 전달한다. prev는 항상 최신 상태를 유지하고 있는 것이다. 
함수형 업데이트를 통해 리액트가 관리를 해주니 우리는 클로저 이슈를 느끼기가 어렵다.

 


2. 리액트의 훅 사용

useEffect, useCallback 등 리액트에서 사용하는 훅들은 클로저 문제를 의존성 배열 기반으로 해결한다.
클로저라는 단어를 사용한다기 보단 의존성 배열을 바꾼다 라는 패턴으로 개발을 하기 때문에 클로저 관리를 하는지 모르고 넘어가는 경우가 있다.

useEffect(() => {
  const handleClick = () => {
    console.log(count); // 항상 최신 count
  };
  
  document.addEventListener('click', handleClick);
  return () => document.removeEventListener('click', handleClick);
}, [count]);


count를 의존성 배열에 넣어주면 클로저 관리를 useEffect가 알아서 관리해준다. 고로 우리는 신경쓸 필요가 없다는것. 

 

언제 주의를 해야 할까?

- 이전 상태값을 기반으로 업데이트 할때
- 비동기 콜백, 이벤트 핸들러 에서 상태를 참조 할때
- 같은 함수에서 여러 상태를 업데이트 하고 리액트가 이를 배치 처리 할때

이러한 이슈를 마주한다면 우리는 클로저를 마주하기 쉽기 때문에 클로저를 생각해야한다. useEffect나 함수형 업데이트가 오래된 상태를 해결 해 줄것이다.
함수형 업데이트와 의존성 관리 같은 패턴을 통해 리액트는 클로저를 자연스럽게 해결해준다. 우리는 리액트가 제공하는 패턴으로 인해 클로저라는 단어 자체를 잊어 버리고 사는 것 일 수도 있다. 이것이 리액트가 의도하는 추상화가 아닐까 라는 생각이 든다.

 

 

반응형