자바스크립트 실행 컨텍스트, 변수의 스코프, 클로저

Kim Jin Hyeok·2021년 2월 19일
0
post-thumbnail

실행 컨텍스트 excution context 의 개념

보통의 고급 프로그래밍 언어의 콜 스택 call stack 이라는 개념이 있다. 이는 함 수 호출 시 호출 정보가 스택 형태로 쌓이는 것을 말한다. 자바스크립트 역시 이 범주를 크게 벗어나지 않는다.

실행 컨텍스트는 콜 스택에 들어가는 실행 정보 하나와 비슷하다. 이를 콜 쉽게 정의하자면 실행 가능한 자바스크립트 코드 블록이 실행되는 환경이라고 말할 수 있다. 여기서 코드 블록은 세 가지가 될 수 있다. 전역 코드, eval() 함수로 실행되는 코드, 그리고 대부분의 경우인 함수 안의 코드가 된다.

컨텍스트의 생성은 다음과 같이 설명할 수 있다.
현재 실행되는 컨텍스트에서 이 컨텍스트와 관련 없는 코드가 실행되면, 새로운 컨텍스트가 생성되어 스택에 들어가고 제어권이 그 컨텍스트로 이동한다.

console.log('global context');

function exContext1() {
  console.log('exContext1');
}

function exContext2() {
  exContext1();
  console.log('exContext2');
}

exContext2();

/* 출력결과
global context
exContext1
exContext2
*/

실행 컨텍스트 생성 과정

실행 컨텍스트 생성 과정을 설명하는 과정에서 다음과 같은 중요 개념도 설명한다.

  • 활성 객체와 변수 객체
  • 스코프 체인
function execute(param1, param2) {
    var a=1, b=2;
    function func() { // 내부 함수
    	return a+b;
    }
    return param1+param2+func();
}

excute(3,4); // 10

위 execute() 함수를 예로 과정을 따라가본다.

활성 객체 activation object 생성

실행 컨텍스트가 실행되면 실행에 필요한 여러 가지 정보를 담을 객체를 생성하는데, 이를 활성 객체라고 한다.

arguments 객체 생성

인자들의 유사 배열 객체인 arguments 객체가 생성된다. 앞서 만들어진 활성 객체의 arguements 프로퍼티가 이를 가리킨다.

예시 코드에선 param1, param2 를 가지고 있다.

스코프 정보 생성

현재 컨텍스트의 유효 범위를 나타내는 스코프 정보를 생성한다. 이 스코프 정보는 실행 컨텍스트 안에서 연결 리스트와 유사한 형식으로 만들어진다.

특정 변수에 접근해야 할 경우, 이 리스트를 활용하며 현재 컨텍스트의 변수 뿐만 아니라 상위 실행 컨텍스트의 변수도 접근이 가능하다. 이 리스트에서 찾지 못한 변수는 정의되지 않은 것으로 판단해 에러를 발생시킨다.

이 리스트를 스코프 체인이라고 하며 [[scope]] 프로퍼티로 참조된다.

변수 생성

실행 컨텍스트 내부에서 사용되는 지역 변수의 생성이 이루어진다. 이 변수들은 앞서 설명한 활성 객체에 저장된다. 그래서 활성 객체를 변수 객체라고도 한다.

변수 객체 안에서, 호출된 함수 인자를 각각의 프로퍼티가 만들어지고 그 값이 할당된다. 그리고 함수 안에서 정의된 변수, 함수를 생성한다. 여기서 주의할 것은 이 과정에선 단지 메모리에 생성 instantation 하기만 하고, 초기화 initialization 는 각 변수와 함수에 해당하는 표현식이 실행되기 전까지는 이루어지지 않는다는 점이다.

예시 코드에선 인자 param1, param2와 함수 안에서 정의된 변수 a, b와 함수 func가 생성된다.

this 바인딩

마지막 단계에서 this 키워드를 사용하는 값이 할당된다. 이 값에 어떤 객체가 바인딩 될지는 호출 형태에 따라 다르다.

예시 코드에선, 메서드나 생성자 함수가 아니므로 전역 객체가 바인딩 된다.

코드 실행

이렇게 실행 컨텍스트가 생성되고, 코드에 있는 여러가지 표현식이 실행된다. 이 과정에서 초기화나 연산, 또 다른 함수 실행 등이 이루어진다.

전역 실행 컨텍스트

전역 실행 컨텍스트는 일반적인 컨텍스트와는 약간 다른데, arguments 객체가 없고 전역 객체 하나만을 포함하는 스코프 체인이 있다. 또한 변수 객체가 곧 전역 객체다.

브라우저와 Node.js에서의 전역 코드 차이

브라우저에선 최상위 코드가 곧 전역 코드지만 Node.js에선 다르다.

    var a=10;
    b=15;
    console.log(window.a); // 10
    console.log(window.b); // 15

