Execution Context, Closure

raccoonback·2020년 6월 16일
1

javascript

목록 보기
3/11
post-thumbnail

Execution Context

실행 컨텍스트는 Javascript 언어로 작성된 코드를 실행하기 위해 필요한 환경이다.

함수가 호출되면, 새로운 실행 컨텍스트는 생성되어 CallStack 에 쌓이고, 함수 실행이 끝나면 제거된다.

function foo() {
    console.log('foo');
    function bar() {
        console.log('bar');
    }

    bar();
}

foo();

위와 예제를 통해 살펴보면, 우선 전역에서 실행되는 환경을 제공하기 위한 Global Context가 CallStack 을 구성한다. 그리고 foo() 함수를 호출하면 foo 함수에 대한 Execution Context 가 쌓이게 되고, 이후 bar() 함수를 위한 실행 컨텍스트도 콜스택에 순차적으로 쌓이게 된다.

실행 컨텍스트가 함수 호출이 발생할 때마다 생성되는 것은 이해가 됐다.

그럼 실행 컨텍스트는 무엇을 위해 필요하고, 왜 함수 호출마다 생성되는 것일까?

이제 하나씩 천천히 살펴보자.

실행 컨텍스트는 무엇을 위해 필요한가

실행 컨텍스트는 세 가지 영역으로 구분된다.

  • LexicalEnvironment
  • VariableEvironment
  • ThisBinding

VariableEvironment는 LexicalEnvironment 과 유사한 기능이므로 LexicalEnvironment만 언급한다.

실행 컨텍스트는 함수 호출마다 생성되므로, LexicalEnvironment는 해당 함수에서 선언된 변수, 내부함수, 파라미터를 저장한다.

function foo(b) {
    var a = 1;
    function bar() {
        var c = 3;
    }

    bar();
}

foo(2);

아래 예제를 살펴보면 foo() 함수에 대한 실행 컨텍스트는 아래와 같이 구성될 것이다.

{
	LexicalEnvironment: {
      		a : 1,
        	b : 2,
		bar: function() {}
	},
	ThisBinding: ...
}

따라서 실행 컨텍스트는 함수 실행에 필요한 여러가지 정보를 저장하기 위한 환경을 구성하기 위해 필요하다.

왜 함수 호출마다 생성되는 것일까

LexicalEnvironment 대해서 자세히 살펴보면, EnvironmentRecord, Outer 로 구성된다.

EnvironmentRecord는 함수 내부에 선언한 변수 또는 함수를 저장하고, Outer는 상위 LexicalEnvironment를 가리킨다.

만약 현재 실행 컨텍스트에서 참조하는 변수가 없는 경우, Outer가 연결하는 상위 LexicalEnvironment 를 참조한다. 또한, 상위 LexicalEnvironment 에도 찾고 있는 변수가 없다면 Chaining 방식으로 최상위 Global LexicalEnvironment 까지 탐색한다. 만약 찾지 못한다면 Reference Error 를 발생시킨다. 일반적으로 이런한 과정을 scope chain 이라 부른다.

유념해야 할 것은 outer 에 연결되는 상위 환경은 함수 호출 시점이 아닌 선언 시점에 결정된다.(렉시컬 스코프)

아래 예제를 통해, 실행 컨텍스트 내부의 LexicalEnvironment 가 어떻게 변수, 함수를 참조하는지 살펴보자.

var out = 1;
function foo() {
    var a = 1;
    function bar() {
        var b = 2;
        function baz() {
            var c = 3;
            console.log(a, b, c);
        }

        baz();
    }

    bar();
}

foo();

baz() 함수에서 console.log() 가 호출되면,

변수 cbaz 실행 컨텍스트에서 참조가 가능하다.

변수 bbaz 실행 컨텍스트에 없기 때문에 outer 가 연결한 상위 bar LexicalEnvironment 를 통해 b 를 참조한다.

같은 방식으로, 변수 abaz 실행 컨텍스트에 없기 때문에 상위 bar LexicalEnvironment 를 통해, foo LexicalEnvironment 를 참조하고 a 를 발견한다.

최상위인 Global 실행 컨텍스트는 전역 객체에 대한 실행 컨텍스트이다.(ex, 브라우저에서는 window 객체)

다른 방법중, Chrome 브라우저를 통해서 console.log() 함수에 중단점을 놓고 디버깅해보면 아래와 같이 나타난다.

위와 같이, baz 실행 컨텍스트는 local에 c 변수를 가지고 있고, 외부에서 별도의 this 바인딩을 하지 않았기 때문에 Global인 Window 객체를 가리킨다.

다음으로, bar 실행 컨텍스트는 b 변수와 baz() 함수 객체를 가지고 있고, 동일하게 Window 객체를 this 바인딩하고 있다.

foo 실행 컨텍스트는 a 변수와 bar() 함수 객체를 가지고 있고, 동일하게 Window 객체를 this 바인딩하고 있다.

마지막으로, Global 컨텍스트는 this 바인딩없이 test 변수를 가지고 있다.

요약해보면, EnvironmentRecord 는 현재 실행 컨텍스트에 필요한 함수 파라미터, 변수, 함수를 저장하고, Outer 는 상위 실행 컨텍스트를 참조하기 위해 필요한 것이다.

따라서, 함수 호출 때마다 실행 컨텍스트를 구성한다는 것은 실행에 필요한 여러가지 정보를 저장하고, 상위 LexicalEnvironment를 연결하기 위함이다.

ThisBinding

ThisBinding 은 함수 내부에서 사용하는 this 를 어떤 참조로 사용할 지를 나타낸다.

