자바스크립트를 어느 정도 쓰다 보면 “클로저”라는 단어를 반드시 마주치게 되는데 막상 설명하려고 하면,
“함수가 스코프를 기억한다…?”라는 말에서 더 나아가기 쉽지 않습니다.
이 글에서는 클로저 정의 → 왜 생기는지 → 언제 문제가 되는지 → 언제 유용한지라는 흐름으로,
자바스크립트/타입스크립트 관점에서 정리 후 React 개발에 어떻게 적용되는지 살펴보겠습니다.
클로저를 한 문장으로 정의하면,
클로저는 함수가 자신이 선언된 렉시컬 스코프의 변수에 대해 실행이 끝난 이후에도 접근할 수 있는 특성입니다.
여기서 핵심 키워드는 함수, 렉시컬 스코프, 실행이 끝난 이후에도 인데,
클로저는 특별한 문법이 아니고 자바스크립트의 스코프 규칙이 그대로 적용된 자연스러운 결과입니다.
렉시컬 스코프부터 짚고 넘어가면,
자바스크립트는 렉시컬(정적) 스코프를 사용합니다. 즉, 함수가 어디서 호출되었는가 가 아니라 어디서 정의되었는 지로 스코프가 결정됩니다.
const x = 10;
function outer() {
const x = 20;
function inner() {
console.log(x);
}
return inner;
}
const fn = outer();
fn(); // 20
위 예시에서 inner 함수는 호출 위치와 상관없이, 자신이 정의된 위치의 x를 참조합니다.
이처럼 함수가 자신이 선언된 스코프의 변수에 접근할 수 있는 특성을 클로저라고 부릅니다.
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
위의 예시 코드를 봤을 때 createCounter 함수가 실행되고 나서도,
count 가 사라지지 않는 이유는 반환된 내부 함수가 count 변수를 참조하고 있기 때문입니다.
이때 count + 내부 함수 묶음이 클로저, 값이 아니라 변수에 대한 참조가 유지되어서 함수 호출 때마다 값이 누적되는 것입니다.
또한, 클로저는 값을 복사하는 것이 아니라 변수에 대한 참조를 유지하기 때문에, 외부 함수의 변수 값이 변경되면 클로저 내부에서 참조하는 값도 변경됩니다.
클로저는 주로 데이터 은닉과 상태 관리를 위해 의도적으로 사용됩니다. 예를 들어, 카운터 함수를 만들 때 외부에서 count 변수에 직접 접근하지 못하게 하여, 함수 내부에서만 상태를 변경하고 조회할 수 있도록 할 수 있습니다.
function createCounter() {
let count = 0;
return {
increment: function () {
count += 1;
return count;
},
getCount: function () {
return count;
},
};
}
React 함수 컴포넌트는 렌더링될 때마다 다시 실행되는 함수입니다.
아래 예시에서 중요한 사실은, 렌더링이 한 번 일어날 때마다 count 와 handleClick 함수가
새로운 스코프에서 다시 생성됩니다. 문제는 이전 렌더에서 만들어진 함수가 여전히 살아있을 수 있다는 것입니다.
이게 바로 React 에서 클로저 문제가 시작되는 지점입니다.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log(count);
};
return <button onClick={handleClick}>{count}</button>;
}
오래된 상태값을 잡고 있는 클로저를 stale closure 라고 부르는데,
stale closure 를 해결하는 대표적인 방법으론 setCount(prev => prev + 1) 와 같이
함수형 업데이트를 사용하는 방법이 있고, useEffect 의 의존성 배열에 상태값을 추가, useRef 로 최신값을 보관하는 방법이 있습니다.
// 예시 코드
import { useRef, useEffect } from 'react';
const MyComponent = ({ count }) => {
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
return (
<button onClick={() => console.log(countRef.current)}>
Log Count
</button>
);
}
React 의 이벤트 핸들러 역시 렌더 시점의 state 를 기억하는데,
대부분의 경우 문제는 없지만 window.addEventListener, document 이벤트,
cleanup 누락과 같은 상황에서 stale closure 문제가 발생할 수 있어 ref 패턴이나 의존성 관리가 필요합니다.
클로저는 자바스크립트의 고급 기능이 아니라, 스코프 규칙이 그대로 드러난 결과입니다.
클로저를 이해한다는 것은, 함수가 어떻게 실행되고 변수가 언제 살아 있고, 왜 어떤 값이 사라지지 않는지를 이해하는 것과 같습니다.
이는 프론트엔드 개발자라면 반드시 숙지해야 할 중요한 개념으로써,
비동기 코드가 덜 헷갈리고, 상태를 숨기는 설계가 자연스러워지며 '왜 이런 결과가 나왔는지'에 대한 질문에 더 쉽게 답할 수 있게 해줍니다.