자바스크립트 공부를 시작하고 나서 가장 이해하기 어려웠던 개념 중 하나가 바로 이 클로저였다.
처음에는 이름만 보고서는 영화 클로저, Closer(2004)와 비슷한 의미인 줄 알고, 가까운? 다가갈 수 있는?
이 정도 느낌으로 다가왔는데, 결론적으로.. 전혀 아니다(일단 단어부터 틀리다, clousure다.) 부끄러운 과거,,
해당 개념을 이해하기까지 MDN이나 다른 블로그를 각각 10번은 본 것 같다.
그만큼 초심자에게 쉽지 않은 개념인 것은 확실하다.
지금까지 클로저에 대해 이해한 내용을 차근차근 정리해보고자한다.
🚨 올바르지 않은 내용이 있을 경우 댓글로 남겨주시면 감사드리겠습니다.
“A closure is the combination of a function and the lexical environment within which that function was declared.”
클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.출처 - MDN
클로저(closure)는 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것을 가르킨다.
출처 - 생활코딩
시작에 앞서 클로저의 정의를 써보았다.
처음에 해당 클로저의 정의를 봤을 때 정말 이게 한국말인가 싶었다.
일부러 이해 못하게 하려고 말 장난하는 것 같았다.
그래도 해당 개념에서 가장 유심히 봐야할 대목은 아래와 같다고 생각한다.
일단 클로저란 개념 자체를 이해하기 위해서는 '실행 컨텍스트'에 대한 개념 이해가 우선되어야 완전한 이해가 될 수 있다.
클로저에 대한 이해를 빠르게 도울 수 있을 정도로만 '실행 컨텍스트'에 대해 살펴보고 얕게 살펴본 후 다시 저 혼란스러운 정의들을 이해해보자.
문제는 이 실행 컨텍스트라는 이름부터도 익숙치 않다. 실행 문맥이라..
일단 컨텍스트(문맥)이라는 것이 대체 무엇인지 간단하게 생각해보자.
상황(환경)1
여행가이드가 관광지에 도착해서 여행객들에게 말하는 상황
"자 이제 도착했으니 우리 모두 내립시다"
-> 진짜 같이 내리자는 것을 의미
상황(환경)2 -
버스에서 내리려고 하는데 문앞에 사람이 가로막고 있는 상황
"좀 내립시다"
-> 비켜달라고는 것을 의미
위 예시에서 볼 수 있듯이 같은 '내립시다'라는 표현이 문맥(환경)에 따라 다르게 사용될 수 있다.
즉, 언어가 제대로 이해되고 사용되기 위해서는 반드시 언어가 사용되는 환경이 필요하다.
결국 컨텍스트(문맥)라는 의미는 대략 '환경'이라는 의미로 나는 이해했다.
그럼 이것을 자바스크립트에서 말하는 실행 컨텍스트란?
실행 컨텍스트는 크게 전역 실행 컨텍스트, 함수 실행 컨텍스트로 나뉘어진다.
그리고 실행 컨텍스트(물리적으로 객체의 형태를 띔)는 3가지 정보(프로퍼티)를 가진다.
this
value그럼 위 3가지 프로퍼티를 중심으로 간단한 예제를 통해 실행 컨텍스트가 어떻게 형성되고 코드가 실행되는 지 살펴보자.
var a = 1;
function outerFunc(b) {
var c = 3;
function innerFunc() {
var d = 4;
console.log(a + b + c + d);
}
innerFunc();
}
outerFunc(2);
먼저 전역 코드로 진입하면 전역 실행 컨텍스트가 생성된다.
이 때 전역 실행 컨텍스트는 3가지 프로퍼티를 가지게 된다.
변수 객체(Variable Object) == Global Object
- arguments : null,
- 변수 : [a, outerFunc]
스코프 체인(Scope Chain) == 일종의 리스트이다
- [Global Scope]
this
value
- window 객체(Global Object)
이후 코드가 순차적으로 위에서부터 실행됨으로서 변수 a
에 1이 할당되고 outerFunc
함수가 실행된다.
그럼 다시outerFunc
함수 실행 컨텍스트가 생성되고, 아래와 같은 프로퍼티를 가지게 된다.
변수 객체(Variable Object) == Activation Object
- arguments : [{ b : 2 }]
- 변수 : [c, innerFunc]
스코프 체인(Scope Chain)
- [OuterFunc Function Scope, Global Scope]
this
value
- window 객체(Global Object)
이후 코드가 다시 위에서부터 실행되고 변수 c
에 3이 할당되고 innerFunc
함수가 실행된다.
innerFunc
함수 실행 컨텍스트가 생성되고, innerFunc
실행 컨텍스트의 프로퍼티는 아래와 같다.
변수 객체(Variable Object) == Activation Object
- arguments : null
- 변수 : [d]
스코프 체인(Scope Chain)
- [innerFunc Function Scope, OuterFunc Function Scope, Global Scope]
this
value
- window 객체(Global Object)
마지막으로 함수 코드가 실행되어 변수 d에 4가 할당되고
console.log(a + b + c + d)
가 실행된다.
여기서 중요한 것은!
비록 innerFunc
함수에는 변수 a
, b
, c
가 존재하지 않지만,
해당 변수에 접근하기 위해 스코프체인의 0번째 인덱스부터 1, 2(글로벌 스코프)까지 순차적으로 검색한다.
innerFunc
의 실행 컨텍스트에 담긴 스코프체인 프로퍼티를 통해
스코프체인[0]인 innerFunc
실행 컨텍스트의 변수 객체(Variable Object)를 참조하여 d 값에 접근
스코프체인[1]인 상위함수 outerFunc
실행 컨텍스트의 변수 객체(Variable Object)를 참조하여 b
, c
값에 접근
스코프체인[2]인 전역 실행 컨텍스트의 전역 객체(Global Object)를 참조하여 a
값에 접근
결국10
(1 + 2 + 3 + 4)을 출력하게 된다.
출처 - PoiemaWeb
실행 컨텍스트는 생성이 될 때마다 논리적 스택 구조를 가지는 새로운 실행 컨텍스트 스택이 생성된다.
스택은 LIFO(Last In First Out, 후입 선출)의 구조를 가지는 나열 구조이다.
var a = 1;
function outerFunc(b) {
var c = 3;
function innerFunc() {
var d = 4;
console.log(a + b + c + d);
}
innerFunc();
}
outerFunc(2);
실행 컨텍스트 스택을 기준으로 위 예시 코드를 간단히 살펴보자.
- 전역코드로 진입하고 전역 실행 컨텍스트 스택이 쌓인다
outerFunc
함수가 실행되고outerFunc
함수 실행 컨텍스트가 스택에 쌓인다.innerFunc
함수가 실행되고innerFunc
함수 실행 컨텍스트가 스택에 쌓인다.innerFunc
함수가 종료되고 실행 컨텍스트 스택에서 소멸된다.outerFunc
함수가 종료되고 실행 컨텍스트 스택에서 소멸된다.- 이제 전역 실행 컨텍스트만에 스택에 남아있고, 전역 실행 컨텍스트는 어플리케이션이 종료될 때, 실행 컨텍스트 스택에서 소멸된다.
기본적으로 함수 실행 컨텍스트가 소멸하면,
함수의 상위 스코프에서는 해당 함수 실행 컨텍스트가 저장하고 있는 변수 객체(Variable Object)에 대해
접근하거나 변화를 추적할 수 없다.
왜냐하면?
상위스코프는 하위스코프에 접근 할 수 없기 때문이기도 하고,
이미 함수 실행 컨텍스트가 스택에서 소멸하면서 가지고 있던 정보나 환경도 함께 소멸하기 때문이다.
function a() {
var name = "chan";
}
a(); // a 함수 실행 컨텍스트가 생성
console.log(name); // 스택에서 실행 컨텍스트가 소멸된 이후이므로, 변수 name에 접근 불가
하지만 실행 컨텍스트가 소멸되어도
소멸된 실행 컨텍스트의 변수 객체에 접근할 수 있는 방법이 존재한다.
바로 클로저를 이용하면 된다!
클로저는 반환된 내부함수가 자신이 생성 혹은 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여, 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수를 말한다.
특정 함수의 상위 스코프는 함수를 호출하는 위치나 시점이 아니라
함수를 어디에서 선언하였는지에 따라 결정된다. 아래 예제를 살펴보자.
var a = 1;
function outerFunc(b) {
var c = 3;
function innerFunc() {
var d = 4;
c++;
console.log(a + b + c + d);
}
return innerFunc;
}
var contextOne = outerFunc(2);
var contextTwo = outerFunc(3);
contextOne(); // 11
contextTwo(); // 12
contextTwo(); // 13
contextTwo(); // 14
outerFunc
함수를 실행시켜 반환된 innerFunc
함수를 contextOne
변수에 담았다. 그 다음 contextOne
을 outerFunc
함수 스코프 밖에서 호출했음에도 불구하고, 변수 a
, c
와 매개변수 b
에 값에 이상없이 접근하여 11
을 호출하였다.
그 이유는 outerFunc
함수가 실행되었을 당시 함수 실행 컨텍스트에 의해
this
가 저장되고 해당 실행 컨텍스트에서 선언(생성)된 innerFunc
함수는 그 주변 환경을 기억하기 때문이다.
contextTwo
의 경우에도 마찬가지이다. 하지만 contextOne
과 다르게 12
를 출력하는 이유는 innerFunc
함수가 생성될 당시 주변환경이 다르기 때문이다.(매개변수 b
의 값이 다름)
결론적으로 코드 상에서는 똑같이 선언되었지만, 다른 함수 실행 컨텍스트를 통해 생성된 함수는 각기 다른 환경을 기억하고 있으며, 그 기억하고 있는 외부환경에 접근할 수 있는 함수를 클로저라고 한다.
그리고 contextTwo
를 계속적으로 실행하면 출력하는 값이 1씩 올라가는 것을 확인할 수 있다. 위를 통해 볼 수 있듯 해당 내부 함수는 주변 환경의 변화에 대해 지속적으로 추적하고 접근할 수 있다.
마지막으로 위 정리된 개념들을 토대로, 어느 곳이든지 클로저를 다루면 고정적으로 나오는 코드 예제를 이해해보고 글을 마치도록하겠다.
var a = [];
for (var i = 0; i < 5; i++) {
a[i] = function foo() {
console.log(i);
};
} // a = [f, f, f, f, f];
for (var j = 0; j < 5; j++) {
a[j](); // ?
}
클로저의 내용을 이해하지 못하고 직관적으로 보았을 때, a
배열에 담기는 함수들이 가지고 있는 i
값은 각각 0~4를 할당된 것처럼 보인다. 하지만 실제로 각 함수를 실행하면 5
가 5번 출력된다.
그 이유는 for문을 통해서 5번 foo
함수가 생성되는데, 5번 모두 생성 될 때 함수의 주변 환경에서의 변수i
값은 이미 반복문이 끝난 전역변수, 5
이기 때문이다.(변수 선언문 var
는 블록스코프를 따르지 않음.)
위와 같이 범할 수 있는 오류를 줄이기 위해 클로저를 이용해보자.
var a = [];
for (var i = 0; i < 5; i++) {
a[i] = function bar(j) { // 매개변수 j는 호출될 때마다 1씩 올라감
return function foo() {
console.log(j); // foo 함수가 생설될 당시의 주변 환경을 기억
}
}(i);
} // a = [f, f, f, f, f];
for (var j = 0; j < 5; j++) {
a[j](); // 0 1 2 3 4
}
위 코드는 즉시 호출 함수 표현식(Immediately Invoked Function Expressions, 줄여서 IIFE)를 통해 해결한 것이다. 각각 다른 함수 실행 컨텍스트를 통해 선언된 foo
가 생성될 당시의 주변 환경도 달라지는 점(bar
함수를 실행할 때 인자로 넘기는i
의 값이 다르기 때문에 매개변수 j
가 다름)을 활용하였다.
var a = [];
function foo(j) {
function bar() {
console.log(j);
}
return bar;
}
for (var i = 0; i < 5; i++) {
a[i] = foo(i);
} // a = [f, f, f, f, f];
for (var j = 0; j < 5; j++) {
a[j](); // 0 1 2 3 4
}
즉시 호출 함수 표현식을 안쓰고도 위처럼 작성하여 해결할 수 있다.
그 동안 클로저에 대해서 개념을 이해하지 못하고 있을때 조차 은연 중에 클로저를 써왔던 것 같다. 왜냐하면 지금보니 클로저 없이 코드를 작성해서 구현한다는 것은 불가능하기 때문이다. 😱
앞으로 클로저를 언제 어떻게 사용하고 있는지 항상 분명하게 인지하면서 코드를 작성하는 연습을 해야겠다.
클로저 영화를 기억하는 한 사람으로써 저도 그걸 생각했었거든요. 마무리투수를 클로저라고도 하죠. 내부함수, 외부함수 개념으로 생각하면 참 쉬운데, 오히려 용어로 인한 오해로 인해 처음에 저도 조금 헤맸습니다.