개발/JavaScript
출처를 먼저 밝힙니다.

http://cafe.naver.com/hacosa 사이트에서

[ES5] 콜 스택과 스코프에 대한 고찰
 제목으로


엑소버드님이 쓰신글을 공부용으로 복사했습니다. 비공개 처리합니다.



저는 하나를 파고들면 끝장을 보는 성격이라 콜스택과 스코프가
말로만 듣던 것과 동일하게 작동하는지 궁금해서 한번 그 내부 구조를 들여다보았습니다~

먼저 자바스크립트에서 실행 컨텍스트, 클로저, 스코프, 콜스택 등등에 대해서
이 글에서 자세하게 설명하지 않습니다.
구글에서 검색해보시면 아주 많이 나옵니다.
이 글은 '아, 브라우저에서 정말 그렇게 돌아가구나' 라는 걸 확인하기 위한 글입니다.
콜 스택 === 실행 컨택스트 스택
정도로 보시면 될 것 같습니다.

다른 브라우저는 모르겠고, 크롬이 주 브라우저고, 크롬에서 디버깅하기가 용이하여

부득이하게 크롬으로 진행해보았습니다~

'use strict';
var A = function() {
var A = 1;
var AA = function() {
console.log(A);
};
AA();
};

A();

위와 같이 외부 함수 A와 내부 함수 AA가 있을 때
외부 함수 A를 실행하면 스코프가 어떻게 형성되고, 콜 스택은 어떻게 변화하는지 보겠습니다.


크롬 개발자 도구에서 소스 탭으로 들어간 후에 브레이크 포인트를 아래와 같이 걸어보고,
프로그램을 돌려봅니다.

콜 스택 맨 아래에 있는 익명함수는 전역 스코프의 실행 컨택스트입니다.



ES5에서 전역 스코프에서 var로 선언한 변수는
Global 객체(브라우저에서는 Window 객체)의 프로퍼티로 들어갑니다.
window.A = 123 등으로 변경이 가능하지만,
delete 연산자로 삭제가 불가능하므로 유사 프로퍼티(?)라고 저는 보고 있습니다.
그리고 2번 라인이 실행되기 직전에 브레이크가 걸렸으므로 A는 아직 undefined입니다.


F8 버튼을 눌러 다음 브레이크 포인트로 이동하면 10번 라인에서 걸립니다.
변수 A에는 함수가 할당된 걸 볼 수 있습니다.


F8을 눌러 다음 브레이크 포인트로 이동하면 10번에서 함수 A를 호출했으므로
함수 내부인 3번으로 들어온 후 브레이크 됩니다.
콜 스택에 함수 A의 실행 컨텍스트가 쌓였습니다.
그리고 스코프 체인에는 Local 객체(현재 제어권을 가지고 있는 함수(A)의 실행 컨택스트 내부의 변수 객체)
가 Global 스코프보다 위로 왔습니다.
아직 지역변수 A와 AA에는 값을 할당하기 이전이고, ES5의 Strict 모드에서 함수에 this를 명시해주지 않으면
this에는 undefined가 할당됩니다. this를 명시해주고 싶다면,
10번 라인과 같이 함수를 호출할 때 call, apply, bind 등의 메소드를 사용하시면 됩니다.


다음 브레이크 포인트로 이동하면 4번 라인에서 걸립니다.
3번이 실행된 이후이므로 지역변수 A에는 1이 할당됨.


그 다음 브레이크 포인트로 이동하면 7번에서 걸리고,
지역변수 AA에는 함수가 할당된 것을 볼 수 있습니다.


그 다음 브레이크 포인트로 이동하면, 7번에서 함수 AA를 호출했으므로
AA 함수 내부인 5번 라인에서 브레이크가 걸립니다.
콜스택을 보면 제일 나중에 실행한 AA 함수의 실행 컨택스트가 제일 위에 쌓여 있고,
그 아래 A 함수의 실행 컨택스트가 쌓여있습니다.
AA 함수의 실행 컨택스트의 스코프 체인을 봅시다.
로컬 스코프를 보면 역시 this를 명시해주지 않았으므로 this에는 undefined가 바인딩 되고,
그 아래 Closure (A) 스코프와 전역 스코프가 있습니다.

이 쯤에서 그럼 클로저에 대한 정의를 다시 한번 살펴봅시다.

