본문 바로가기

Web + APP/JavaScript

실행 컨텍스트와 클로저

반응형
SMALL

안녕하세요 !

 

다들 새해 복 많이 받으시고 있나요 !! 안 받으시고 있으시다면, 어서 챙겨가세요 !!

 

ㅎㅎㅎㅎㅎㅎㅎㅎㅎ

이번 글에선 실행 컨텍스트와 클로저를 배워 보려고 합니다.

 

저는 항상 클로저라는 개념이 어렵게 다가왔었는데, 그 이유가 실행 컨텍스트라는 개념을 몰라서 어렵게 느껴졌던거 같아요.

 

그러니 이번엔 '실행 컨텍스트와 클로저'를 같이 배워보려합니다.

 

정확히는 아래를 배울 거에요 !

 

  1. 실행 컨텍스트의 개념
  2. 활성 객체와 변수 객체
  3. 스코프 체인
  4. 클로저

시작합니다 !


실행 컨텍스트 개념

기존에 언어를 공부해보셨다면 Call Stack의 개념을 들어보셨을 거에요. C언어로 예를 들면 함수의 호출 정보, 지역 변수, 인자값 등이 차곡차곡 쌓이는 스택을 의미해요.

 

그래서 콜 스택의 호출 정보 등으로 코드의 실행 과정을 추적하며 디버깅을 하기도 하죠. JS도 다르지 않습니다.

 

대충 이렇게 생긴 친구. 감이 바로 잡히죠?

실행 컨텍스트는 콜 스택에 들어가는 실행 정보 하나를 의미합니다. ECMAScript에서는 "실행 가능한 코드를 형상화하고 구분하는 추상적인 개념"이라고 설명하네요.

 

음.. 간단하게 "실행 가능한 자바스크립트 코드 블록이 실행되는 환경"이라고 이해합시다.

 

ECMAScript에서 실행 컨텍스트가 형성되는 경우를 세 가지로 규정하는데, 전역 코드, eval() 함수로 실행되는 코드, 함수 안의 코드를 실행할 경우입니다. 저희 대부분 함수로 실행 컨텍스트를 만들잖아요? 그럴 때 생성된다고 생각하면 되고요. 제일 위에 위치하는 실행 컨텍스트가 현재 실행되고 있는 컨텍스트입니다. (Stack이니 LIFO, 여담으로 제가 코딩 개차반이었을 때, 면접에서 FIFO를 LILO라고 했던 적이 기억이 나네요 ㅎㅎ... 사실 의미는 맞는뎈ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 개념을 이해하는 것도 중요하지만, 이래서 개념의 명칭을 외우는게 중요합니다.)

 

ECMAScript에서는 실행 컨텍스트의 생성을 다음처럼 설명을 합니다.

 

"현재 실행되는 컨텍스트에서 이 컨텍스트와 관련 없는 실행 코드가 실행되면, 새로운 컨텍스트가 생성되어 스택에 들어가고 제어권이 그 컨텍스트로 이동한다."

console.log("This is global context")

function ExContext1() {
	console.log("This is ExContext1");
};

function ExContext2() {
	ExContext1();
    console.log("This is ExContext2");
};

ExContext2();

// This is global context
// This is ExContext1
// This is ExContext2

쉬운 예제에요. 여러분들이 아셔야하 하는 점은 전역 실행 컨텍스트가 가장 먼저 실행된다는 거에요. (단지 가장 먼저 실행되는 실행 컨텍스트라고 알고계시면 되요)

 

실행 컨텍스트 생성 과정

이 과정에서 중요한 점은

  • 활성 객체와 변수 객체
  • 스코프 체인

입니다.

function execute(param1, param2) {
	var a = 1, b = 2;
    function func() {
    	return a + b;
	}
    return param1 + param2 + func2();
}

excute(3, 4);

위의 예제를 두고 설명해보죠.

1. 활성 객체(변수 객체) 생성

실행 컨텍스트가 생성되면 JS 엔진은 해당 컨텍스트에서 실행에 필요한 여러 가지 정보를 담을 객체를 생성합니다. 이를 활성 객체라고 합니다.

2. arguments 객체 생성

제가 저번에 작성한 글에서 arguments 객체에 대해 설명을 했었죠? 앞서 만들어진 활성 객체(변수 객체)는 arguments 프로퍼티로 이 arguments 객체를 참조해요.

3. 스코프 정보 생성

