실행 컨텍스트와 클로저

Kyung yup Lee·2021년 5월 31일
0

자바스크립트

목록 보기
8/12

실행 컨텍스트와 클로저는 자바스크립트 및 함수형 언어의 꽃이라고 생각한다. 특히 실행 컨텍스트는 자바스크립트의 동작 원리를 담고 있는 핵심이라고 생각한다.

실행컨텍스트와 클로저

실행 컨텍스트

먼저 실행 컨텍스트가 무엇인지 이해해야 한다. 실행 컨텍스트는 코드의 흐름을 실체화시켜놓은 것이다. 보통은 깊게 생각해보지 않겠지만, 우리가 작성하는 코드는 분명 엔진에서 돌아가, 어떤 곳에 저장되고, 실행되게 될 것이다. 이 실체화 될 코드 조각을 실행 컨텍스트 컴포넌트라 하고 자바스크립트는 이 코드 조각의 단위를 4가지로 구분한다.

  • 전역 코드
  • 함수 코드
  • eval 코드
  • 모듈 코드

해당 코드들이 실행 컨텍스트 컴포넌트가 만들어지는 단위이다.

전역 코드

프로그램이 실행되면 기본적으로 전역 환경이다. 그 이후 여러 함수와 모듈이 얽히고 섥히면서 프로그램이 작동되게 된다. 전역 코드는 전역에 존재하는 소스코드를 말한다. 전역에 정의된 함수, 클래스 등의 내부 코드는 포함되지 않는다. 전역 코드가 평가 되면 전역 실행 컨텍스트가 생성되고 이에 연결된 다양한 컴포넌트들이 생겨난다(아래에서 자세한 설명)

전역 코드 평가

전역 코드를 실행하기 전에 전역 코드 평가 과정을 거친다. 평가 과정에서는 선언문을 먼저 실행한다. 이게 호이스팅과 연결된다. 자바스크립트는 전역에 선언된 모든 변수를 읽어와 평가하고 모든 정의된 함수를 읽어와서 평가한다. 이 때, 전역 실행 컨텍스트 컴포넌트가 생성된다. 그리고 이 컴포넌트에 연결된 렉시컬 환경(추후 설명)에 모든 식별자가 등록된다.

전역 코드 실행

이렇게 평가가 끝나면 전역 코드를 실행한다. 모든 전역변수에 값이 할당되고, 함수가 호출된다. 그리고 함수가 호출되면, 런타임을 멈추고 다시 함수 평가에 들어간다.

함수 코드

전역 코드와 크게 다르지 않게 동작한다. 작은 전역 코드라고 생각하면 된다. 특정 함수가 호출된 경우, 마찬가지로 그 함수 코드 내부에 정의된 함수, 클래스 등의 내부 코드는 읽지 않는다.

함수 코드 평가

함수 호출에 의해 런타임이 중지되고 함수 내부로 코드 실행 순서가 돌아오면 함수 코드를 평가하기 시작한다. 전역 평가와 마찬가지로 내부에 선언된 변수와 정의된 함수들을 모두 렉시컬 환경에 등록한다. 함수는 매개변수가 있기 때문에 매개 변수도 함께 처리되는 것을 알아야 한다. arguments 객체가 생성되고, this 바인딩도 평가 과정에서 결정된다.

함수 코드 실행

함수 코드의 실행에서의 차이점은 스코프가 전역 내부에 있다는 것이다. 즉, 상위 스코프가 존재한다는 것. 상위 스코프가 존재하기 때문에, 함수 코드에서 만들어지는 렉시컬 환경에는 상위 스코프를 참조하는 공간이 따로 존재한다. 이를 스코프 체인이라고 부른다.

코드 흐름

실행 컨텍스트는 코드 흐름을 실체화 시킨 것이라 했다. 실행 컨텍스트는 스택 형태로 만들어진다. 가장 먼저 전역 실행 컨텍스트가 스택에 푸쉬되고, 그리고 함수가 호출되면 해당 함수가 푸쉬되게 된다. 이렇게 여러 함수들이 전역 실행 컨텍스트 위에서 쌓였다 사라지면서 프로그램이 실행되고, 마지막에는 전역 실행 컨텍스트가 팝되면서 프로그램이 종료되게 된다.

