블로그 내 검색

2012. 1. 18.

클로저(Closure) 사용에는 주의가 필요합니다

# 글을 쓴지 좀 됐는데 방문자가 아직 많네요 ~ 좋습니다 :)
# 곧 글을 대폭 수정할 예정입니다; 오래전에 쓴 글이라 잘못된게 많네요

전 포스팅에 이어 이번 포스팅에서는 클로저의 주의점에 대한 짧은 내용입니다.
클로저의 동작을 이해하기 위해서는 선행 지식이 조금 필요합니다.

자바스크립트의 유효 범위는 함수로 구분된다고 전 포스팅에서 말씀드렸습니다.

이것을 염두에 두고 새로운 사실을 알아봅시다.
자바스크립트에서 함수가 만들어질 때에는 일반적인 코드 구조나 블럭외에, 위에서 유효 범위를 탐색하기 위한 중요한 내부적 속성이 만들어 집니다.

이것을 스코프 체인(Scope Chain)이라고 합니다

이것은 ECMA-262(PDF 파일)에서 정의된 속성인 [[Scope]] 라는 속성으로 참조가 가능합니다. 이 [[Scope]] 속성은 프로그램 문법으로는 일반적으로 접근할 수 없는 속성이기도 합니다. (크롬 인스펙터 - 개발자 도구 - 등의 툴을 사용하면 볼 수 있습니다.)

이 속성은 일종의 스택(Stack)같은 컬렉션 형태로서, 변수 객체 (Variable Object)라는 키와 값의 집합 객체를 각 컬렉션 요소로 가집니다.

잠깐 정리하고 갑시다.

변수 객체 (Variable Object)
어떤 코드가 실행될 때 찾을 변수를 키와 값으로 가진 객체. 프로그램에서는 접근할 수 없습니다.

스코프 체인 (Scope Chain)
함수가 생성될 때 같이 생성되는 변수 객체의 컬렉션.
요소로 변수 객체를 가집니다.
변수를 찾을 때 이 컬렉션을 스택처럼 사용하게 됩니다.

[[Scope]] 
함수가 만들어질 때 생성되는 스코프 체인(Scope Chain)  참조하는 함수의 속성.
역시 프로그램에서 접근 불가능입니다.

스코프 체인
[0]변수객체
[1]변수객체
[2]변수객체

[스코프 체인의 구조는 대략 이렇습니다]

그럼 위 스코프 체인과 변수 객체를 이해하기 위해 아래와 같은 평범한 코드를 예로 들어 봅시다.

var v = "global_context";
function showVar(i) {
    return i+v;
}

별 의미없는 함수죠.
이것의 스코프 체인은 다음과 같은 구조일 것입니다.

showVar 함수의 Scope Chain 구조
[0] 글로벌 오브젝트 (변수 객체)
thisglobal(브라우저일 경우 window)
global(브라우저일 경우 window)구현 object
document구현 object
showVarshowVar 함수
기타 글로벌 객체들의 이름과 값들
...
etc
...

좀더 살펴봅시다.
자바스크립트에서 함수가 실행될 경우에 자바스크립트 엔진은 먼저 실행 문맥 (execution context) 을 생성합니다.
함수와 실행 문맥은 1:1이 아니며, 함수가 실행될 때마다 하나씩 생성됩니다.

실행 문맥은 실행기의 스택에 차례로 쌓이게 되고 실행되고 나면 파괴되기에 무한정 생길 염려는 없습니다. (간혹 과도한 재귀 호출이나 무리한 루프문으로 자바스크립트가 정지하면서 Stack Overflow 오류를 내는 경우가 있는데 이것은 이 실행 문맥이 무리하게 실행 스택에 쌓여서 발생하는 것입니다.)

실행 문맥은 함수를 실행시키기 위해 아주 중요한 사전 작업을 진행하는데 그중 하나는 그 함수가 가진 스코프 체인을 자신의 스코프 체인으로 복사하는 일입니다.
실행 문맥또한 스코프 체인을 통해 유효범위를 계산하기 때문이죠.