인사이드 자바스크립트 책에서는...
이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수
라고 정의하고 있고, 한 블로그( http://blog.javarouka.me/2012/01/blog-post_13.html ) 에서는
생성 당시의 스코프에 대한 연결을 갖는 블록
이라고 정의하고 있습니다.



빨간색 부분이 이미 생명 주기가 끝난 외부 함수, 생성 당시의 스코프이고,
파란색 부분 외부 함수의 변수를 참조하는 함수, 연결을 갖는 블록이 된다.

함수 AA(클로저)의 실행 컨택스트의 스코프 체인은
AA 자신만의 지역 스코프를 가지고,
또 5번 라인에서 자신의 스코프를 벗어난 변수 A를 참조해야 하므로
이미 생명 주기가 끝난 외부 함수를 참조하는 스코프를 생성했다. (Closure (A))
이렇게 클로저를 많이 쓰다보면 스코프 체인에 여러 스코프가 생기고,
이는 로컬 스코프만 가지는 함수보다 더 많은 메모리 공간을 차지함으로써
메모리 누수와도 연결이 되므로 적절하게 써야합니다.


다음 브레이크 포인트로 이동하면 함수의 실행을 마치고
함수 구문의 끝인 6번 라인으로 이동합니다.
이 때 로컬 스코프를 보면 Return value: 라는 프로퍼티가 생기는데
return을 명시하지 않았으므로 undefined가 반환됩니다.


다음 브레이크 포인트로 이동하면 콜 스택에서 AA 함수의 실행 컨택스트가 반환(pop)된 걸 볼 수 있습니다.
함수 A의 끝인 8번 라인에서 브레이크가 걸리고, 역시 Return value가 생기고
return 값을 명시하지 않았으므로 undefined가 반환됩니다.

그 다음 브레이크 포인트로 이동하면 브레이크 걸리는 것 없이 프로그램이 끝납니다.
10번 라인에서 A함수를 호출했고, A함수의 호출을 마쳤기 때문이죠.

이번에는 다른 예제를 한 번 봐보겠습니다.
'use strict';
var B = function() {
var A = 1;
C();
};

var C = function() {
console.log(A);
};

var A = 2;

B();

함수 B와 함수 D가 있고 전역 스코프에 A라는 변수가 있고,
함수 B는 지역변수 A를 가지고 있고, 함수 C를 호출합니다.
함수 C는 참조할 수 없는 변수인 A를 콘솔창에 찍는 함수입니다.
마지막으로 함수 B를 실행하고 끝납니다.
브레이크 포인트를 아래와 같이 걸어줍시다.

그럼 함수 B를 호출하는 13번 라인에서 브레이크가 걸립니다.


그 다음은 함수 B 내부인 4번에서 브레이크가 걸립니다.
콜스택에는 B가 쌓여있고, 스코프에는 B 함수의 로컬 스코프와 전역 스코프가 있습니다.
로컬 스코프의 로컬 변수 A는 1이고, 글로벌 스코프의 글로벌 변수 A는 2입니다.
이제 다음 브레이크 포인트로 이동해서 함수 C를 호출해봅시다.


함수 C 호출 한 후에 8번 라인에서 브레이크가 걸렸습니다.
콜스택에는 B 위에 C가 Push 되었고,
스코프를 보면 C의 로컬 스코프, 글로벌 스코프가 있고, 글로벌 스코프에 A 변수가 있습니다.


그리고 다음 브레이크 포인트로 이동하면 C의 실행 컨텍스트가 스택에서 Pop 됐습니다.
그리고 콘솔탭으로 이동하면 2가 찍혀있습니다.
1이 찍힐 것이라고 예상하신 분들은 아래를 보시면 되겠습니다...

ES5에서 변수의 스코프는 함수 단위입니다.(ES6의 let과 const는 블록 스코프!)

첫 번째 예제를 도식화하면 좌측과 같고, 두 번째 예제를 도식화 하면 우측과 같습니다.



마지막으로 전형적인 클로저 활용 방안(?)에 대해서 정리하고 끝마치겠습니다.
여러가지가 있지만 클로저를 이용하여 콜백 함수에서 반복 변수(?) i를 묶어보도록 하겠습니다.

'use strict';
for(var i=1; i<=3;) {
setTimeout(function() {
console.log(i);
}, i++ * 1000);
}
콜백 함수는 이벤트 핸들러 등록할 때 함수를 실행하는 게 아니라,
등록만 해두고 이벤트가 발생했을 때 이벤트 핸들러가 실행하는 점을 유념하셔야합니다.
setTimeout 함수의 콜백함수로 익명함수를 등록해놓고, 1초가 지날 때마다 로그를 찍습니다.
1
2
3
이 찍히리란 걸 예상할 수 있는데, 어떨지 한 번 봐야할 것 같습니다.


반복문 1회차 전역 스코프의 i의 값은 1.
i++ * 1000(ms), 즉 1초 후에 이벤트 핸들러를 실행하라고 합니다.



반복문 2회차 전역 스코프의 i의 값은 2.
i++ * 1000(ms), 즉 2초 후에 이벤트 핸들러를 실행하라고 합니다.



반복문 3회차 전역 스코프의 i의 값은 3.
i++ * 1000(ms), 즉 3초 후에 이벤트 핸들러를 실행하라고 합니다.

반복문 4회차에는 전역 스코프의 i의 값은 4이고 3보다 크므로 false.
반복문을 탈출합니다.


1초가 지났으므로 이벤트 핸들러가 동작합니다.
i를 콘솔 탭에 찍는데 현재 전역 스코프의 i의 값은 4입니다.


2초가 지났으므로 이벤트 핸들러가 동작합니다.
i를 콘솔 탭에 찍는데 현재 전역 스코프의 i의 값은 4입니다.


3초가 지났으므로 이벤트 핸들러가 동작합니다.
i를 콘솔 탭에 찍는데 현재 전역 스코프의 i의 값은 4입니다.

결과적으로
4
4
4
가 찍힙니다.
저희가 예상한 결과와는 완전 딴판이 나왔습니다.
이벤트가 몇 초 후에 동작해라~ 라는 것은 정확히 동작하지만
콜백 함수는 등록 시점이 아닌 실행 시점의 i를 참조하므로
i를 묶어둘(복사해 둘) 필요가 있습니다.

이 때 사용하는 게 바로 클로저,
상위 스코프에 i를 복사해두고
하위 스코프(이벤트 핸들러)에서 상위 스코프의 복사된 i를 참조하게 하는 기법입니다.

도식화 해보자면 아래와 같습니다.


'use strict';
for(var i=1; i<=3; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}(i));
}