브라우저에선 위 코드가 잘 실행된다. var a로 정의한 변수가 전역 객체인 window의 한 프로퍼티로 들어갔다.

    var a=10;
    b=15;
    console.log(global.a); // undefined
    console.log(global.b); // 15

하지만 Node.js에선 최상위 코드가 전역 코드가 아니다. var a로 정의된 변수가 전역 객체 global에 들어가지 않는다.

Node.js에선 일반적으로 자바스크립트 파일(*.js) 하나가 모듈로 동작하고 이 파일의 최상위에 변수를 선언해도 그 모듈의 지역변수가 된다. 하지만 var을 사용하지 않을 경우 전역 객체인 global에 들어간다.

스코프 체인

스코프 체인의 이해는 자바스크립트 코드 이해에 큰 도움을 준다. 프로토타입 체인과 거의 비슷한 메커니즘이라 어렵진 않다.

자바스크립트도 다른 언어처럼 스코프, 즉 유효 범위가 있다. 하지만 다른 점이 있는데, C와 비교해보자면, C에선 중괄호({})로 묶여 있는 범위 안에서 선언된 변수는 블록이 끝나는 순간 사라지므로 밖에서는 접근할 수 없다. 이는 함수의 {}뿐만 아니라 if, for문 같은 제어문의 {}도 포함이다.

하지만 자바스크립트에선 함수 내의 {} 블록, 이를테면 for문, if문 같은 구문은 유효 범위가 없다. 오직 함수만이 유효 범위의 단위가 된다. 이 유효 범위를 나타내는 스코프가 [[scope]] 프로퍼티로 각 함수 객체 내에서 연결 리스트 형식으로 관리되는데, 이를 스코프 체인이라고 한다.

전역 실행 컨텍스트의 스코프 체인

전역 실행 컨텍스트는 단 하나만 실행되고 있어서 참조할 상위 컨텍스트가 없다. 즉 자신이 최상위에 위치하는 변수 객체다. 따라서, 이 변수 객체의 스코프 체인은 자기 자신만을 가진다.

함수 호출 시 생성되는 실행 컨텍스트의 스코프 체인

함수 호출은 전역 실행 컨텍스트가 생성되고 이루어진다. 이 함수가 호출되고 만들어진 새 컨텍스트는 함수의 [[scope]] 프로퍼티를 복사한 후, 현재 생성된 변수 객체를 복사한 스코프 체인의 맨 앞에 추가한다.

이를 정리하면 다음과 같다.

  • 각 함수 객체는 [[scope]] 프로퍼티로 현재 컨텍스트의 스코프 체인을 참조한다.
  • 한 함수가 실행되면 새로운 컨텍스트가 만들어지는데, 이 새로운 실행 컨텍스트는 자신이 사용할 스코프 체인을 다음과 같은 방법으로 만든다. 현재 실행되는 함수 객체의 [[scope]] 프로퍼티를 복사하고, 새롭게 생성된 변수 객체를 해당 체인의 제일 앞에 추가한다.
  • 한 줄로 요약하면 스코프 체인은 다음과 같이 표현할 수 있다.

    스코프 체인 = 현재 실행된 컨텍스트 변수 객체 + 상위 컨텍스트의 스코프 체인

이렇게 만들어진 스코프 체인으로 식별자 인식 identifier resolution 이루어진다. 찾으려는 식별자에 대응되는 것을 찾지 못하면 다음 객체로 이동하여 찾는다.

클로저

클로저의 개념

function outerFunc() {
	var x = 10;
    var innerFunc = function() { console.log(x); };
    return innerFunc;
}
var inner = outerFunc();
inner(); // 10

위 예제가 자바스크립트로 클로저를 구현하는 전형적인 패턴이다.

이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수를 클로저 closure 라고 한다.
예제에선 outerFunc의 변수 x를 참조하는 innerFunc가 클로저다. 그리고 클로저로 참조되는 x 같은 변수를 자유 변수 free variable 라고 한다.

이러한 클로저로 자바스크립트로 함수형 프로그래명을 구현할 수 있다.

클로저의 활용

클로저는 성능을 저하시킬 수 있다는 단점이 있어서 적절하게 잘 사용해야 한다.

특정 함수에 사용자가 정의한 객체의 메서드 연결하기

function HelloFunc() {
	this.greeting = 'hello';
}

HelloFunc.prototype.call = function(func) {
	func ? func(this.greeting) : this.func(this.greeting);
}

var userFunc = function(greeting) {
	console.log(greeting);
}

var objHello = new HelloFunc();
objHello.func = userFunc;
objHello.call();

위 코드에서 HelloFunc()은 greeting만을 인자로 넣어 사용자가 인자로 넘긴 함수를 실행시킨다. 그래서 사용자가 정의한 함수도 한 개의 인자를 받는 함수를 정의할 수 밖에 없다.

