클로저는 JS 같은 함수형 프로그래밍 언어가 가지는 독특한 문법이다. 보통 다음을 만족하면 클로저라고 부르는 듯하다.
내부 함수가 외부 함수보다 더 오래 유지되고 있음에도 내부 함수가 외부 함수의 식별자를 기억하고 있을 때.
클로저가 JS의 특이한 문법이라는데, 처음 배울 때는 뭐가 특이한지 잘 몰랐다. 왜 특이한 문법이라고 하는지 아래 예시로 알아보자.
function outer() {
const x = 1;
const inner = function () {
console.log(x);
};
return inner;
}
const innerFunc = outer();
innerFunc(); //결과: 1
위 코드에서 inner는 클로저이다. 클로저가 특이한 이유는 일반적으로 외부 함수의 생명주기가 다하면, 내부 함수가 외부함수의 정보를 알 방법이 없기 때문이다.
일반적인 함수 실행 과정은 이렇다
1) innerFunc 할당과정에서 outer가 호출
2) 호출을 완료하고 inner를 반환
3) innerFunc()를 하면 console.log(x)가 실행
4) 하지만 이때 inner만 반환됐기 때문에 'const x= 1' 라는 정보는 같이 반환되지 않는다. 즉 console.log(x)를 할 때 x를 참조하는 값이 없어서 에러가 떠야한다.
하지만 JS는 x가 1이라는 것을 기억한다. 즉 outer 함수는 없어졌지만, outer가 가진 'const x = 1'이라는 정보를 inner함수가 기억하고 있다. 이 점이 바로 클로저가 특이한 문법이라고 불리는 이유다.
그럼 JS는 어떻게 이것을 가능하게 했을까?
결론부터 말하면 JS 엔진이 함수 객체 생성시 내부 슬롯 [[Environment]]에 현재 실행 컨텍스트의 Lexical Environment를 기록하면서 이를 가능하게 했다.
이 말을 이해하기 위해 실행 컨텍스트와 렉시컬 환경이 무엇인지 알아보자.
1️⃣ 실행 컨텍스트
function outer() {
const x = 1;
const inner = function () {
console.log(x);
};
return inner;
}
const innerFunc = outer();
innerFunc(); //결과: 1
JS엔진은 코드의 실행 순서를 콜 스택을 통해 제어할 수 있다. 함수가 실행되면 콜 스택에 해당 함수와 관련된 요소가 push되고, 실행이 끝나면 pop이 된다. 이때 콜 스택에 있는 내용을 맨 위의 요소부터 실행할텐데, 당연히 요소는 함수의 실행과 관련된 내용을 가지고 있어야한다. 이 요소의 이름이 바로 실행 컨텍스트이다.
함수의 코드를 실행하기 위해서는 변수, 함수, 클래스등 식별자 정보가 필요하다. 위의 outer 함수 예시에서 return inner를 하기 위해서는 inner가 무엇인지 알고 있어야하는 것처럼 말이다. 이를 위해 outer함수의 실행컨텍스트가 생성될 때, inner가 무엇인지 기록한다. 이 외에도 x가 1이라는 것도 기록되어 있다.
그런데 식별자가 함수 외부에 정의될 수도 있다. inner함수 안에서 호출하는 console.log(x)의 x가 바로 외부에 정의된 식별자다. 이 경우 x는 outer 함수의 실행컨텍스트에 저장되어 있다. 이를 inner 함수의 실행 컨텍스트가 실행 중일 때 알기 위해서는 outer 함수의 실행컨텍스트를 탐색할 수 있는 방법이 필요하다.
정리해보면 콜스택에 실행컨텍스트가 올라가면 해당 함수가 실행된다는 의미인데, 이때 실행 컨텍스트는 2가지를 필수로 해야한다.
1) 함수 내부에서 사용하는 식별자 정보를 가지고 있어야한다.
2) 외부 실행 컨텍스트를 탐색할 수 있어야 한다.
이 2가지를 가능하게 하는 것이 바로 실행 컨텍스트가 가지는 렉시컬 환경(Lexical Environment)이다. 전자의 기능은 렉시컬 환경의 '환경 레코드'로, 후자의 기능은 렉시컬 환경의 '외부 렉시컬 환경에 대한 참조'로 가능하다.
✅ 환경 레코드 (Environment Record)

JS 엔진은 소스코드를 한줄씩 실행하기 전에 평가 과정을 거친다. 이 평가 과정에서 실행 컨텍스트를 생성하고, 렉시컬 환경을 생성한다. 렉시컬 환경의 '환경 레코드'에는 함수 내부의 식별자와 값을 key-value형태로 저장한다. 평가 과정에서는 코드를 실행하기 전이기 때문에 key에 식별자만 두고, value는 다른 값으로 채워둔다.
변수 선언문과 매개변수는 key에는 식별자, value는 undefined 혹은 uninitialized된 상태로 둔다.
함수 선언문은 key에 함수 식별자, value에 힙 메모리에 저장한 함수 객체를 참조한다.
우리가 흔히 들어본 변수, 함수 호이스팅은 코드 평가 과정에서 렉시컬 환경에 key가 먼저 등록되기 때문에 일어난 것이다.

