# 곧 글을 대폭 수정할 예정입니다; 오래전에 쓴 글이라 잘못된게 많네요
전 포스팅에 이어 이번 포스팅에서는 클로저의 주의점에 대한 짧은 내용입니다.
클로저의 동작을 이해하기 위해서는 선행 지식이 조금 필요합니다.
자바스크립트의 유효 범위는 함수로 구분된다고 전 포스팅에서 말씀드렸습니다.
이것을 염두에 두고 새로운 사실을 알아봅시다.
자바스크립트에서 함수가 만들어질 때에는 일반적인 코드 구조나 블럭외에, 위에서 유효 범위를 탐색하기 위한 중요한 내부적 속성이 만들어 집니다.
이것을 스코프 체인(Scope Chain)이라고 합니다
이것은 ECMA-262(PDF 파일)에서 정의된 속성인 [[Scope]] 라는 속성으로 참조가 가능합니다. 이 [[Scope]] 속성은 프로그램 문법으로는 일반적으로 접근할 수 없는 속성이기도 합니다. (크롬 인스펙터 - 개발자 도구 - 등의 툴을 사용하면 볼 수 있습니다.)
이 속성은 일종의 스택(Stack)같은 컬렉션 형태로서, 변수 객체 (Variable Object)라는 키와 값의 집합 객체를 각 컬렉션 요소로 가집니다.
잠깐 정리하고 갑시다.
변수 객체 (Variable Object)
어떤 코드가 실행될 때 찾을 변수를 키와 값으로 가진 객체. 프로그램에서는 접근할 수 없습니다.
스코프 체인 (Scope Chain)
함수가 생성될 때 같이 생성되는 변수 객체의 컬렉션.
요소로 변수 객체를 가집니다.
변수를 찾을 때 이 컬렉션을 스택처럼 사용하게 됩니다.
[[Scope]]
함수가 만들어질 때 생성되는 스코프 체인(Scope Chain) 을 참조하는 함수의 속성.
역시 프로그램에서 접근 불가능입니다.
어떤 코드가 실행될 때 찾을 변수를 키와 값으로 가진 객체. 프로그램에서는 접근할 수 없습니다.
스코프 체인 (Scope Chain)
함수가 생성될 때 같이 생성되는 변수 객체의 컬렉션.
요소로 변수 객체를 가집니다.
변수를 찾을 때 이 컬렉션을 스택처럼 사용하게 됩니다.
[[Scope]]
함수가 만들어질 때 생성되는 스코프 체인(Scope Chain) 을 참조하는 함수의 속성.
역시 프로그램에서 접근 불가능입니다.
스코프 체인
[0]변수객체
[1]변수객체
[2]변수객체
[스코프 체인의 구조는 대략 이렇습니다]
그럼 위 스코프 체인과 변수 객체를 이해하기 위해 아래와 같은 평범한 코드를 예로 들어 봅시다.
var v = "global_context"; function showVar(i) { return i+v; }
별 의미없는 함수죠.
이것의 스코프 체인은 다음과 같은 구조일 것입니다.
showVar 함수의 Scope Chain 구조
[0] 글로벌 오브젝트 (변수 객체) | |
---|---|
this | global(브라우저일 경우 window) |
global(브라우저일 경우 window) | 구현 object |
document | 구현 object |
showVar | showVar 함수 |
기타 글로벌 객체들의 이름과 값들 ... etc ... |
좀더 살펴봅시다.
자바스크립트에서 함수가 실행될 경우에 자바스크립트 엔진은 먼저 실행 문맥 (execution context) 을 생성합니다.
함수와 실행 문맥은 1:1이 아니며, 함수가 실행될 때마다 하나씩 생성됩니다.
실행 문맥은 실행기의 스택에 차례로 쌓이게 되고 실행되고 나면 파괴되기에 무한정 생길 염려는 없습니다. (간혹 과도한 재귀 호출이나 무리한 루프문으로 자바스크립트가 정지하면서 Stack Overflow 오류를 내는 경우가 있는데 이것은 이 실행 문맥이 무리하게 실행 스택에 쌓여서 발생하는 것입니다.)
실행 문맥은 함수를 실행시키기 위해 아주 중요한 사전 작업을 진행하는데 그중 하나는 그 함수가 가진 스코프 체인을 자신의 스코프 체인으로 복사하는 일입니다.
실행 문맥또한 스코프 체인을 통해 유효범위를 계산하기 때문이죠.
그 뒤 실행 문맥은 활성 객체 (Activation Object) 라는 변수 객체를 하나 만들어서, 자신의 스코프 체인의 맨 앞에 삽입합니다.
이 활성 객체는 함수 단위의 유효범위에서 정의된 지역 변수들과, 인자, this, arguments 등의 특수한 변수까지 포함한 변수 객체입니다.
다시 용어 두개가 등장했네요.
정리하고 넘어가죠.
실행 문맥 (Execution Conext)
코드가 실행될 때마다 생성되는, 실행 환경을 정의하고 실제 실행하는 내부 객체.
실행 후 파괴되어 없어집니다.
활성 객체 (Activation Object)
함수가 실행될 때 실행 문맥에 의해 생성되며, 함수 자체에 대한 변수 객체가 됩니다.
때때로 호출 객체 (Call Object) 라고도 불립니다.
인자, 지역변수, this, arguments등이 포함되어 있고 스코프 체인의 맨 앞에 옵니다.
코드가 실행될 때마다 생성되는, 실행 환경을 정의하고 실제 실행하는 내부 객체.
실행 후 파괴되어 없어집니다.
활성 객체 (Activation Object)
함수가 실행될 때 실행 문맥에 의해 생성되며, 함수 자체에 대한 변수 객체가 됩니다.
때때로 호출 객체 (Call Object) 라고도 불립니다.
인자, 지역변수, this, arguments등이 포함되어 있고 스코프 체인의 맨 앞에 옵니다.
실행 문맥은 코드를 실행하면서 변수나 프로퍼티를 만날 경우 실행중인 영역이 가진, 스코프 체인을 순서대로 넘기면서 해당 변수와 프로퍼티를 찾아 값을 참조하면서 코드를 실행시킵니다.
showVar("This is ") 이라고 실행한다면 showVar 실행 문맥의 스코프 체인은 다음과 같을 것입니다
showVar 실행 문맥의 Scope Chain 구조
[0] 활성 객체 (변수 객체) | |
---|---|
this | global(브라우저일 경우 window) |
arguments | [ "This is" ] |
i | "This is" |
[1] 글로벌 오브젝트 (변수 객체) | |
---|---|
this | global(브라우저일 경우 window) |
global(브라우저일 경우 window) | 구현 object |
document | 구현 object |
showVar | showVar 함수 |
기타 글로벌 객체들의 이름과 값들 ... etc ... |
함수 showVar가 실행되고, return i + v; 라는 문장을 만났습니다. 자바스크립트는 i라는 변수를 찾기 위해 제일 먼저 실행 문맥의 스코프 체인 맨 앞의 변수객체, 즉 위 구조를 참고한다면 활성 객체를 뒤집니다.
활성 객체에서 변수 i를 찾을 수 있습니다. 사용합니다.
그 다음 v를 찾습니다. 그런데, 활성 객체에는 그런데 v가 없습니다. 그렇다면 그 다음의 변수 객체인 글로벌 오브젝트를 뒤지게 됩니다.
글로벌에는 v가 있습니다. 그럼 그 변수를 참조하고, 성공적으로 값을 리턴하게 될 것입니다.
이제 대충 함수 실행 시, 변수 객체와 실행 문맥, 그리고 유효 범위를 검색하는 방법에 대해 이해하셨을 것 같습니다. 프로토타입에 대해 좀 아시는 분이라면 이 방식이 프로토타입 체인 검색과 상당히 유사하다는 것도 아시게 될 듯 합니다.
(이 블로그의 프로토타입 포스팅을 보고 싶으면 이곳으로)
만일 함수가 여러개 중첩되거나, eval, try-catch, with 등의 동적 스코프 작성 구문이나 함수를 사용한다거나 하면 (eval, try-catch, with 등의 문법이 스코프 체인에 어떠한 영향을 주는지는 여기서 설명하기엔 너무 길어집니다. 기회가 있다면 포스팅해 보겠습니다), 스코프 체인의 크기는 늘어나게 되며, 변수를 찾을 때 뒤지는 스코프 체인의 변수 객체도 그에 맞춰 늘어납니다.
그에 따라 스코프 체인을 검색하는 시간은 늘어나고, 당연히 스크립트의 효율은 떨어집니다.
사용자는 굳어버린 화면을 바라보며 복잡한 표정을 짓겠죠;
이제 클로저가 왜 안좋은 영향을 주는지 알아볼 때군요.
클로저의 경우 이 스코프 체인의 구성이 특이하게 진행됩니다.
function hello() { var f = "hello, "; function world() { alert(f + "world"); } return world; }; var say = hello();
이러한 클로저를 사용하는 함수가 있다고 가정합니다.
hello를 실행할 경우의 스코프 체인은 약간 복잡합니다.
실행시에 역시 hello를 실행하기 위한 실행 문맥이 생성됩니다. 그리고 미리 생성된 hello의 스코프 체인에 있는 글로벌 변수객체가 먼저 삽입된 뒤, 생성된 활성화 객체가 차례로 스코프 체인에 삽입될 것입니다.
그리고 이제 실행 결과로 얻어진 클로저 say의 스코프 체인도 생성됩니다.
say 클로저의 스코프 체인에는 글로벌 객체가 먼저 삽입된 뒤, 그 다음 실행 문맥이 생성한 hello의 활성화 객체가 변수 객체로 삽입됩니다.
함수 say의 스코프 체인의 구조는 다음과 같을 것입니다
say의 Scope Chain 구조 (Closure 생성 시)
[0] hello 함수가 실행될 때 생성된 활성화 객체 | |
---|---|
this | global(브라우저일 경우 window) |
arguments | [ ] |
f | "hello" |
world | world 함수 |
[1] 글로벌 오브젝트 | |
---|---|
this | global(브라우저일 경우 window) |
global(브라우저일 경우 window) | 구현 object |
document | 구현 object |
hello | hello 함수 |
say | undefined |
alert | 내장 함수 |
기타 글로벌 객체들의 이름과 값들 ... etc ... |
여기서 문제 하나가 나옵니다.
활성화 객체는 보통 함수가 실행될 때 실행 문맥에 의해 만들어지고, 활성화 객체가 파괴될 때 같이 없어지는 인스턴트 객체입니다.
그런데 클로저에 쓰일 경우 클로저의 스코프 체인에 활성화 객체가 속하게 되면서 실행 문맥이 끝나도 활성화 객체는 남아있습니다. 클로저가 다수 생성되고, 그것을 해제해주지 않는다면 다수의 활성화 객체가 생성된채로 남고 이것은 문제가 됩니다.
이것이 클로저를 남용할 때 오는 첫번째 문제입니다.
이제 클로저 say를 실행해 봅시다.
클로저 say가 실행되면 역시 실행 문맥이 만들어지고 say의 스코프 체인을 실행 문맥의 스코프 체인에 복사한 뒤, say를 위한 새로운 활성화 객체가 생성되고 스코프 체인의 앞에 삽입됩니다. 다음과 같습니다.
say 실행 문맥의 Scope Chain 구조 (Closure 실행 시)
[0] say 함수가 실행될 때 생성된 활성화 객체 | |
---|---|
this | global(브라우저일 경우 window) |
arguments | [ ] |
[1] hello 함수가 실행될 때 생성된 활성화 객체 | |
---|---|
this | global(브라우저일 경우 window) |
arguments | [ ] |
f | "hello" |
world | world 함수 |
[2] 글로벌 오브젝트 | |
---|---|
this | global(브라우저일 경우 window) |
global(브라우저일 경우 window) | 구현 object |
document | 구현 object |
hello | hello 함수 |
say | undefined |
alert | 내장 함수 |
기타 글로벌 객체들의 이름과 값들 ... etc ... |
다른 일반적 함수와 다르게 스코프 체인이 하나 더 늘었을 뿐 아니라 자주 접근해야 할 글로벌 오브젝트 변수 객체가 제일 마지막입니다.
결과적으로 alert을 찾기 위해서 스코프 체인을 두개 다 뒤진 뒤에야, alert을 찾고 실행할 겁니다. 당연히 성능은 저하됩니다. 주의해야 할 점이죠.
요약해 봅시다.
- 클로저는 클로저를 생성한 함수의 활성화 객체를 그대로 가지고 있게 되어 의도치 않은 메모리 낭비가 발생할 수 있다.
- 클로저 실행 시, 불필요한 스코프 탐색을 하게 되어 성능이 나빠진다.
좋은 점이 있다면 나쁜 점도 있기 마련입니다.
이런 점들이 있다고 해서 클로저를 아예 안쓰거나 버리는 것 또한 바보짓입니다.
절대 남용하지 말고 변수 스코프를 잘 고려한 변수 위치 선정등으로 스코프 체인 탐색을 줄여가면서 클로저를 활용하는 기법을 익힌다면 좀더 자바스크립트 코딩이 즐거워질 지 모릅니다(?)
포스팅 내용을 좀 더 자세히 알아보고 싶다면 다음 책을 추천드립니다.
더글라스 크락포드의 자바스크립트 핵심 가이드, 자바스크립트 성능 최적화
기회가 되면 실제 스코프 체인의 구조를 크롬 인스펙터(크롬 개발자 도구)를 사용하여 탐색하면서 여러 경우의 스코프 체인 구조를 확인해보는 포스팅을 해보겠습니다.
마지막으로 문제 하나 던져 보면서 포스팅을 마칩니다.
이 문제의 실행 결과를 정확히 예측했다면 스코프 체인을 완전히 이해했다고 봐도 될 것 같네요.
// 결과로 찍히는 경고창의 문자열은 뭘까요? var word = "global"; function showVar() { return word; } function receiveFunc(showVar) { var word = "activate"; alert(showVar()); } receiveFunc(showVar);
답 보기