구체적으로, Javascript 에서 This 는 함수가 호출된 시점에서 결정이 되는데, 이렇게 결정된 this 참조를 ThisBinding 에서 저장한다.

자세한 내용은 This 이해 에서 설명하겠다.

생성, 실행 단계

갑자기 의문점이 생긴다...

실행 컨텍스트가 생성되는 시점에서 선언된 변수, 함수를 저장하고, 상위 컨텍스트를 참조한다고 하였다.

그럼 함수의 생성과 실행 시점이 다르다는 것인가?

그렇다. 생성실행 단계라는 두 단계가 있다.

생성 단계에서는 선언된 변수, 함수를 모두 실행 컨텍스트에 저장한다. 이때, 변수는 메모리에 undefined 상태로 할당되고, 함수에는 함수 객체(Function)이 할당이 된다. 이러한 과정을 호이스팅(Hoisting) 이라고 부릅니다.

실행 단계 에서는 코드를 한 줄씩 실행하면서 undefined 상태로 메모리 할당된 변수에 값을 할당합니다.

이후, 새로운 함수 호출하게 되면 새로운 실행 컨텍스트가 생성되고, 생성, 실행 단계가 다시 시작됩니다.

호이스팅(Hoisting)

Hoisting 을 번역하면 '끌어 올리기' 라고 나온다. MDN 문서에 따르면 변수 및 함수 선언이 물리적으로 코드 상단으로 옮겨지는 것이 아니라, 컴파일 단계에서 메모리 영역에서 끌어올려져 참조하는 것으로 해석된다.

'무엇'을 끌어올린다는 것일까?

  • 바로 선언문 이 해당 스코프 상단부로 끌어올린다는 의미이다.

'어디로' 선언문을 끌어올리지?

  • 함수 또는 전역 코드의 상단으로 이동한다.

'왜' 선언문을 끌어올리지?

  • 실행 컨텍스트, 스코프 체인 과정에서 설명하겠습니다.

아무튼 이러한 특성을 바탕으로, 함수 또는 변수 선언문은 상단으로 끌어올려져 선언되고, 초기화는 기존 코드와 동일하게 이루어집니다.

test();

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

위와 같은 경우, test() 함수 선언이 호출 이후에 선언되었음에도 정상적으로 동작하는 것을 확인할 수 있습니다. 이는 아래와 같이, 함수 선언이 호이스팅되었기 때문입니다.

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

test();

다음은 변수 호이스팅에 관한 예제입니다.

function test() {
    console.log(a);
    var a = 'test';
    console.log(a);
}

test();

변수 a는 test() 함수 상단으로 호이스팅됩니다.

function test() {
		var a;
    console.log(a);
    a = 'test';
    console.log(a);
}

test();

첫번째 출력에서 'undefined' 나오고, 두 번째에서는 'test' 가 출력되는 것을 확인할 수 있습니다.

Closure

클로저는 자신을 포함하고 있는 외부 함수보다 더 오래 유지되어,

외부 함수 밖에서 호출된 경우라도 외부 함수 내부 지역 변수를 참조할 수 있는 함수를 의미한다.

말이 어려우니 예제를 통해 살펴보자.

function foo() {
    var a = "ok";
    return function bar() {
        console.log(a);
    }
}

var bar = foo();
bar();

bar() 함수는 foo() 함수의 지역변수 인 a 를 참조하고 있고, foo() 함수가 bar() 함수를 반환하고 있다.

bar() 함수는 foo() 함수 외부에서 호출되고, foo() 함수는 콜스택에서 모두 사라졌다. 그러므로, 변수 a 참조할 수 없기 때문에 undefined 가 출력될 것이라고 생각할 것이다.

하지만, 정상적으로 a 를 참조해 ok 를 출력한다.

무슨일인가?

내부 함수인 bar() 함수의 상위 컨텍스트는 foo 실행 컨텍스트이므로, 렉시컬 스코프에 따라 bar() 함수가 선언된 시점에서의 환경을 기억하고 있다.

즉, bar 실행 컨텍스트 내부의 LexicalEnvironment 에서 outer 에서 상위 실행 컨텍스트인 foo 실행 컨텍스트 를 참조하고 있다는 것이다. foo 실행 컨텍스트가 아무리 콜스택에서 제거됐을지라도 참조되고 있기 때문에 메모리에서 삭제되지 않습니다.

따라서, 외부 함수인 foo() 가 반환한 bar() 함수를 foo() 함수 외부에서 실행하여도 foo() 의 지역변수를 참조할 수 있는 것입니다.

결과적으로, 클로저는 상위 함수의 실행 단계가 끝났을 지라도, 상위 function scope 에 접근할 수 있는 함수를 의미한다.

클로저를 이용하면, 전역변수의 사용을 억제할 수 있을 뿐만 아니라, javascript private 기능을 구현해 볼 수 있습니다.

아래 예제와 같이, num 변수를 전역변수로 사용하지 않고, IIFE(즉시 실행 함수)와 클로저를 이용해 num 변수 private하게 사용할 수 있습니다.

const count = (function() {
	let num = 0;
  	return function () {
      return ++num;
    }
})();

IIFE(즉시 실행 함수)

IIFE(즉시 실행 함수)는 익명함수를 선언해 즉시 실행하는 방식의 함수이다.

IIFE는 불필요한 전역변수나 함수를 생성하지 않고, 외부(or Global)의 스코프에 영향을 주지 않게 하기 위해 많이 사용하는 함수이다.

아래와 같은 형식으로 사용한다.

(function(파라미터){

  // 기능
  
})(아규먼트);

참고자료

profile
한번도 실수하지 않은 사람은, 한번도 새로운 것을 시도하지 않은 사람이다.

0개의 댓글