클로저와 실행 컨텍스트
실행 컨텍스트와 호출 스택, 렉시컬 환경을 따라 변수 탐색을 이해하고 클로저가 외부 상태를 기억하는 원리를 익힙니다.
우리는 5장에서 자바스크립트 기본 문법부터 함수, DOM 조작과 이벤트 처리까지 웹 개발의 필수 내용을 학습했습니다.
이제 기본 페이지에 동작을 붙일 수 있는 단계에 올라온 상태입니다.
다음 단계에서는 왜 이렇게 동작하는가를 이해해야 합니다.
코드가 어떻게 평가되고 실행되는지, 변수와 함수가 메모리에서 어떻게 관리되는지를 알면 디버깅 속도와 코드 품질이 확실히 달라집니다.
6장 자바스크립트 심화 I는 이 내부 원리를 다룹니다.
그 첫 주제가 많은 개발자가 어려워하는 클로저(Closure)와 실행 컨텍스트(Execution Context)입니다.
자바스크립트는 Lexical Scoping(어휘적 스코핑)을 기반으로 동작합니다.
함수를 어디서 선언했는지가 스코프를 결정한다는 뜻입니다.
이 특성과 실행 환경이 결합되면서 실행 컨텍스트와 클로저가 만들어지고, 이 메커니즘이 비동기 처리, 모듈 패턴, 데이터 은닉의 기반이 됩니다.
클로저와 렉시컬 환경의 수명 — 상태 추적
counter1/2의 반환 함수와 독립 환경 count가 호출 뒤에도 남고 각각 다르게 변한다. 함수 frame 종료와 환경 보존/독립 변경을 고정 객체로 추적할 가치가 있다.
LIFETIME · LEXICAL SCOPE
클로저: 호출이 끝나도 남는 바인딩
makeCounter는 호출마다 독립적인 count를 만든다. 반환된 함수를 계속 참조하면 바깥 호출이 끝난 뒤에도 해당 count에 접근할 수 있다.
counter1 → 함수 1 → 환경 1
makeCounter 첫 호출이 만든 count를 사용한다. counter1()의 반환값은 1 → 2 → 3이다.
counter2 → 함수 2 → 환경 2
makeCounter 둘째 호출은 별도의 count를 만든다. counter2()는 1을 반환하고 counter1의 count를 바꾸지 않는다.
실행 컨텍스트 종료
makeCounter 호출이 끝나면 그 실행 컨텍스트는 호출 스택에서 제거된다. 반환된 함수의 외부 바인딩 접근은 계속 가능하다.
마지막 유지 경로 제거
함수와 필요한 환경으로 이어지는 모든 도달 경로가 없어지면 회수 대상이 될 수 있다. 참조 해제가 즉시 GC 실행을 뜻하지는 않는다.
스택의 수명과 바인딩의 수명은 다르다
클로저가 종료된 실행 컨텍스트를 스택에 남겨 두는 것은 아니다. 반환된 함수는 자신이 만들어진 렉시컬 환경을 통해 필요한 바인딩에 접근한다. 그림은 의미 관계를 나타내며 엔진의 실제 메모리 배치를 지정하지 않는다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
- 본문의 실제 호출 순서 counter1(), counter1(), counter2(), counter1()을 따릅니다. 각 함수가 보존한 count는 독립적이며 호출 횟수를 다른 순서로 재배열하지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | counter1에서 함수 1, 환경 1로 이어지는 참조와 counter2에서 함수 2, 환경 2로 이어지는 참조는 독립적이다. 첫 환경 count는 0부터 1, 2, 3으로 바뀌고 둘째는 0부터 1로 바뀐다. 화살표는 실행 순서가 아닌 함수와 환경의 의미상 참조다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| counter1 두 번 호출 | counter1: 2 / counter2: 0 | 독립 count=0 환경을 만든 뒤 counter1을 두 번 호출한다 | counter1이 보존한 count만 0→1→2로 증가하고 counter2는 아직 호출되지 않았다 |
| counter2 첫 호출 | counter1: 2 / counter2: 1 | 다음 호출은 counter2의 독립 count를 증가시킨다 | 같은 함수 정의여도 독립 환경을 참조하므로 counter2의 첫 결과는 1이다 |
| counter1 다시 호출 | counter1: 3 / counter2: 1 | 마지막 counter1 호출은 이전 count=2에서 계속한다 | 외부 생성 함수의 frame 종료 뒤에도 반환 함수가 참조한 환경은 남아 counter1만 3으로 증가한다 |
클로저와 렉시컬 환경의 수명 — 상태 추적
counter1/2의 반환 함수와 독립 환경 count가 호출 뒤에도 남고 각각 다르게 변한다. 함수 frame 종료와 환경 보존/독립 변경을 고정 객체로 추적할 가치가 있다.
LIFETIME · LEXICAL SCOPE
클로저: 호출이 끝나도 남는 바인딩
makeCounter는 호출마다 독립적인 count를 만든다. 반환된 함수를 계속 참조하면 바깥 호출이 끝난 뒤에도 해당 count에 접근할 수 있다.
counter1 → 함수 1 → 환경 1
makeCounter 첫 호출이 만든 count를 사용한다. counter1()의 반환값은 1 → 2 → 3이다.
counter2 → 함수 2 → 환경 2
makeCounter 둘째 호출은 별도의 count를 만든다. counter2()는 1을 반환하고 counter1의 count를 바꾸지 않는다.
실행 컨텍스트 종료
makeCounter 호출이 끝나면 그 실행 컨텍스트는 호출 스택에서 제거된다. 반환된 함수의 외부 바인딩 접근은 계속 가능하다.
마지막 유지 경로 제거
함수와 필요한 환경으로 이어지는 모든 도달 경로가 없어지면 회수 대상이 될 수 있다. 참조 해제가 즉시 GC 실행을 뜻하지는 않는다.
스택의 수명과 바인딩의 수명은 다르다
클로저가 종료된 실행 컨텍스트를 스택에 남겨 두는 것은 아니다. 반환된 함수는 자신이 만들어진 렉시컬 환경을 통해 필요한 바인딩에 접근한다. 그림은 의미 관계를 나타내며 엔진의 실제 메모리 배치를 지정하지 않는다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
- 본문의 실제 호출 순서 counter1(), counter1(), counter2(), counter1()을 따릅니다. 각 함수가 보존한 count는 독립적이며 호출 횟수를 다른 순서로 재배열하지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | counter1에서 함수 1, 환경 1로 이어지는 참조와 counter2에서 함수 2, 환경 2로 이어지는 참조는 독립적이다. 첫 환경 count는 0부터 1, 2, 3으로 바뀌고 둘째는 0부터 1로 바뀐다. 화살표는 실행 순서가 아닌 함수와 환경의 의미상 참조다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| counter1 두 번 호출 | counter1: 2 / counter2: 0 | 독립 count=0 환경을 만든 뒤 counter1을 두 번 호출한다 | counter1이 보존한 count만 0→1→2로 증가하고 counter2는 아직 호출되지 않았다 |
| counter2 첫 호출 | counter1: 2 / counter2: 1 | 다음 호출은 counter2의 독립 count를 증가시킨다 | 같은 함수 정의여도 독립 환경을 참조하므로 counter2의 첫 결과는 1이다 |
| counter1 다시 호출 | counter1: 3 / counter2: 1 | 마지막 counter1 호출은 이전 count=2에서 계속한다 | 외부 생성 함수의 frame 종료 뒤에도 반환 함수가 참조한 환경은 남아 counter1만 3으로 증가한다 |
실행 컨텍스트: 코드가 실행되는 환경
우리가 작성한 자바스크립트 코드가 실행되기 위해서는 특정 환경이 필요합니다.
코드의 실행 상태를 설명하는 명세상의 모델을 실행 컨텍스트(Execution Context)라고 부릅니다.
실행 컨텍스트는 현재 실행 위치와 변수 탐색 환경 등을 설명하는 추상 모델입니다. 엔진이 반드시 이 모양의 메모리 객체를 만들어야 한다는 뜻은 아닙니다. 이 절의 스택 그림도 코드의 호출·복귀 순서를 추적하기 위한 모델입니다.
자바스크립트 코드가 실행되는 순간, 엔진은 이 실행 컨텍스트를 생성하고, 이를 통해 변수를 관리하고, 스코프 체인을 구성하며, this 값을 결정하는 등의 작업을 수행합니다.
실행 컨텍스트의 종류
아래 동기 classic script 예시에서는 전역 코드와 함수 호출의 컨텍스트를 구분합니다. 모듈 평가·eval·제너레이터 등에는 추가 규칙이 있으므로 이 두 항목을 모든 실행 형식의 전체 목록으로 외우지는 않습니다.
- 자바스크립트 코드가 처음 실행될 때 생성되는 최상위 컨텍스트입니다.
- 브라우저의 일반
<script>(classic script) 전역 코드에서는 전역 객체인window가this가 됩니다. 모듈과 함수 호출에서의this는 다음 절에서 구분합니다. - Node.js 환경에서는
global객체가 전역 객체가 됩니다. - 아래 예시의 전역 코드를 실행하는 동안 스택에 있습니다. 스크립트 평가가 끝나 스택에서 빠지는 것과 전역 변수의 바인딩이 유지되는 것은 별개입니다.
- 함수를 호출할 때마다 별도로 생성되는 컨텍스트입니다.
- 호출마다 매개변수·지역 변수의 바인딩이 만들어집니다. 바깥 환경 연결은 함수의 정의 위치를 따르며, 화살표 함수의
this는 바깥 환경에서 찾습니다. - 아래처럼 동기 함수가 끝나면 실행 컨텍스트는 스택에서 제거됩니다. 클로저가 참조하는 바인딩까지 즉시 없어지는 것은 아닙니다.
실행 컨텍스트 스택 (Call Stack)
호출 스택의 push와 pop — 상태 추적
first/second 호출·종료에서 스택이 push/pop되고 현재 실행 함수가 바뀐다. 고정 스택 칸으로 6개 snapshot을 전환하면 복귀 순서를 명확히 읽는다.
SEQUENCE · WEB PLATFORM CONTRACT
호출 스택의 push와 pop
first가 second를 호출하면 second가 먼저 끝나고 first로 복귀한다. 각 스냅샷은 호출과 종료 뒤의 스택을 아래 전역 프레임부터 위 현재 프레임까지 보여 준다.
- PUSH global
script 시작 — stack [global]
- CALL first · PUSH first
first activation 시작 — stack [first, global]
- CALL second · PUSH second
second activation 시작 — stack [second, first, global]
- RETURN second · POP second
second activation 종료 — stack [first, global]
- RETURN first · POP first
first activation 종료 — stack [global]
- script end · POP global
stack [empty]. 호출한 역순으로만 frame이 제거된다.
가장 위에 있는 실행 컨텍스트가 실행한다
본문의 first·second는 명시적 return 없이 끝나므로 undefined를 반환한다. 이 동기 호출 예시에서는 second → first → script 순으로 스택에서 제거된다. script가 끝나도 전역 바인딩이 모두 없어지는 것은 아니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 시간 순서 1부터 6으로 전역 코드, first 호출, second 호출, second 종료, first 종료, script 종료가 이어진다. 각 영역의 아래쪽부터 전역과 호출된 함수의 프레임이 쌓이며 맨 위 프레임에서 현재 실행한다. 종료하면 호출 역순으로 제거된다. 마지막 스택은 비지만 전역 바인딩의 삭제를 뜻하지 않는다. |
| 원도표 조건·주석 | PUSH global: script 시작 — stack [global] |
| 원도표 조건·주석 | CALL first · PUSH first: first activation 시작 — stack [first, global] |
| 원도표 조건·주석 | CALL second · PUSH second: second activation 시작 — stack [second, first, global] |
| 원도표 조건·주석 | RETURN second · POP second: second activation 종료 — stack [first, global] |
| 원도표 조건·주석 | RETURN first · POP first: first activation 종료 — stack [global] |
| 원도표 조건·주석 | script end · POP global: stack [empty]. 호출한 역순으로만 frame이 제거된다. |
| 원도표 설명 | 가장 위에 있는 실행 컨텍스트에서 코드가 실행된다 |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 전역에서 first | stack: global → first / 현재: first | global 실행 위에 first 호출 frame이 올라간다 | 호출은 새 실행 frame을 쌓는다 |
| second 호출과 복귀 | second 실행 중 stack: global → first → second / second 반환 후 stack: global → first | first가 second를 호출해 쌓은 frame은 second가 반환하면 제거된다 | second 실행 중에는 맨 위 second frame이 실행되고, 반환 후 first frame에서 이어 실행한다 |
| 모두 반환 | stack: 비어 있음 / global binding: 자동 삭제 아님 | first와 script 실행 frame이 사라진다 | 콜 스택의 frame과 도달 가능한 전역 값의 수명은 다르다 |
호출 스택의 push와 pop — 상태 추적
first/second 호출·종료에서 스택이 push/pop되고 현재 실행 함수가 바뀐다. 고정 스택 칸으로 6개 snapshot을 전환하면 복귀 순서를 명확히 읽는다.
SEQUENCE · WEB PLATFORM CONTRACT
호출 스택의 push와 pop
first가 second를 호출하면 second가 먼저 끝나고 first로 복귀한다. 각 스냅샷은 호출과 종료 뒤의 스택을 아래 전역 프레임부터 위 현재 프레임까지 보여 준다.
- PUSH global
script 시작 — stack [global]
- CALL first · PUSH first
first activation 시작 — stack [first, global]
- CALL second · PUSH second
second activation 시작 — stack [second, first, global]
- RETURN second · POP second
second activation 종료 — stack [first, global]
- RETURN first · POP first
first activation 종료 — stack [global]
- script end · POP global
stack [empty]. 호출한 역순으로만 frame이 제거된다.
가장 위에 있는 실행 컨텍스트가 실행한다
본문의 first·second는 명시적 return 없이 끝나므로 undefined를 반환한다. 이 동기 호출 예시에서는 second → first → script 순으로 스택에서 제거된다. script가 끝나도 전역 바인딩이 모두 없어지는 것은 아니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 시간 순서 1부터 6으로 전역 코드, first 호출, second 호출, second 종료, first 종료, script 종료가 이어진다. 각 영역의 아래쪽부터 전역과 호출된 함수의 프레임이 쌓이며 맨 위 프레임에서 현재 실행한다. 종료하면 호출 역순으로 제거된다. 마지막 스택은 비지만 전역 바인딩의 삭제를 뜻하지 않는다. |
| 원도표 조건·주석 | PUSH global: script 시작 — stack [global] |
| 원도표 조건·주석 | CALL first · PUSH first: first activation 시작 — stack [first, global] |
| 원도표 조건·주석 | CALL second · PUSH second: second activation 시작 — stack [second, first, global] |
| 원도표 조건·주석 | RETURN second · POP second: second activation 종료 — stack [first, global] |
| 원도표 조건·주석 | RETURN first · POP first: first activation 종료 — stack [global] |
| 원도표 조건·주석 | script end · POP global: stack [empty]. 호출한 역순으로만 frame이 제거된다. |
| 원도표 설명 | 가장 위에 있는 실행 컨텍스트에서 코드가 실행된다 |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 전역에서 first | stack: global → first / 현재: first | global 실행 위에 first 호출 frame이 올라간다 | 호출은 새 실행 frame을 쌓는다 |
| second 호출과 복귀 | second 실행 중 stack: global → first → second / second 반환 후 stack: global → first | first가 second를 호출해 쌓은 frame은 second가 반환하면 제거된다 | second 실행 중에는 맨 위 second frame이 실행되고, 반환 후 first frame에서 이어 실행한다 |
| 모두 반환 | stack: 비어 있음 / global binding: 자동 삭제 아님 | first와 script 실행 frame이 사라진다 | 콜 스택의 frame과 도달 가능한 전역 값의 수명은 다르다 |
자바스크립트 엔진은 여러 개의 실행 컨텍스트를 동시에 관리하기 위해 스택(Stack) 자료구조를 사용합니다.
이를 실행 컨텍스트 스택 또는 호출 스택(Call Stack)이라고 부릅니다.
- 아래 동기 classic script 실행 중에는 가장 아래에 해당 전역 코드의 컨텍스트가 있습니다. 스크립트 평가가 끝난 뒤 스택이 비어 있어도 전역 바인딩은 남을 수 있습니다.
- 함수를 호출할 때마다 새로운 함수 실행 컨텍스트가 스택의 맨 위에 추가(push)됩니다.
- 함수의 실행이 완료되면 해당 함수 실행 컨텍스트는 스택에서 제거(pop)됩니다.
- 스택의 가장 위에 있는 컨텍스트가 현재 실행 중인 코드의 환경입니다.
function first() {
console.log('첫 번째 함수 실행');
second(); // second 함수 호출
console.log('첫 번째 함수 종료');
}
function second() {
console.log('두 번째 함수 실행');
// third 함수는 호출되지 않음
console.log('두 번째 함수 종료');
}
first(); // first 함수 호출
console.log('전역 코드 종료');위 코드가 실행될 때 호출 스택의 변화
(console.log('전역 코드 종료'); 대기)
first() 호출: first 함수 실행 컨텍스트 생성 및 스택에 push.
(console.log('첫 번째 함수 실행'); 실행)
second() 호출: second 함수 실행 컨텍스트 생성 및 스택에 push.
(console.log('두 번째 함수 실행'); 실행)
second 함수 종료: second 함수 실행 컨텍스트 스택에서 pop.
first 함수 종료: first 함수 실행 컨텍스트 스택에서 pop.
모든 전역 코드 실행 종료: 전역 실행 컨텍스트 스택에서 pop.
이러한 스택 구조 덕분에 자바스크립트는 함수 호출 순서를 기억하고, 올바른 실행 흐름을 유지할 수 있습니다.
실행 컨텍스트의 구성 요소 (세부 심화)
여기서는 실행 상태 중 변수 탐색과 관련된 환경 참조를 중심으로 봅니다. LexicalEnvironment와 VariableEnvironment는 환경 레코드를 가리킵니다. 아래의 “레코드 + 바깥 참조” 표현은 이름 탐색 관계를 풀어 쓴 것이며, 실행 컨텍스트 전체를 정확히 세 칸으로 나눈 물리적 구조는 아닙니다.
var선언의 바인딩을 관리하는 환경을 가리킵니다.let·const의 블록 범위 바인딩을 모두 여기에 저장한다고 생각하면 안 됩니다.- 일반 함수의 매개변수와 필요한
arguments바인딩은 함수의 환경에서 관리됩니다. 화살표 함수는 자체arguments를 만들지 않습니다. - 호이스팅을 볼 때는
var의 초기값undefined와 초기화 전 접근이 금지되는let·const를 구분해야 합니다.
- 현재 코드가 식별자 이름을 찾기 시작할 환경 레코드를 가리킵니다. 블록에 진입하면
let·const를 찾을 환경이 추가될 수 있습니다. - 각 환경 레코드는 바깥 환경과 연결됩니다. 이 연결을 따라 이름을 찾는 관계가 스코프 체인입니다.
Environment Record: 현재 스코프 내의 변수와 함수 선언을 기록하는 곳입니다.- 선언적 환경 레코드(Declarative Environment Record): 식별자 바인딩을 기록합니다. 브라우저의 classic script 전역에서는
let·const가 이 부분에 저장되어window속성이 되지 않습니다. - 객체 환경 레코드(Object Environment Record): 객체의 속성과 바인딩을 연결합니다. 같은 classic script의 전역
var·함수 선언은 전역 환경의 객체 레코드에서 처리됩니다. 함수·모듈의 선언까지 모두window속성이 되는 것은 아닙니다.
- 선언적 환경 레코드(Declarative Environment Record): 식별자 바인딩을 기록합니다. 브라우저의 classic script 전역에서는
Outer Environment Reference: 현재Lexical Environment가 생성될 당시의 외부(상위)Lexical Environment를 참조합니다. 이 참조가 바로 스코프 체인을 형성하는 핵심적인 부분입니다.
this 바인딩 (This Binding)this는 해당 바인딩을 제공하는 환경에서 찾습니다. 일반 함수는 호출 방식과 엄격 모드에 영향을 받고, 화살표 함수는 바깥의this를 사용합니다. 값이 항상 객체인 것도 아닙니다. 다음 절에서 구분합니다.
자바스크립트 엔진이 어떤 변수를 찾을 때, 먼저 현재 실행 컨텍스트의 Lexical Environment에 있는 Environment Record에서 해당 변수를 찾습니다.
만약 찾지 못하면, Outer Environment Reference가 가리키는 외부 Lexical Environment로 이동하여 다시 변수를 찾습니다.
이 과정이 전역 Lexical Environment에 도달할 때까지 반복됩니다.
이 연결된 체인이 바로 스코프 체인이며, 변수 검색의 기본 원리입니다.
let a = 10; // 전역 렉시컬 환경의 Environment Record에 저장
function outer() {
let b = 20; // outer 함수의 렉시컬 환경의 Environment Record에 저장
// outer 함수의 Outer Environment Reference는 전역 렉시컬 환경을 가리킴
function inner() {
let c = 30; // inner 함수의 렉시컬 환경의 Environment Record에 저장
// inner 함수의 Outer Environment Reference는 outer 함수의 렉시컬 환경을 가리킴
console.log(a + b + c); // 10 + 20 + 30 = 60
// c는 현재 스코프에서 찾음
// b는 Outer (outer 함수) 스코프에서 찾음
// a는 Outer (전역) 스코프에서 찾음
}
inner();
}
outer();이러한 스코프 체인 덕분에 중첩된 함수는 외부 함수의 변수에 접근할 수 있게 됩니다.
클로저: 함수와 렉시컬 환경의 조합
이제 실행 컨텍스트와 렉시컬 환경, 스코프 체인에 대한 이해를 바탕으로 클로저 (Closure)라는 중요한 개념을 살펴보겠습니다.
클로저는 함수가 선언될 때의 렉시컬 환경(Lexical Environment)을 기억하여, 함수가 외부에서 호출될 때도 그 렉시컬 환경에 접근할 수 있는 현상을 의미합니다.
또는 더 간단히 말해, 자신의 외부 함수가 실행 컨텍스트에서 사라진 이후에도 외부 함수의 변수에 접근할 수 있는 함수를 클로저라고도 부릅니다.
조금 복잡하게 들릴 수 있지만, 예시를 통해 이해하면 훨씬 명확해집니다.
function makeCounter() {
let count = 0; // 이 변수는 makeCounter의 렉시컬 환경에 포함됩니다.
return function() { // 이 익명 함수가 클로저입니다.
count++; // 외부 함수(makeCounter)의 count 변수에 접근
return count;
};
}
const counter1 = makeCounter(); // makeCounter 호출, 새로운 count=0을 가진 클로저 반환
const counter2 = makeCounter(); // makeCounter 또 호출, 또 다른 count=0을 가진 클로저 반환
console.log(counter1()); // 결과: 1 (counter1만의 count 변수를 증가시킴)
console.log(counter1()); // 결과: 2
console.log(counter2()); // 결과: 1 (counter2만의 count 변수를 증가시킴)
console.log(counter1()); // 결과: 3makeCounter() 함수가 호출되면, 새로운 함수 실행 컨텍스트가 생성되고, 그 안에 count 변수를 포함하는 렉시컬 환경이 생성됩니다.
makeCounter()는 return function() { ... }이라는 익명 함수를 반환합니다.
이 익명 함수는 자신이 선언될 당시의 렉시컬 환경(makeCounter의 렉시컬 환경, 즉 count 변수가 있는 환경)을 기억합니다.
이 기억된 렉시컬 환경을 클로저 환경(Closure Environment)이라고도 부릅니다.
makeCounter() 함수의 실행은 끝났고, 그 실행 컨텍스트는 스택에서 제거됩니다.
실행 컨텍스트가 스택에서 제거되어도 참조된 바인딩이 즉시 삭제되는 것은 아닙니다. 아래처럼 클로저가 참조하는 count는 계속 유지될 수 있습니다.
하지만 counter1과 counter2 변수에 할당된 익명 함수(클로저)는 여전히 자신이 기억하는 makeCounter의 count 변수에 접근할 수 있습니다!
함수에서 필요한 바인딩에 접근할 수 있는 관계가 유지되는 것입니다. 그림은 이 의미 관계를 표현하며 특정 엔진의 메모리 배치나 가비지 컬렉션 시점을 보장하지 않습니다.
counter1과 counter2는 각각 별개의 makeCounter 호출로 생성되었기 때문에, 각자 자신만의 count 변수를 기억하고 유지합니다.
이처럼 클로저는 함수가 생성될 당시의 환경(스코프)을 포획(capture)하여, 나중에 함수가 실행될 때 그 환경에 접근할 수 있도록 해주는 강력한 메커니즘입니다.
클로저의 활용 사례
클로저는 자바스크립트에서 매우 다양하게 활용됩니다.
데이터 은닉 (Private Variables): 특정 변수에 외부에서 직접 접근하는 것을 막고, 오직 클로저를 통해서만 접근할 수 있도록 하여 정보 은닉을 구현합니다.
function createWallet(initialBalance) {
let balance = initialBalance; // 외부에서 직접 접근 불가
return {
getBalance: function() {
return balance;
},
deposit: function(amount) {
balance += amount;
console.log(`${amount}원 입금. 현재 잔액: ${balance}`);
},
withdraw: function(amount) {
if (balance >= amount) {
balance -= amount;
console.log(`${amount}원 출금. 현재 잔액: ${balance}`);
} else {
console.log("잔액이 부족합니다.");
}
}
};
}
const myWallet = createWallet(10000);
myWallet.deposit(5000); // 5000원 입금. 현재 잔액: 15000
myWallet.withdraw(7000); // 7000원 출금. 현재 잔액: 8000
console.log(myWallet.getBalance()); // 8000
// console.log(myWallet.balance); // undefined (balance 변수는 외부에서 직접 접근 불가)이 지갑 예시는 유한한 양수 금액만 넣는 학습용 코드입니다. deposit(-500)은 잔액을 줄이고 withdraw(-500)은 오히려 잔액을 늘리므로, 클로저로 값을 숨기는 것만으로 입력 검증이 완성되지는 않습니다. 실제 입출금 규칙을 만들 때는 초기 잔액·입출금액의 자료형과 범위를 메서드 진입 시 검사해야 합니다.
상태 유지 (State Management): 특정 상태를 기억하고, 그 상태를 기반으로 동작하는 함수를 만들 때 사용됩니다.
(위의 makeCounter 예시와 유사)
콜백 함수 (Callback Functions)와 비동기 처리: setTimeout, 이벤트 핸들러 등 비동기적으로 실행되는 함수는 대부분 클로저의 특성을 활용합니다.
function delayedMessage(message, delay) {
setTimeout(function() {
console.log(message); // 외부 함수의 message 변수에 접근
}, delay);
}
delayedMessage("5초 뒤에 출력될 메시지", 5000);setTimeout에 전달된 익명 함수는 delayedMessage 함수의 message 변수에 대한 클로저를 형성합니다.
delayedMessage가 반환한 뒤에도 등록된 함수는 message에 접근합니다. 5000은 최소 지연을 요청하는 값이며, 실행 중인 작업이나 브라우저 정책에 따라 실제 출력은 더 늦을 수 있습니다.
부분 적용 (Partial Application) / 커링 (Currying): 함수에 인자를 미리 일부만 적용하여 새로운 함수를 만드는 기법에 활용됩니다.
function multiply(a) {
return function(b) { // 이 내부 함수가 클로저
return a * b; // 외부 함수의 a 변수에 접근
};
}
const multiplyBy5 = multiply(5); // a에 5를 고정시킨 새로운 함수 생성
console.log(multiplyBy5(10)); // 결과: 50
console.log(multiplyBy5(20)); // 결과: 100클로저는 자바스크립트의 비동기성, 함수형 프로그래밍 패러다임과 밀접하게 연결되어 있으며, 자바스크립트의 강력함을 이해하는 데 필수적인 개념입니다.
직접 추적: 같은 함수의 별칭과 새 클로저
새 콘솔에서 아래 코드의 네 출력값을 실행 전에 적어 보세요. a와 alias가 같은 함수를 가리키는지, b가 별도 호출로 만들어졌는지를 기준으로 count 표를 두 줄로 나누면 됩니다.
function makeCounterForPractice() {
let count = 0;
return () => ++count;
}
const a = makeCounterForPractice();
const alias = a;
const b = makeCounterForPractice();
console.log(a());
console.log(alias());
console.log(b());
console.log(a());예상 결과와 바인딩 추적
출력은 1, 2, 1, 3입니다. alias = a는 함수를 새로 만들거나 makeCounterForPractice를 다시 실행하지 않습니다. a와 alias는 같은 count를 갱신합니다. b만 둘째 호출에서 만든 count를 사용합니다. 클로저는 생성 순간의 숫자 0을 복사해 고정한 것이 아니라, 나중에도 같은 바인딩을 읽고 바꿉니다.
독립 연습
위 코드에서 const alias = a; 한 줄만 const alias = makeCounterForPractice();로 바꾸세요. 네 출력값과 유지되는 count의 개수를 예측하고 실행으로 확인하세요.
변경 결과 확인
출력은 1, 1, 1, 2이고 count 바인딩은 세 개입니다. a만 마지막에 둘째 호출을 하며, alias와 b는 각자 처음 호출합니다.
근거: ECMAScript 실행 컨텍스트와 환경 참조, MDN 클로저.
실행 컨텍스트·클로저 정리
이 절에서는 호출 스택과 렉시컬 환경이 실제 변수 탐색 과정에서 어떻게 연결되는지 복기합니다.
이번 장에서는 자바스크립트의 코드가 실제로 어떻게 실행되고 변수가 관리되는지에 대한 근본적인 원리인 실행 컨텍스트와 클로저에 대해 학습했습니다.
실행 컨텍스트는 코드가 실행되는 환경이며, 스택 구조로 관리되어 함수의 호출과 종료에 따라 생성되고 소멸됩니다.
특히 실행 컨텍스트 내부의 렉시컬 환경과 그 안의 Outer Environment Reference가 어떻게 스코프 체인을 형성하여 변수를 탐색하는지 이해하는 것이 중요했습니다.
그리고 이러한 스코프 체인의 특성과 함수의 렉시컬 환경 포획 능력 덕분에 클로저라는 강력한 개념이 탄생함을 배웠습니다.
클로저는 바깥 함수의 호출이 끝난 뒤에도 필요한 바깥 바인딩에 접근할 수 있게 해줍니다. 데이터 은닉, 상태 유지, 나중 콜백에서 이 관계를 활용합니다.
클로저와 실행 컨텍스트는 함수가 어떤 스코프를 기억하고 어떤 순서로 실행되는지 설명할 때 필요합니다.
예제를 실행하면서 각 변수가 어느 스코프에 있고 언제 접근 가능한지 확인해 보세요.
실행 컨텍스트 생성, 렉시컬 환경 연결, 클로저 보존은 호출 스택과 환경 참조를 함께 따라가며 이해할 수 있습니다.
디버깅할 때는 “지금 실행 중인 호출”, “이름을 찾는 정의 위치”, “같은 바인딩을 공유하는 함수”를 구분해 보세요. 호출이 끝났다는 사실만으로 변수가 사라졌다고 결론 내리지 않습니다.