fuction saySomething(obj, methodName, name) {
	return (function(greeting) {
   		return obj[methodName](greeting, name);
    });
}

function newObj(obj, name) {
	obj.func = saySomethig(this, 'who', name);
    return obj;
}

newObj.prototype.who = function(greeting, name) {
	console.log(greeting + ' ' + (name || 'everyone') );
}


위 예제는 정해진 형식의 함수를 콜백해주는 라이브러리가 있을 경우, 그 정해진 형식과는 다른 형식의 사용자 정의 함수를 호출할 때 유용하게 쓸 수 있다.
예를 들어, 브라우저에서 onclick, onmouseover 등의 프로퍼티에 해당 이벤트핸들러를 사용자가 정의해 놓을 수 있는데, 이 이벤트 핸들러의 형식은 function(event) {}이다. 만약 여기에 event 외에 원하는 인자를 더 추가한 이벤트 핸들러를 사용하고 싶을 때 위와 같은 방식으로 활용할 수 있다.

함수의 캡슐화

"I am XXX. I live in XXX." 라는 문장을 출력하는데 XXX는 사용자에게 인자로 입력을 받아 값을 출력한다고 가정해보자.

가장 쉽게 생각할 수 있는 방법은 문장 템플릿을 전역 변수에 저장하고 사용자의 입력을 받은 후 전역 변수에 접근하여 완성된 문장을 출력하는 것이다.

var buffArr = [ 
    'I am ',
    '',
    '. I live in ',
    '',
    '.'
];

function getCompletedStr(name, city) {
    buffArr[1] = name;
    buffArr[3] = city;
    
    return buffArr.join('');
}

var result = getCompletedStr('kim', 'seoul');
console.log(result);

하지만 위 방식에는 buffArr은 전역 변수라 외부에 노출되어 있다는 단점이 있다. 다른 함수에서 쉽게 접근하여 값을 바꾸거나, 실수로 같은 이름의 변수를 만들어 충돌을 일으킬 수 있다.

이러한 문제를 해결하기 위해 클로저를 사용하여 buffArr을 추가적인 스코프에 넣고 사용하게 되면 이 문제를 해결할 수 있다.

var getCompletedStr = (function(){
    var buffArr = [ 
    	'I am ',
    	'',	
        '. I live in ',
        '',
        '.'
     ];
  
    return (function(name, city) {
        buffArr[1] = name;
        buffArr[3] = city;
    
        return buffArr.join('');
    });
})();

var result = getCompletedStr('kim', 'seoul');
console.log(result);

여기서 반환되는 내부 익명 함수가 클로저, 외부 익명 함수의 buffArr이 자유 변수가 된다.

setTiemout()에 지정되는 함수의 사용자 정의

setTimeout 함수는 브라우저에서 제공하는 함수인데, 첫 번째 인자로 넘겨지는 함수 실행의 스케쥴링을 할 수 있다. 두 번째 인자인 밀리 초 단위 숫자만큼의 시간 간격으로 해당 함수를 호출하는데, 실행될 함수에 인자를 같이 넣어줄 수는 없다.

자신이 정의한 함수에 인자를 넣어주는 것 또한 클로저로 해결할 수 있으며 이는 첫 번째 활용 예제와 유사하다.

function callLater(obj, a, b) {
    return (function(){
        obj["sum"] = a + b;
    	console.log(obj["sum"]);
    });
}

var sumObj = {
	sum: 0
};

var func = callLater(sumObj, 1, 2);
setTimeout(func, 500);

클로저 사용 시 주의 사항

클로저의 프로퍼티 값은 쓰기 가능하므로 그 값이 여러 번 호출로 변할 수 있다

function outerFunc(argNum) {
    var num = argNum;
    return function(x) {
        num+=x;
        console.log(num);
    }
}
var exam = outerFunc(40);
exam(5); // 45
exam(-10); // 35

exam을 호출할 때마다, 자유 변수 num의 값은 변화한다.

하나의 클로저가 여러 함수 객체의 스코프 체인에 들어가는 경우가 있다

function func() {
    var x = 1;
    return {
    	func1: function() { console.log(++x); },
        func2: function() { console.log(--x); }
    };
}

var exam = func();
exam.func1(); // 2
exam.func2(); // 1

반환되는 객체(클로저)에 두 개의 함수가 같은 자유변수 x를 참조한다.

루프 안에서 클로저 사용시 주의한다

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

1, 2, 3을 1초 간격으로 출력하려는 의도의 코드이나 4가 3번 1초 간격으로 출력된다. 그 이유는 setTimeout에 들어가는 함수가 실행되는 시점은 countSeconds 함수의 실행이 종료된 이후이다.

참고: 송형주, 고현준, 인사이드 자바스크립트(2014)

0개의 댓글