클로저(Closure)는 “함수와 그 함수가 선언될 당시의 어휘적 환경(Lexical Environment)의 조합”이다.
조금 어렵게 느껴질 수 있지만, 핵심은 “함수가 선언될 때의 스코프를 기억한다” 는 것이다.
즉, 내부 함수가 외부 함수의 변수에 접근할 수 있는 능력을 말한다.
function makeFunc() {
const name = "Mozilla";
function displayName() {
console.log(name);
}
return displayName;
}
const myFunc = makeFunc();
myFunc(); // Mozilla
makeFunc()이 실행된 후라면, 보통 name은 사라졌을 것처럼 보인다.
하지만 displayName 함수는 makeFunc이 실행될 당시의 환경을 기억하고 있다.
이게 바로 클로저다.
자바스크립트는 정적 스코프(static scope), 즉 “코드가 작성된 위치”에 따라 변수의 유효 범위를 결정한다.
이는 런타임이 아니라 컴파일(파싱) 시점에 결정된다.
function init() {
const name = "Mozilla";
function displayName() {
console.log(name);
}
displayName();
}
init(); // Mozilla
내부 함수 displayName은 자신이 어디에서 정의되었는지를 기준으로 외부 스코프(init)에 접근한다.
이러한 어휘적 스코프 덕분에 클로저가 가능해진다.
클로저가 만들어질 때, 자바스크립트 엔진은 함수가 참조하는 외부 변수들을 별도의 환경 객체(Environment Record) 안에 저장한다.
이 객체는 함수가 반환된 뒤에도 GC(가비지 컬렉션)으로부터 제거되지 않고 남는다.
즉, 함수가 실행될 때마다 새로운 “환경 스냅샷”이 생성되는 것이다.
function makeAdder(x) {
return function (y) {
return x + y;
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
add5와 add10은 서로 다른 환경을 기억한다.
그래서 각자의 x 값을 따로 유지할 수 있다.
클로저를 사용하면, 외부에서 접근할 수 없는 “비공개 상태”를 만들 수 있다.
function createCounter() {
let count = 0;
return {
increment() {
count++;
},
decrement() {
count--;
},
getCount() {
return count;
},
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
외부에서는 count에 직접 접근할 수 없다.
오직 increment, decrement, getCount를 통해서만 접근 가능하다.
이것은 클래스의 “private 속성”과 같은 효과를 낸다.
function makeSizer(size) {
return function () {
document.body.style.fontSize = `${size}px`;
};
}
const size12 = makeSizer(12);
const size16 = makeSizer(16);
document.getElementById("small").onclick = size12;
document.getElementById("large").onclick = size16;
각 버튼은 클릭될 때마다 서로 다른 size 값을 기억하고 있다.
이처럼 클로저는 이벤트 기반 코드에서 상태를 저장하는 강력한 도구로 쓰인다.
아래 코드는 모든 입력창이 마지막 도움말만 보여준다.
function setupHelp() {
var helpText = [
{ id: "email", help: "Your email address" },
{ id: "name", help: "Your full name" },
{ id: "age", help: "Your age" },
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function () {
showHelp(item.help); // 마지막 값만 유지
};
}
}
해결 방법은 let을 사용해 각 반복마다 별도의 블록 스코프를 생성하는 것이다.
for (let i = 0; i < helpText.length; i++) {
const item = helpText[i];
document.getElementById(item.id).onfocus = () => showHelp(item.help);
}
function MyObject(name) {
this.name = name;
this.getName = function () {
return this.name; // 매번 새 함수 생성
};
}
// 개선
MyObject.prototype.getName = function () {
return this.name;
};
클로저는 함수가 기억하는 ‘환경’이다.
기억해야 할 건 변수 값이 아니라 변수를 둘러싼 스코프 자체다.
실행 컨텍스트(Execution Context)는 자바스크립트 코드가 실행되는 환경(문맥) 이다.
즉, “현재 코드가 어디서 실행 중이며, 어떤 변수·함수에 접근할 수 있는지”를 정의하는 객체라고 할 수 있다.
한 문장으로 다시 정리하자면 “실행 컨텍스트는 자바스크립트 엔진이 코드를 실행하기 위해 필요한 모든 정보를 담은 객체”다.
자바스크립트 엔진은 코드를 읽을 때 여러 종류의 컨텍스트를 만든다.
크게 세 가지다.
| 종류 | 생성 시점 | 역할 |
|---|---|---|
| 전역 컨텍스트(Global Context) | 프로그램 시작 시 한 번 생성 | 전역 코드(파일 전체) 실행 환경 |
| 함수 컨텍스트(Function Context) | 함수가 호출될 때마다 생성 | 함수 내부 실행 환경 |
| Eval 컨텍스트(Eval Context) | eval()이 호출될 때 | (권장 X) 문자열 코드를 실행하는 특수 컨텍스트 |
실행 중에는 이들이 스택 구조(Call Stack) 형태로 쌓였다가, 실행이 끝나면 제거된다.
하나의 실행 컨텍스트는 내부적으로 다음 세 가지로 구성된다.
var로 선언된 변수들이 등록되는 곳
선언과 초기화가 동시에 일어남 (그래서 undefined로 호이스팅됨)
let, const 변수, 함수 선언, 외부 스코프 정보 등을 저장
실제 스코프 체인이 여기서 형성됨
내부적으로는 두 개의 구성요소를 가진다:
Environment Record: 현재 스코프의 변수/함수 선언 정보를 기록
Outer Lexical Environment Reference: 외부(부모) 스코프를 참조
실행 문맥에 따라 this가 어떤 객체를 가리키는지 결정됨
전역 컨텍스트: this → window (브라우저 기준)
함수 호출 방식에 따라 동적 바인딩 (call, apply, bind, 메서드 호출 등)
함수가 호출될 때, 실행 컨텍스트는 다음 순서로 만들어진다.
function sayHello(name) {
var greeting = "Hello";
console.log(greeting + " " + name);
}
sayHello("Hyerin");
전역 코드가 실행되기 전에 생성
sayHello 함수가 메모리에 등록됨
GlobalEC = {
LexicalEnvironment: {
EnvironmentRecord: { sayHello: <function> },
Outer: null,
},
VariableEnvironment: {...},
ThisBinding: window
}
FunctionEC = {
LexicalEnvironment: {
EnvironmentRecord: { name: "Hyerin", greeting: undefined },
Outer: GlobalLexicalEnvironment
},
VariableEnvironment: {...},
ThisBinding: undefined (strict mode 기준)
}
var greeting이 선언되고 undefined로 초기화됨
name은 매개변수로 이미 Environment Record에 존재
greeting = "Hello" 할당
console.log 실행
실행 컨텍스트가 생성될 때, 자바스크립트 엔진은 스코프 내 선언들을 미리 스캔하고, 변수/함수 선언을 메모리에 기록해둔다.
이게 바로 호이스팅(끌어올리기)이다.
console.log(x); // undefined
var x = 10;
위 코드의 실제 내부 동작은 다음과 같다.
var x; // 선언(등록)
console.log(x);
x = 10; // 초기화(할당)
반면 let과 const는 TDZ(Temporal Dead Zone)를 가지므로 선언 전 접근 시 ReferenceError를 발생시킨다.
스코프 체인(Scope Chain)은 현재 컨텍스트의 LexicalEnvironment에서 OuterEnvironmentReference를 따라가며 변수 탐색을 하는 구조다.
const x = 1;
function outer() {
const y = 2;
function inner() {
const z = 3;
console.log(x + y + z);
}
inner();
}
outer(); // 6
내부적으로는 아래처럼 스코프 체인이 연결된다.
GlobalLE → outerLE → innerLE
↑ ↑ ↑
x=1 y=2 z=3
inner()에서 x를 찾을 때 inner 스코프에 없으면 → outer → global 순으로 탐색한다.
이 탐색 경로가 클로저가 외부 변수를 기억할 수 있는 근거다.
클로저는 함수가 생성될 당시의 Lexical Environment를 기억하는 현상이다.
즉, 실행 컨텍스트가 사라져도 그 안의 Environment Record가 참조되고 있다면 GC(가비지 컬렉션)으로부터 해제되지 않는다.
function outer() {
const x = 10;
return function inner() {
console.log(x);
};
}
const fn = outer();
fn(); // 10
이때 outer()의 실행 컨텍스트는 끝났지만 inner 함수가 여전히 x를 참조하고 있기 때문에 outer의 Lexical Environment는 메모리에 남아있다.
정리하자면 실행 컨텍스트는 함수가 실행될 때 생성되는 실행 환경이며 클로저는 함수가 생성될 때 그 환경을 기억하는 메커니즘이다.
function first() {
second();
console.log("End of first");
}
function second() {
console.log("In second");
}
first();
console.log("End of global");
| 스택 상태 | 실행 중인 코드 |
|---|---|
| [Global] | 프로그램 시작 |
| [Global → first] | first() 호출 |
| [Global → first → second] | second() 호출 |
| [Global → first] | second() 종료 |
| [Global] | first() 종료 |
| [] | 프로그램 종료 |
이런 스택 기반 실행 모델 덕분에 자바스크립트는 동기적 실행 흐름을 보장한다.