현재 컨텍스트의 유효 범위를 나타내는 스코프 정보를 생성해요. 이 정보는 연결 리스트와 유사한 형식으로 만들어지고, 특정 변수에 접근해야 하는 경우, 이 리스트를 활용해요.

 

물론 상위 실행 컨텍스트의 변수도 접근이 가능하고, 결국 리스트 내에서 찾지 못한 변수는 정의되지 않은 변수이기에 에러를 검출해요.

 

이 리스트를 스코프 체인이라고 하는데, [[scope]] 프로퍼티로 참조됩니다.

 

현재 생성된 활성 객체(변수 객체)가 스코프 체인의 제일 앞에 추가됩니다.

4. 변수 생성

현재 실행 컨텍스트 내부에서 사용되는 지역 변수의 생성이 이루어집니다. ECMAScript에서는 생성되는 변수를 저장하는 변수 객체를 언급합니다. 실질적으로 활성 객체, 변수 객체를 같이 봐도 됩니다.

 

변수 객체 안에서 호출된 함수 인자는 각각의 프로퍼티가 만들어지고 그 값이 할당됩니다. 없으면 undefined가 할당되죠.

 

여기서 아셔야하는 점은 이러한 변수나 내부 함수는 메모리에 생성될 뿐, 초기화가 되지 않아서 undefined가 먼저 할당 된다는 점입니다. 표현식 실행은 변수 객체 생성이 다 이루어진 후 이뤄집니다.

5. this 바인딩

이는 저번 글에서 했었죠? 넘어가도록 할게요 !

 

하나만 짚고 가자면, this가 참조하는 객체가 없으면 전역 객체를 참조합니다. 이 것도 말했었죠? 넘어가 !

6. 코드 실행

이렇게 실행 컨텍스트가 생성되고, 변수 객체가 만들어진 후, 코드에 있는 여러 가지 표현식이 실행됩니다. 이 순간 할당도 이루어지는거죠.

 

참고로 전역 컨텍스트는 일반적인 실행 컨텍스트와는 약간 다른데, arguments 객체가 없어요. 전역 객체 하나만을 포함하는 스코프 체인이 존재하고요.

 

즉, 전역 실행 컨텍스트에서는 변수 객체가 곧 전역 객체입니다.

 

Node.js의 경우엔 달라요. 최상위 코드가 브라우저와는 달리 전역 코드가 아닙니다. filename.js의 파일 하나가 모듈로 동작하고 이 팔일의 최상위에 변수를 선언해도 그 모듈의 지역 변수가 됩니다. 즉, 전역 실행 컨텍스트에서의 변수 객체가 전역 객체가 아니고, 그 모듈 내에서의 지역 변수가 되는 것이죠 !

스코프 체인

JS도 다른 언어와 마찬가지로 스코프, 즉 유효 범위가 있어요.

 

C언어의 경우엔 if, for 문의 블록도 묶여서 그 안에서 선언된 변수가 밖에서 접근이 불가능 했었죠? JS는 배려심이 있어서 for, if 구문의 유효 범위가 없어요. 오직 함수에만 적용됩니다.

 

이 유효 범위를 나타내는 스코프가 [[scope]] 프로퍼티로 각 함수 객체 내에서 연결 리스트 형식으로 관리되는데, 이를 스코프 체인이라고 해요.

 

각각의 함수는 [[scope]] 프로퍼티로 자신이 생성된 실행 컨텍스트의 스코프 체인을 참조합니다. 함수가 실행되는 순간 실행 컨텍스트가 만들어지고, 이 실행 컨텍스트는 실행된 함수의 [[scope]] 프로퍼티를 기반으로 새로운 스코프 체인을 만들어요.

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

var var1 = 1;
var var2 = 2;
console.log(var1); // 1
console.log(var2); // 2

이를 실행하면 전역 실행 컨텍스트가 생성되고, 변수 객체가 만들어지겠죠.

 

현재 전역 실행 컨텍스트 단 하나만 실행되고 있어 참조할 상위 컨텍스트가 없어요.

 

따라서, 이 변수 객체의 스코프 체인은 자신만을 가리키고, 이 변수 객체가 전역 객체가 됩니다.

 

2. 함수를 호출한 경우 생성되는 실행 컨텍스트의 스코프 체인

var var1 = 1;
var var2 = 2;

function func() {
	var var1 = 10;
    var var2 = 20;
    console.log(var1); // 10
    console.log(var2); // 20
}