이를 보면 실행 컨텍스트는 코드의 흐름을 관리한다고 알 수 있다. 하지만 변수는 실행 컨텍스트에 저장된다고 했는데, 이 내용은 어디에 저장되는가라는 궁금증이 일어야 한다. 이 부분을 관리하는 것은 실행 컨텍스트에 바인딩된 렉시컬 환경에서 관리한다. 즉 실행 컨텍스트는 코드의 흐름을 관리하고, 렉시컬 환경은 해당 흐름 내부에서 식별자와 상위 스코프에 대한 참조를 기록하는 컴포넌트이다.

렉시컬 환경


https://publizm.github.io/posts/javascript/Closure

위 그림은 실행 컨텍스트 내부에 바인딩된 요소들을 나타낸다.

Global Lexical Enviroment라고 명시된 컴포넌트가 보일 것이다. 이것이 렉시컬 환경이다. 렉시컬 환경은 식별자를 관리하고 스코프에 대한 참조를 기록한다고 했다.

  • 전역 환경 레코드
  • 외부 렉시컬 환경 참조
  • this 바인딩

이렇게 세 가지 요소가 렉시컬 환경에 존재한다. 환경 레코드가 식별자를 관리하고, 외부 렉시컬 환경 참조 부분이 상위 스코프를 관리한다.

전역 환경 레코드

전역 환경 레코드는 객체 환경 레코드와 선언적 환경 레코드로 나누어진다. 객체 환경 레코드에는 전역 객체가 바인딩되고, 해당 window에는 모든 전역 객체와 var로 선언된 변수가 관리된다.
선언전 환경 레코드는 전역 환경 레코드에만 있는 공간인데, let 과 const 로 선언된 변수가 관리된다. var로 선언된 변수는 전역객체의 프로퍼티로 만들어진다. 하지만 es6에서 도입된 let, const 는 이런 방식으로 선언되지 않는다. 때문에 추가적인 공간이 필요했고, 이를 통해 만들어진것이 선언적 환경 레코드 공간이다. 내부 함수 렉시컬 환경에는 함수 환경 레코드에 모든 변수와 객체가 다 기록되기 때문에 따로 선언적 환경 레코드가 없다.

객체 환경 레코드

윈도우 객체와 바인딩된 객체를 저장하는 공간이다. 자바스크립트의 window 객체(전역객체)는 자바스크립트에서 만들어낸 객체가 아니다. 브라우저에서 만들어서 쥐어주는 객체이다. 그러므로 전역객체의 내용은 환경마다 다르다. 때문에, 이 전역객체를 환경에 맞게 바꿔주기 위해 참조값을 저장하는 식으로 작동한다.

선언적 환경 레코드

es5 이전에는 없던 공간이다. let, const를 전역 객체에 프로퍼티로 넣지 않기 위해 새로 만든 공간이다.

함수 환경 레코드

함수의 경우 객체 환경 레코드가 필요가 없다. 따로 전역객체를 바인딩해줄 일이 없기 때문이다. 때문에 함수의 환경 레코드를 통합으로 관리하고 해당 공간에, 새로 함수에서 선언되는 지역 변수나, 매개변수들을 저장하고 관리한다.

외부 렉시컬 환경 참조

외부 렉시컬 환경 참조는 스코프 체인을 나타낸다. 외부 렉시컬 환경 참조 공간은 반드시 자신의 상위 스코프의 실행 컨텍스트의 렉시컬 환경을 참조하게 되고, 이를 통해 스코프 체인을 구현한다.
전역 외부 렉시컬 환경 참조의 경우 상위 실행 컨텍스트가 존재하지 않기 때문에 null이 담기게 된다.

함수에서 만들어진 렉시컬 환경의 경우 외부 렉시컬 환경 참조를 해당 함수가 호출되기 직전의 환경을 포인팅하게 된다. 지금까지 여러번 얘기가 나왔었던 스코프 체인의 실체가 이것이다. 자바스크립트는 렉시컬 스코프(정적 스코프) 를 따르기 때문에, 함수가 정의되는 곳에서 지역변수의 스코프가 결정된다. 그리고 렉시컬 환경은 함수가 정의될 때 생겨나기 때문에, 스코프도 자연스럽게 렉시컬 스코프를 따르게 된다.

블록 레벨 스코프의 렉시컬 환경

블록 레벨 스코프도 존재한다. 하지만 블록 레벨은 실행 컨텍스트를 생성하지 않는다. 그러므로 현재 실행중인 실행 컨텍스트에서 렉시컬 환경만 새로 생성해, 참조를 잇는다. 또한 let, const 로 선언된 변수의 경우 블록 스코프를 갖으므로, 렉시컬 환경에 선언적 환경 레코드 공간에 저장되게 된다.

클로저