그 뒤 실행 문맥은 활성 객체 (Activation Object) 라는 변수 객체를 하나 만들어서, 자신의 스코프 체인의 맨 앞에 삽입합니다.
이 활성 객체는 함수 단위의 유효범위에서 정의된 지역 변수들과, 인자, this, arguments 등의 특수한 변수까지 포함한 변수 객체입니다.

다시 용어 두개가 등장했네요.
정리하고 넘어가죠.

실행 문맥 (Execution Conext)
코드가 실행될 때마다 생성되는, 실행 환경을 정의하고 실제 실행하는 내부 객체.
실행 후 파괴되어 없어집니다.

활성 객체 (Activation Object)
함수가 실행될 때 실행 문맥에 의해 생성되며, 함수 자체에 대한 변수 객체가 됩니다.
때때로 호출 객체 (Call Object) 라고도 불립니다.
인자, 지역변수, this, arguments등이 포함되어 있고 스코프 체인의 맨 앞에 옵니다.

실행 문맥은 코드를 실행하면서 변수나 프로퍼티를 만날 경우 실행중인 영역이 가진, 스코프 체인을 순서대로 넘기면서 해당 변수와 프로퍼티를 찾아 값을 참조하면서 코드를 실행시킵니다.

showVar("This is ") 이라고 실행한다면 showVar 실행 문맥의 스코프 체인은 다음과 같을 것입니다

showVar 실행 문맥의 Scope Chain 구조
[0] 활성 객체 (변수 객체)
thisglobal(브라우저일 경우 window)
arguments[ "This is" ]
i"This is"
[1] 글로벌 오브젝트 (변수 객체)
thisglobal(브라우저일 경우 window)
global(브라우저일 경우 window)구현 object
document구현 object
showVarshowVar 함수
기타 글로벌 객체들의 이름과 값들
...
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 함수가 실행될 때 생성된 활성화 객체
thisglobal(브라우저일 경우 window)
arguments[ ]
f"hello"
worldworld 함수
[1] 글로벌 오브젝트
thisglobal(브라우저일 경우 window)
global(브라우저일 경우 window)구현 object
document구현 object
hellohello 함수
sayundefined
alert내장 함수
기타 글로벌 객체들의 이름과 값들
...
etc
...

여기서 문제 하나가 나옵니다.
활성화 객체는 보통 함수가 실행될 때 실행 문맥에 의해 만들어지고, 활성화 객체가 파괴될 때 같이 없어지는 인스턴트 객체입니다.

그런데 클로저에 쓰일 경우 클로저의 스코프 체인에 활성화 객체가 속하게 되면서 실행 문맥이 끝나도 활성화 객체는 남아있습니다. 클로저가 다수 생성되고, 그것을 해제해주지 않는다면 다수의 활성화 객체가 생성된채로 남고 이것은 문제가 됩니다.

이것이 클로저를 남용할 때 오는 첫번째 문제입니다.

이제 클로저 say를 실행해 봅시다.
클로저 say가 실행되면 역시 실행 문맥이 만들어지고 say의 스코프 체인을 실행 문맥의 스코프 체인에 복사한 뒤, say를 위한 새로운 활성화 객체가 생성되고 스코프 체인의 앞에 삽입됩니다. 다음과 같습니다.

say 실행 문맥의 Scope Chain 구조 (Closure 실행 시)
[0] say 함수가 실행될 때 생성된 활성화 객체
thisglobal(브라우저일 경우 window)
arguments[ ]
[1] hello 함수가 실행될 때 생성된 활성화 객체
thisglobal(브라우저일 경우 window)
arguments[ ]
f"hello"
worldworld 함수
[2] 글로벌 오브젝트
thisglobal(브라우저일 경우 window)
global(브라우저일 경우 window)구현 object
document구현 object
hellohello 함수
sayundefined
alert내장 함수
기타 글로벌 객체들의 이름과 값들
...
etc
...