func();

console.log(var1); // 1
console.log(var2); // 2

너무 당연한 예제인데, 내부적으로 어떻게 돌아가는지 한 번 짚어보도록 하죠.

 

1. 전역 실행 컨텍스트가 실행됩니다.

 

2. func() 함수 객체가 만들어져요. 이 함수 객체의 [[scope]]는 함수 객체가 생성될 때, 그 함수를 부르는 컨텍스트의 변수 객체에 있는 [[scope]]를 가지게 됩니다.

 

3. 가지게 된 [[scope]]에다 자신이 생성한 변수 객체를 추가합니다.

 

스코프 체인 = 현재 실행 컨텍스트의 변수 객체 + 상위 컨텍스트의 스코프 체인이 되는 겁니다.

var value = "value1";

function printFunc() {
	var value = "value2";
    
    function printValue() {
    	return value;
	}
    
    console.log(printValue());
}

printFunc(); // value2

쉽죠? 스코프 체이닝을 이해했다면 쉽게 이해가 가능한 예제라고 생각을 합니다.

 

아래의 예제는 좀 난이도가 있을거에요.

var value = "value1";

function printValue() {
	return value;
}

function printFunc(func) {
	var value = "value2";
    console.log(func());
}

printFunc(printValue);

각 함수 객체가 처은 생성될 당시 실행 컨텍스트가 무엇인지를 생각해봐요.

 

각 함수가 처음 생성될 때, [[scope]]는 전역 객체의 [[scope]]를 가리키게 됩니다. 왜냐면 자신이 생성된 실행 컨텍스트의 [[scope]]를 참조하게 되니까요.

 

여기까지 이해가 됐다면, 함수 호이스팅을 한 번 더 짚고 넘어가도록 합시다.

아래 예제를 보죠.
foo();
bar();

var foo = function() {
	console.log("foo and x = " + x);
}

function bar() {
	console.log("bar and x = " + x);
}

var x = 1;
해당 예제는 아래와 같습니다.
var foo;

function bar() {
	console.log("bar and x = " + x);
}

var x;

foo(); // TypeError
bar();

foo = function() {
	console.log("foo and x = " + x);
}

x = 1;
함수 생성 과정에서 변수 foo, 함수 객체 bar, 변수 x를 차례로 생성하게 되는데, 이 때 foo / x는 undefined가 할당됩니다. 그 상태에서 foo를 호출하려고 하니 엑 ! 에러가 떠버리는 것이죠. 왜냐면 foo에 함수 객체가 할당 되는 순간은 호출이 일어나고 나서이니까요 !

클로저

대망의 클로저입니다. 함수형 프로그래밍에서 반드시 사용하는 클로저

 

제가 이 글을 작성하고 있는 이유는 JS만을 위해서가 아닙니다. 최근 공부하고 있는 swift라는 언어가 이 클로저라는 개념을 너무나도 많이 사용하는데, 안 익숙한 swift로 클로저를 공부하는 것보다, JS로 초벌 굽고 공부하는게 더 나을 거란 생각을 했거든요 ㅎㅎ;;

 

그래서 아마 추후에 swift 클로저 개념도 같이 포스팅할 거 같습니다 !

 

개념은 다음과 같습니다. 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수를 클로저입니다.

 

여기서 중요시 해야하는건, 클로저 역시 함수라는 점입니다 !!! 즉, 클로저도 실행 컨텍스트가 생성되며 [[scope]]를 가지고 있다는 점이죠.

 

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

var inner = outerFunc();
inner(); // 10

이처럼 outerFunc 실행 컨텍스트가 사라진 이후에 innerFunc 실행 컨텍스트가 생성되는데, innerFunc 스코프 체인은 outerFunc 변수 객체를 여전히 참조한다는 점입니다.

 

근데 이렇게 참조에 의해 가비지 컬렉터가 메모리를 정리를 하지 않는다면, 메모리에 부담이 많이 생기겠죠? 허나 클로저의 대단한 기능을 무시할 수 없으니, 클로저를 어떻게 쓸건지 영리한 판단이 중요시 여겨집니다.

클로저의 활용

추후에 소개할 함수형 프로그래밍에서 더 얘기를 나눌 것이라 간단하게 보고 갈게요.

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

해당 부분은 정확하게 이해하지 못해서 이런 경우가 있다는 점만 적어놓을게요 ㅠ.. 꼭 수정해놓겠습니다.

