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 멤버가 없던 시절에는 클로저로 객체의 내부 상태를 숨기는 패턴이 표준처럼 쓰였습니다.
Gain 클래스나 전역 상태 없이도 함수 하나에 private한 상태와 설정을 함께 묶어 둘 수 있어, 팩토리 함수와 콜백 설계가 간결해집니다. Cost 클로저가 큰 객체나 오래된 state를 붙잡으면 메모리가 예상보다 오래 살아남고, 특히 비동기 콜백이나 React 훅에서는 stale closure 버그가 생기기 쉽습니다. Decision Scale 짧은 범위의 설정값이나 인스턴스별 상태를 숨겨야 할 때는 클로저가 가장 가볍습니다. 반대로 여러 메서드가 같은 긴 수명의 상태를 공유해야 하거나, 무엇이 캡처되는지 팀이 자주 헷갈린다면 더 명시적인 구조가 낫습니다.
클로저는 React 훅, 이벤트 핸들러, setTimeout과 Promise 콜백, 미들웨어 체인, 모듈 패턴처럼 함수를 오래 들고 다니는 코드에서 끊임없이 등장합니다. useState가 돌려주는 setter도, useEffect 안의 함수가 당시 state를 기억하는 것도 전부 클로저 덕분입니다. 실무에서는 '이 함수가 지금 무엇을 붙잡고 있지?'를 의식하는 습관이 중요합니다. 특히 React에서는 의존성 배열을 놓치면 예전 값을 붙잡은 클로저(stale closure) 버그가 쉽게 생기기 때문입니다.