다른 일반적 함수와 다르게 스코프 체인이 하나 더 늘었을 뿐 아니라 자주 접근해야 할 글로벌 오브젝트 변수 객체가 제일 마지막입니다.

결과적으로 alert을 찾기 위해서 스코프 체인을 두개 다 뒤진 뒤에야, alert을 찾고 실행할 겁니다. 당연히 성능은 저하됩니다. 주의해야 할 점이죠.

요약해 봅시다.

  • 클로저는 클로저를 생성한 함수의 활성화 객체를 그대로 가지고 있게 되어 의도치 않은 메모리 낭비가 발생할 수 있다.
  • 클로저 실행 시, 불필요한 스코프 탐색을 하게 되어 성능이 나빠진다.

좋은 점이 있다면 나쁜 점도 있기 마련입니다.
이런 점들이 있다고 해서 클로저를 아예 안쓰거나 버리는 것 또한 바보짓입니다.

절대 남용하지 말고 변수 스코프를 잘 고려한 변수 위치 선정등으로 스코프 체인 탐색을 줄여가면서 클로저를 활용하는 기법을 익힌다면 좀더 자바스크립트 코딩이 즐거워질 지 모릅니다(?)

포스팅 내용을 좀 더 자세히 알아보고 싶다면 다음 책을 추천드립니다.
더글라스 크락포드의 자바스크립트 핵심 가이드자바스크립트 성능 최적화

기회가 되면 실제 스코프 체인의 구조를 크롬 인스펙터(크롬 개발자 도구)를 사용하여 탐색하면서 여러 경우의 스코프 체인 구조를 확인해보는 포스팅을 해보겠습니다.

마지막으로 문제 하나 던져 보면서 포스팅을 마칩니다.
이 문제의 실행 결과를 정확히 예측했다면 스코프 체인을 완전히 이해했다고 봐도 될 것 같네요.

// 결과로 찍히는 경고창의 문자열은 뭘까요?
var word = "global";
function showVar() {
    return word;
}
function receiveFunc(showVar) {
    var word = "activate";
    alert(showVar());
}
receiveFunc(showVar);

답 보기