지금까지 실행 컨텍스트에 대한 내용을 줄줄줄 얘기했는데, 이는 사실 클로저를 설명하기 위해서라고 생각해도 무방하다. 클로저는 기본만 확실하다면 금방 이해할 수 있다.

클로저의 정의

클로저의 정의는 클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합 이다. 아무것도 모르는 상태에서 이 정의를 보면, 렉시컬 환경이 뭐여? 하는 생각부터 들 것이다. 그러므로 선행되는 개념들에 대한 공부가 필요하다.

위에서 실행 컨텍스트가 만들어지면서, 렉시컬 환경까지 함께 만들어진다고 했다. 이 렉시컬 환경과 함수의 조합으로 클로저라는 현상이 만들어진다.

클로저 코드

클로저는 반드시 코드와 함께 봐야한다.

const x = 1;
function outer() {
  const x = 10;
  function inner = function (){
   console.log(x); 
  }
  return inner;
}

const innerFunc = outer();
innerFunc(); // 10

위의 코드는 전형적인 클로저 현상이 나타나는 코드이다. outer 함수는 inner 함수를 리턴하면서 실행이 종료되었다. 그러므로 x 변수는 사라져야 정상이다. 그러므로 innerFunc 가 실행되면 리턴된 inner 함수만 실행되고, x 변수는 참조할 수 없어야 한다. 하지만 결과는 10이 나온다. 즉 inner 함수와 inner 함수의 렉시컬 환경이 조합되어 만들어진 결과이다.

결론부터 말하자면 outer 함수 실행 컨텍스트에서는 pop이 되지만, 이 함수의 렉시컬 환경은 살아있다. inner 함수에서 해당 렉시컬 환경을 참조하고 있기 때문에 렉시컬 환경이 가비지 컬렉터에 의해 사라지지 않은 것이다.

클로저 현상의 흐름

그림이 있으면 매우 이해하기 편하지만 그림은 저작권 때문에 가져오기 힘드니 글로 나만 이해하겠다.

위 코드를 그대로 예시로 든다. 가장 먼저 전역 실행 컨텍스트가 만들어지고, 전역 렉시컬 환경이 만들어진다. 자연스럽게 window 객체에 outer function이 프로퍼티로 만들어진다. (전역 함수 선언문은 window 객체에 프로퍼티로 만들어짐) 그리고 이 함수 객체는 [[environment]] 내부 슬롯을 갖는다. 이 내부 슬롯은 자신의 상위 스코프의 참조를 저장한다. 즉 outer 함수의 상위 스코프인 전역 렉시컬 환경을 참조한다.

이제 innerFunc 라는 변수에 outer 함수를 호출한 결과를 저장하기 위해 outer 함수를 호출한다. 그러면 outer 함수 실행컨텍스트가 만들어진다. 그리고 함수 렉시컬 환경이 만들어지고, 함수 환경 레코드에 x와 inner함수를 저장하기 위한 inner 변수가 만들어진다. 또한 이 inner 함수 역시 [[environment]] 슬롯을 가지고 있으며, 상위 스코프인 outer 함수의 렉시컬 환경을 참조한다.

이제 마지막으로 inner 함수는 호출되지 않고 바로 outer 함수의 리턴값으로 넘어가게 된다. 그러면 전역 객체에 만들어져 있는 innerFunc 변수에 해당 함수를 참조하는 참조값을 넣게 되고, 연결이 발생하게 된다. 그리고 outer 함수는 실행이 종료 되었으니, 실행컨텍스트에서 pop 되고, 사라진다. 하지만 이 outer 함수는 아직도 innerFunc에 의해 참조되고 있으며, 연쇄적으로 outer 함수의 렉시컬 환경, x 변수도 모두 참조되게 된다.

그러므로 자바스크립트의 가비지 컬렉터는 참조되고 있는 메모리는 해제하지 않으므로, 함수가 종료되었음에도 불구하고 밖에서 참조할 수 있게 된다.

이게 클로저다.

클로저의 활용

클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다. 일반적으로 자바스크립트의 변수는 스코프 내에 있는 한 누구나 변경할 수 있기 때문에, 위험하다. 이를 위해 함수를 중첩해서 만들고, 특정 클로저로 작동하는 함수만 해당 변수에 접근할 수 있도록 만들어 은닉을 구현한다. 이런 변수를 자유변수라 부른다.

결론

내가 자바스크립트를 공부하면서 가장 궁금했고, 알고 싶었던 내용이었다. 말끔하게 해결되어 매우 기분이 좋다.

profile
성장하는 개발자

0개의 댓글