이후 코드를 한줄씩 실행하는 런타임에 value를 바꿔나가면서 환경 레코드를 완성한다.
✅ 외부 렉시컬 환경에 대한 참조 (Outer Lexical Environment Reference)
function A() {
const x = 1;
const B = function () {
console.log(x);
};
B();
}
코드 평가 과정에서 환경 레코드 생성 뿐만 아니라 '외부 렉시컬 환경에 대한 참조'를 결정한다. 위의 코드는 지금까지 본 예시와 살짝 다르다. 이 코드에는 클로저는 없다. 다만 외부 렉시컬 환경에 대한 참조가 무엇인지 설명하려고 변형한 예시이다.
A 함수를 실행한 상태에서 종료되지 않은 상황에서 B함수를 실행하면, JS엔진의 콜 스택에서는 A함수의 실행 컨텍스트가 있는 상황에서 inner함수의 실행 컨텍스트가 생성된다. 이 상황에서 B함수의 렉시컬 환경의 '외부 렉시컬 환경에 대한 참조'로 A함수의 렉시컬 스코프를 참조하게 된다.
B함수를 실행하면서 console.log(x)를 만난다면, '외부 렉시컬 환경에 대한 참조'로 A함수의 렉시컬 스코프가 있기 때문에 이를 활용해 A함수의 렉시컬 스코프의 환경 레코드에 있는 x를 가져올 수가 있게 된다.
이렇게 실행컨텍스트는 함수에 필요한 식별자가 내부에 정의되었다면 환경레코드로, 외부에 정의되었다면 '외부 렉시컬 환경에 대한 참조'에서 가져오면 된다.
2️⃣ 클로저
이제 실행컨텍스트를 활용하여 어떻게 JS가 클로저를 구현했는지 설명할 차례다. '외부 렉시컬 환경에 대한 참조'를 설명하면서 그냥 넘어간 부분이 있다. B함수가 '외부 렉시컬 환경에 대한 참조'로 A함수의 렉시컬 환경을 참조할 때, 어떻게 그것이 가능할까? 만약 이 내용을 구현한다면 A함수의 렉시컬 환경을 어디에 저장했다가 B의 '외부 렉시컬 환경에 대한 참조'에 할당해야할까?
JS는 이를 [[Environment]] 내부 슬롯에 저장해둔다. 내부슬롯은 JS 구현에 필요한 정보인데 개발자가 접근할 수 있는 프로퍼티는 아니지만 JS 엔진의 내부 로직에 필요한 값이다. 함수 객체도 여러 내부슬롯을 갖고 있는데, 이 중 [[Environment]]에 현재 실행중인 함수의 렉시컬 스코프를 담아 놓는다.

다시 Outer, Inner 예시로 돌아가보자. 즉 Outer 함수가 실행 중일때, Inner 함수의 객체가 생성된다. 그 객체의 [[Environment]] 내부슬롯에 외부함수의 렉시컬 환경을 참조해 놓는다. Inner 함수는 InnerFunc가 참조하고 있어서 GC의 대상이 아니다. 또한 inner함수가 Outer 렉시컬 환경 객체를 참조하는 중이라 GC의 대상이 아니다. 이제 Outer 실행이 종료되고 Inner가 실행될 때를 보자.

inner 함수가 실행 중일 때, 아직 Heap 메모리에 남아있는 inner함수 객체의 [[Environment]] 내부 슬롯을 활용할 수 있다. 여기에 담겨 있는 Outer함수의 렉시컬 환경을 inner 렉시컬 환경의 '외부 렉시컬 환경에 대한 참조'에 할당해주면 된다. 그래서 outer함수의 실행은 끝났지만 아직 GC의 대상이 되지 않은 outer 렉시컬 환경을 inner함수가 접근할 수 있는 것이다. 이게 JS가 클로저를 구현한 원리이다.
'개발' 카테고리의 다른 글
| openapi-typescript + Zod로 API 타입 빈틈 메우기 (0) | 2026.02.01 |
|---|---|
| React에 MVVM 아키텍처 적용하기 (1) | 2025.07.25 |
| WebRTC에 필요한 서버 4가지 (0) | 2025.04.28 |
| Spring Security + 세션 + 소셜로그인 (0) | 2025.01.08 |
| CORS 에러가 발생하는 과정 (0) | 2025.01.03 |