댓글 11개:

  1. 변수 객체 (Variable Object)
    어떤 코드가 실행될 때 찾을 변수를 키와 값으로 가진 객체.
    ---
    변수 객체가 variable, function, function parameter로 구성되므로 '찾을 변수'라는 표현보다는 참조할 property로 하는 것이 더 정확할 것 같습니다.
    ---------
    스코프 체인 (Scope Chain)
    함수가 생성될 때
    --
    Execution Context가 Executable Code를 stack에 설정하므로 "어떤 코드"보다 Executable Code가 더 정확하며,
    '함수가 생성될 때'보다 Variable Object에 function property로 설정될 때가 맞을 것 같습니다.
    ------
    "같이 생성되는"
    Executable Context에 Variable Object가 바인딩되는...
    -------------------------------
    수준높은 글 잘 읽었습니다.
    나중에 시간을 내어 다시 읽어 보겠습니다.

    답글삭제
    답글
    1. 감사합니다.
      수준높은 댓글에 정말 포스팅한 보람을 느낍니다.

      삭제
  2. 클로저를 자주 쓰는 편인데, 이렇게 심도있게 다룬 글은 처음 읽습니다. 좋은 글 써주셔서 감사합니다. 본문에 링크걸린 게시물들을 하나씩 읽고 있는데 내용이 전부 좋네요 : )

    답글삭제
    답글
    1. 감사합니다!
      칭찬은 글쓴이를 춤추게 합니다(?)

      삭제
    2. 그리고...
      이 글 댓글에 고수님이 제가 잘못 쓴 내용에 대해 콕 찔러주신 내용이 있으니 참고하시면 더욱 좋을 듯 합니다.

      삭제
  3. 좋은글 잘 읽었네요.
    하단에 언급하신 책도 봤지만 책보다 더 세심한 설명이신거 같아요.

    궁금한것이 2가지 있는데,
    function hello() {
    var f = "hello, ";
    function world() {
    alert(f + "world");
    }
    return world;
    };
    var say = hello();

    이 예제에서 클로저로 인한 스코프 체인검색은 활성화객체 까지 아닌가요?
    본문에
    "결과적으로 alert을 찾기 위해서 스코프 체인을 두개 다 뒤진 뒤에야, alert을 찾고 실행할 겁니다. 당연히 성능은 저하됩니다. 주의해야 할 점이죠."

    라고 하셨는데, 언뜻보기에 글로벌 오브젝트까지 뒤지는걸로 오해할 수 있을것 같아서 언급해봅니다. ^^

    그리고 한가지는, 그렇다면 과연 이 성능도 느리고, 활성화 객체로인한 메모리 누수까지 걱정되면서 이를 활용할 방안이 무었일까요?

    위의 예제로는 그 이점이 잘 안보이지만, 만약 콜백펑션에 클로저로 반환한다면 지역변수 생성의 번거로움이 줄어드는 정도인가요? 궁금하군요.

    그것을 찾으려다가 이 포스트를 봤고, 잘 읽었습니다.
    제가 찾게되면 다시 댓글 달아 둘게요^^

    답글삭제
    답글
    1. 오우...답글 감사합니다! 답글이 언제나 큰 힘이 됩니다.

      일단, alert이 브라우저의 전역객체인 window의 메서드라, 스코프체인의 마지막에 존재하는 글로벌객체까지 뒤지는 동작을 하기에 그렇게 적었는데 틀린 점이 있을까요..?

      클로저의 문제는 활성화 객체가 없어지지 않는 것도 있지만, 변수 탐색의 문제도 크다고 생각하기에 실제 코딩 시 변수 캐시만 잘 활용하면 문제가 좀 줄어들 것 같습니다.

      삭제
  4. 작성자가 댓글을 삭제했습니다.

    답글삭제
  5. 안녕하세요. 자바스크립트를 공부하고 있는 신입개발자 입니다. 맨 마지막문제의 경우, 결론적으로 showVar함수에 word변수가 없으므로, 상위 컨텍스트를 찾아가는데, 다음 context인 receiveFunc함수의 word변수를 참조해야 되는거 아닌가요? 제가 생각하는 현재실행컨택스트의 스코프체인 순서는 showVar context - receive context - 전역 context 순 입니다. alert을 사용하여서 스코프체인에 변화가 생긴건가여?

    답글삭제
    답글
    1. 간단하게 showVar 의 정의가 있는 인스턴스가 global 이니 실행 시점의 global에 word 를 참조하는게 맞습니다. 결과가 바뀌려면 receiveFunc내에 word 의 var 가 빠져 global 을 가르키면 되겠져

      삭제
  6. 안녕하세요! 좋은 글 잘 읽었습니다. 개념들이 여기저기 다 흩어져있었는데. 이것을 읽고 나니깐 하나로 뭉쳐지는 느낌이 드네요!!
    앞으로 좋은 글 부탁드립니다. 다름이 아니라. 마지막 예제를 가지고 실행문맥을 나름대로 그림을 그려보고 있는데요. 상위부터
    receiveFunc - showVar - global obj 이 맞나요?
    읽어보고 다 이해가 되는줄 알았는데. 마지막에 receiveFunc var를 빼니깐 결과값이 바뀌더라고요...; 이게 var뺴면 전역변수로 취급하고 글로벌 오브젝트에 있는 기존의 global 값을 덮어씌어서 그런건가요? ㅠㅠ 먼가 공부할수록 더 헷갈리네용

    답글삭제