콜스택에 빨간 화살표시 한 익명함수의 실행 컨택스트가 쌓입니다.
그리고 로컬 스코프를 보면 전역에 존재하는 현재 반복문의 i의 값을 복사했습니다.


2회차
i의 값인 2를 또 복사함.



3회차
i의 값인 3을 또 복사하고 반복문이 끝납니다.


1초 후...
콜스택에 익명함수의 실행 컨택스트가 쌓였고
이 실행 컨택스트는 1초 후 실행되는 이벤트 핸들러 함수의 실행 컨택스트입니다.
그리고 스코프를 보면 로컬 이외에 상위 스코프인 클로저가 생기고,
클로저에는 파란색 동그라미 쳐놓은 상위 스코프의 복사해놓은 i를 참조하게 됩니다.


2초 후...
역시 상위 스코프에 복사한 i값인 2를 참조합니다.


3초 후...
상위 스코프에 복사한 i값인 3을 참조합니다...

결과적으로 우리가 원하는 결과인
1
2
3
을 출력합니다.

다음의 예제에서 클로저가 있는지 없는지, 이유까지 한 번 생각해보세요.
'use strict';
var outer = function() {
var x = 2;
var inner = function() {
var y = 2;
console.log(y);
};
return inner;
};
var inner = outer();
inner();

즉시 실행 함수 예제도 한번 디버깅 해보세요.

'use strict';
(function() {
var outer = function() {
var x = 2;
var inner = function() {
console.log(x);
};
return inner;
};
var inner = outer();
inner();
}());
오늘 ES6의 스코프까지도 다뤄보려고 했는데
이 글 쓰느라 5시간 이상을 쓴 것 같네요.
이 범위 공부하는데만 이틀은 넘게 걸린 것 같고요...
ES6에서는 얼마나 달라졌을지 또 공부해봐야겠습니다~


'개발 > JavaScript' 카테고리의 다른 글

자바스크립트 비동기가 되는 원리  (0) 2017.02.24