2. 함수의 캡슐화

var buffAr = [
	"안녕 ",
   	"",
    ", 난 ",
    "",
    "야."
];

function getCompletedStr(you, me) {
	buffAr[1] = you;
    buffAr[3] = me;
    
    return buffAr.join('');
}

var str = getCompletedStr('꼬동', '티스토리');
console.log(str); // 안녕 꼬동, 난 티스토리야.

이 예제는 단점이 있습니다. 이 코드를 라이브러리로 만들려고 할 때 실수로 같은 이름의 변수를 누군가 만든다면 ? 충돌 가능성이 충분하죠. 그래서 클로져를 활용하여 buffAr을 추가적인 스코프에 넣고 사용해야합니다.

 

var getCompletedStr = (function() {
	var buffAr = [
    	"안녕 ",
        "",
        ", 난 ",
        "",
        "야."
	]
    
    return (function(you, me) {
    	buffAr[1] = you;
        buffAr[3] = me;
        
        return buffAr.join('');
	});
})();

var str = getCompletedStr("꼬동", "티스토리");
console.log(str);

getCompletedStr에 익명 즉시 함수를 실행시켜 반환되는 함수를 str에 할당합니다. 이 반환되는 함수는 클로저가 되고, 이 클로저는 buffAr를 스코프 체인에서 참조할 수 있죠 !

 

3. setTimeout()에 지정되는 함수의 사용자 정의

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);

클로저를 활용할 때 주의사항

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

function outerFunc(argNum) {
	var num = argNum;
    return function(x) {
    	num += x;
        console.log('num: ' + num);
	}
}

var exam = outerFunc(40);
exam(5); // num: 45
exam(-10); // num: 35

이처럼 코드를 실행하면 num의 값은 호출할 때마다 바뀝니다.

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

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

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

3. 루프 안에서 클로저를 활용할 때는 주의

드디어 나왔습니다. 클로저 단골 면접 문제 !!

 

실제로 카카오 채용형 인턴 면접에서도 나온 그 질문 !!! (진심 개꿀팁 ㄹㅇ... 꼭 제 블로그를 구독해주세요 ! 이런 꿀팁을 더 알 수 있어요 ㅎ)

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

countSeconds(3);

원래 의도는 1, 2, 3을 1초 간격으로 출력하는 의도로 만든 예제이지만, 뜻대로 안됩니다. 그 이유가 뭘까요?

 

setTimeout 함수의 인자로 들어가는 함수는 i를 참조합니다. 허나 함수가 실행되는 시점은 countSeconds가 종료된 시점이고, i는 이미 4가 된 상태이기 때문에, setTimeout()로 실행되는 함수는 모두 4를 출력하게 됩니다.

 

즉, 우선 for문이 실행이 되고 아래와 같이 3개의 함수가 생깁니다.

setTimeout(function() {
	console.log(i);
}, 1 * 1000);
        
setTimeout(function() {
	console.log(i);
}, 2 * 1000);
        
setTimeout(function() {
	console.log(i);
}, 3 * 1000);

이 때 console.log(i)에서의 i는 실행 될 때 값이 결정되는데, 그 순간의 i는 4니까요 !

 

이를 해결하기 위해선 즉시 실행 함수를 사용하여 i 값을 복사하여 원하는 결과를 얻는 것입니다.

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

countSeconds(3);

아래 방법은 댓글에 LazyCode님에서 알려주신 방법입니다. function factory를 이용하여 해당 함수를 계속해서 생성하고 실행하는 방법도 존재합니다. 이렇게되면 val은 전달 받은 i로 실행되기 때문에, 순회하는 i를 받아서 실행할 수 있습니다.

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

아래 방법도 존재합니다. var가 아닌 let으로 i를 선언하여, 함수 레벨 스코프를 블록 레벨 스코프로 하면, setTimeout의 i는 각각의 블록마다 생성되기 때문에, 해당 방법을 사용할 수 있습니다.

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

후.. 먼 길 오셨습니다. 그토록 오래 씨름하던 클로저를 배워봤는데, 실행 컨텍스트와 같이 공부하니 별 일아니죠?

 

클로저란 개념을 모르면 이름 충돌, 성능 저하, 비효율적 자원 활용 등의 문제가 생기니, 반드시 알고 넘어가도록 합시다.

 

이상 실행 컨텍스트와 클로저였습니다 ^_^

반응형
LIST