Closure
클로저는 함수가 만들어질 당시의 바깥 스코프를 함께 기억하는 구조입니다. 함수 안에서 바깥 변수를 참조하면, 바깥 함수가 이미 끝나 사라진 뒤에도 안쪽 함수는 그 변수를 계속 쓸 수 있습니다. '함수 + 그 함수가 자라난 환경'이 한 덩어리로 묶여 다니는 셈입니다. 자바스크립트의 스코프와 함수 반환 구조에서 자연스럽게 생기는 개념이라 별도의 문법이 없는 언어도 많습니다.
▶아키텍처 다이어그램
🔍 구조 다이어그램점선 애니메이션은 데이터 또는 요청의 흐름 방향을 나타냅니다
함수를 반환하는 고차 함수를 쓰다 보면 이상한 순간이 옵니다. 바깥 함수는 이미 호출이 끝나서 지역 변수가 전부 사라졌을 텐데, 반환된 함수는 그 변수를 여전히 기억합니다. 직관적으로는 '함수가 끝나면 스택이 정리된다'고 배웠는데, 반환된 함수가 과거 값을 들고 다니는 현상은 이 상식과 어긋나 보입니다. 이벤트 핸들러나 비동기 콜백에서 'i가 왜 마지막 값만 찍히지?' 같은 버그를 만나는 것도 이 지점입니다. 클로저가 어떻게 변수를 붙잡는지 이해하지 못하면 고차 함수와 비동기 코드에서 예측 불가능한 동작을 계속 마주치게 됩니다.
클로저는 1960년대 Scheme과 Lisp 계열에서 '함수를 값으로 다루려면 그 함수가 참조하는 바깥 변수도 함께 붙잡아야 한다'는 요구에서 나왔습니다. 함수를 반환해서 나중에 호출할 때 그 함수가 생성되던 시점의 환경이 유지되지 않으면, 함수가 진짜 값으로 다뤄진다고 말하기 어렵기 때문입니다. 이후 일급 함수를 지원하는 거의 모든 모던 언어가 클로저를 기본 동작으로 포함했고, 자바스크립트에서는 특히 콜백과 비동기 처리가 일상화되면서 클로저를 이해하는 것이 실무의 필수 조건이 됐습니다. React 훅이 전면에 등장한 뒤로는 '렌더링마다 새로 만들어지는 함수가 그 시점 state를 클로저로 붙잡는다'는 감각 없이 useEffect의 동작을 설명하기 어렵습니다.
클로저는 함수를 정의할 때 '이 함수가 참조하는 바깥 변수가 무엇인가'를 기록해 두는 것입니다. 함수가 호출되면 자신의 지역 변수뿐 아니라 정의되던 시점의 바깥 스코프도 같이 올려다봅니다. 바깥 함수가 이미 리턴되어 호출 스택에서는 사라졌지만, 반환된 안쪽 함수가 그 변수를 참조하고 있으면 자바스크립트 엔진은 그 변수를 힙에 유지합니다. 덕분에 바깥 함수의 지역 변수가 안쪽 함수의 '사적인 저장소' 역할을 하게 됩니다. 같은 함수 팩토리를 여러 번 호출하면 호출마다 새로운 스코프가 만들어지므로, 반환된 함수들끼리는 서로의 변수를 공유하지 않고 각자 독립된 사본을 들고 다닙니다.
카운터로 보는 클로저
function makeCounter() {
let count = 0; // 바깥 함수의 지역 변수
return function() {
count += 1; // 바깥 스코프의 count를 기억
return count;
};
}
const counter1 = makeCounter();
counter1(); // 1
counter1(); // 2
counter1(); // 3
const counter2 = makeCounter(); // 새로운 스코프
counter2(); // 1 (counter1과 독립된 count)
counter1(); // 4 (counter1의 count는 그대로)makeCounter가 끝나도 counter1은 count 변수를 계속 참조합니다. 새로 만든 counter2는 별도 스코프를 가지므로 counter1과 값을 공유하지 않습니다. 함수마다 자기만의 count를 들고 다니는 것이 클로저의 핵심입니다.
클로저와 스코프는 비슷해 보이지만 층위가 다릅니다. 스코프는 '코드의 어느 영역에서 어떤 변수가 보이는가'라는 정적 규칙이고, 클로저는 '함수가 생성된 시점의 스코프를 런타임에도 계속 붙잡고 있는' 동적 현상입니다. 스코프는 컴파일 시점의 개념이고, 클로저는 함수가 값으로 돌아다닐 때 드러나는 실행 시점의 개념입니다. 클로저와 객체도 자주 비교됩니다. 둘 다 '상태와 동작을 함께 묶어 다닌다'는 점에서 비슷하지만, 객체는 메서드와 필드를 명시적으로 선언하는 구조이고 클로저는 함수가 주변 변수를 암묵적으로 붙잡는 구조입니다. 자바스크립트에서 private 멤버가 없던 시절에는 클로저로 객체의 내부 상태를 숨기는 패턴이 표준처럼 쓰였습니다.
클로저는 React 훅, 이벤트 핸들러, setTimeout/Promise 같은 비동기 콜백, 미들웨어 체인, 모듈 패턴에서 쉬지 않고 등장합니다. React의 useState가 반환하는 setter 함수도, useEffect 안에 작성한 함수가 state 변수를 '기억'하는 방식도 전부 클로저의 동작입니다. 실무에서는 '이 함수가 무엇을 붙잡고 있는가'를 의식하는 습관이 중요합니다. 특히 React에서 의존성 배열을 빠뜨리면 클로저가 과거 값을 계속 들고 있어 최신 state가 반영되지 않는 stale closure 버그가 생깁니다. 클로저는 '이해'보다 '언제 붙잡히는지 감'이 중요한 개념이라, 실제 버그를 만나고 고치면서 손에 익히는 것이 제일 빠릅니다.