[JavaScript] 렉시컬 환경과 클로저

Yeon Jeffrey Seo·2021년 11월 22일
1

JavaScript

목록 보기
1/7

이번 포스팅을 통해 클로저, 렉시컬 환경 마스터하기!!

코드 블록

코드 블록 {...} 안에서 선언한 변수는 블록 안에서만 사용 가능하다.
블록을 사용하여 특정 작업을 수행하는 코드를 한데 묶어두는 용도로 활용 가능하다.

{
  let messeage = "Hello World!";
  console.log(message);	// Hello World!
}
console.log(message);	// ReferenceError

if(){...}, for(){...}, while(){...} 등에서도 마찬가지로 {...} 안에서 선언한 변수는 오직 블록 안에서만 접근 가능하다.

중첩 함수

함수 내부에서 선언한 함수를 중첩 함수(Nested function)이라 부른다.
중첩 함수는 새로운 객체의 프로퍼티 형태나 중첩 함수 그 자체로 반환될 수 있다. 이렇게 반환된 중첩 함수는 어디서든 호출해 사용할 수 있다. 이 때에도 외부 변수에 접근할 수 있다.

Lexical Environment

1. 변수

자바스크립트에서는 실행 중인 함수, 코드 블록, 스크립트 전체는 렉시컬 환경이라는 내부 숨김 연관 객체를 갖는다. Lexical Environment(이하 렉시컬 환경)은 특정 코드가 선언/정의/작성된 환경을 의미하는 객체이다.
모든 코드 블록은 어떤 렉시컬 환경에 속해 있는지에 따라 이용가능한 변수가 달라진다. 다시 말해 어떤 변수나 함수의 값은, '어디에서 선언했는지', 즉 렉시컬 환경이 어디인지에 따라 결정된다.
다시 한번 말하지만, 렉시컬 환경은 객체이다. 렉시컬 환경 객체는 두 부분으로 구성된다.

  1. Environment Record (환경 레코드)
  • 모든 지역 변수를 프로퍼티로 저장하고 있는 객체. this 값과 같은 기타 정보도 여기에 저장됨.
  1. 외부 렉시컬 환경에 대한 참조
  • 외부 코드와 연관됨(?)

'변수'는 특수 내부 객체인 환경 레코드의 프로퍼티이다. 변수를 가져오거나 변경하는 것은 환경 레코드의 프로퍼티를 가져오거나 변경함을 의미한다.

스크립트 전체와 관련된 렉시컬 환경을 전역 렉시컬 환경(Global Lexical Environment)이라 한다. 각 렉시컬 환경은 외부와 내부 렉시컬 환경을 갖는다. 내 외부의 포함 관계를 확인하기 위해 내부 렉시컬 환경은 외부 렉시컬 환경에 대한 참조를 갖는다. 전역 렉시컬 환경의 경우, 외부 참조를 갖지 않기 때문에(최상위이기 때문에) 외부 참조가 null을 갖는다.

2. 함수 선언문

함수는 변수와 마찬가지로 값이다. 다만, 함수 선언문(function delaration)으로 선언한 함수는 일반 변수와는 달리 바로 초기화된다는 점에서 차이가 있다. 함수 선언문으로 선언한 함수는 렉시컬 환경이 만들어지는 즉시 사용할 수 있다. 반면 let say = function() ... 와 같은 함수 표현식(function expression)은 선언과 동시에 초기화 되지 않는다.

3. 외부와 내부 Lexical Environment

함수를 호출해 실행하면, 새로운 렉시컬 환경이 자동으로 만들어진다. 이 렉시컬 환경엔 함수 호출 시 넘겨받은 매개변수와 함수의 지역 변수가 저장된다.

하나의 함수가 실행될 때 내부와 외부, 두개의 렉시컬 환경이 존재한다. 코드가 변수에 접근할 때, 내부 렉시컬 환경을 먼저 탐색하고, 그 이후 외부 렉시컬 환경을 탐색한다.

let phrase = "hello";

function say(name) {
  console.log(`${phrase}, ${name}`);
}

say("John");

함수가 호출중인 동안에는 호출 중인 함수를 위한 내부 렉시컬 환경과, 이 내부 렉시컬 환경이 참조하는 외부 렉시컬 환경을 갖게 된다.
코드가 변수에 접근할 때에는, 내부 렉시컬 환경부터 검색하며, 전역 렉시컬 환경으로 검색 범위를 넓혀나가며 변수를 찾는다. 전역 렉시컬 환경에 도달할 때까지 변수를 찾지 못하면 에러를 반환한다.

4. 함수를 반환하는 함수

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

makeCounter를 호출할 때마다 새로운 렉시컬 환경 객체가 만들어지고 여기에 makeCounter를 실행하는데 필요한 변수들이 저장된다. 여기서 makeCounter()가 실행되는 도중엔 본문(return count++)이 한줄 짜리인 중첩 함수가 만들어지고 실행은 되지 않는다.

모든 함수는 함수가 생성된 곳의 렉시컬 환경을 기억한다. 함수는 [[Environment]]라 불리는 숨김 프로퍼티를 갖는데, 여기에 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장된다.

따라서 counter.[[Environment]]에는 {count : 0}이 있는 렉시컬 환경에 대한 참조가 저장된다.

counter()를 호출할 때마다, 새로운 렉시컬 환경이 생성된다. 새롭게 생긴 각 렉시컬 환경은 counter.[[Environment]] 에 저장된 렉시컬 환경을 외부 렉시컬 환경으로서 참조한다.

Closure

"A closure is the combination of a function and the lexical environment within which that function was declared" - MDN

클로저는 일반적으로, 어떤 함수가 자신의 외부에서 선언된 변수에 접근하는 것을 뜻한다. 자바스크립트에서 모든 함수는 클로저이다. (? 모든 함수 선언이 전역 렉시컬 환경 아래에서 이루어지기 때문인가?)

클로저는 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용하는 중요한 특성이다. 따라서 자바스크립트 고유의 개념이 아니다.

2022.08.28 추가

MDN에서는 클로저를 위와 같이 설명한다. 직역하면, "클로저는 함수와 그 함수가 선언될 당시의 lexcial environment의 상호 관계에 따른 현상" 정도 된다.
LexicalEnvironment 의 environmentRecord와 outerEnvironmentReference에 의해 변수의 유효 범위인 "스코프" 결정되고 스코프 체인이 가능해진다.

var outer = function() {
	var a = 1;
	var inner = function() {
		return ++a;
	}
	return inner();
}

var outer2 = outer();
console.log(outer2); // 2

위 코드에서 inner 함수는 외부 변수인 a를 참조하여 1을 더한 뒤 그 값을 반환한다. outer 함수는 inner 함수의 실행 결과를 반환하므로, outer 함수의 실행 컨텍스트가 종료되는 시점에는 a 변수를 참조하는 대상이 없어진다.

var outer = function() {
	var a = 1;
	var inner = function() {
		return ++a;
	}
	return inner;
}

var outer2 = outer();
console.log(outer2());
console.log(outer2());

이번에는 outer 함수의 반환 값으로 inner 함수의 실행 결과가 아닌 inner 함수 그 자체를 반환했다. 그러면 outer 함수의 실행 컨텍스트가 종료될 때, outer2 변수는 inner함수의 선언을 참조하게 된다.

여기서 outer2를 호출 (outer2())하게 되면, inner 함수의 실행 컨텍스트가 생성된다. inner 함수의 lexical environment에는 a에 대한 정보가 없으므로, outerEnvironmentReference를 통해 inner 함수가 선언되었을 당시의 외부 lexical environment를 참조하게 된다.(스코프 체이닝)
a라는 변수에 1이라는 값이 할당되어있는걸 발견한다. inner 함수는 그 값에 1을 더하고 그 결과를 반환한다. 그 뒤 inner의 실행 컨텍스트가 종료된다.

그렇다면 outer 함수는 이미 실행이 종료되었는데, 어떻게 outer 함수의 LexicalEnvironment에 접근을 할 수 있는 걸까? 이는 가비지 컬렉터가 outer의 lexical environment를 컬렉팅 대상에서 제외했기 때문이다.
가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 컬렉팅 대상에서 제외하는데, outer2 에 할당한 inner 함수가 선언된 환경이 outer의 lexical environment이다.(inner 함수의 lexical environment 중 outerEnvironmentReference가 outer의 lexical environment를 참조하고 있다.)
따라서, outer 함수의 실행이 종료되었음에도 불구하고, outer의 lexical environment에 접근할 수 있는 것이다.

두번째 코드의 현상을 다시 요약해보면, 중첩 함수에서 내부함수가 지역변수를 (외부 함수의 변수)를 참조하는데, 이 때 내부함수가 외부로 전달된 경우, 스코프 체이닝이 발생하여 외부함수의 lexical environment가 가비지 컬렉팅되지 않는 현상이다.

여기서 클로저를 다시 정의하자면,

클로저란, 중첩 함수에서 어떤 함수가 자신(함수)의 외부에서 선언된 지역 변수를 참조하도록 선언되어, 외부 실행 컨텍스트가 종료되었음에도 불구하고 그 Lexical Environment 정보가 남아 있어 이에 접근할 수 있는 함수 또는 그 현상

정도 되겠다.

자바스크립트에서 함수는 모두 클로저라고 한다. 하지만 이에 대한 명쾌한 설명을 해주는 자료를 아직 찾지 못했다. 추측컨대 자바스크립트에서 프로그래밍은 전역 객체 내부에서 이뤄지기 때문에, 함수의 스코프 체이닝의 최상위 레벨은 결국 전역 객체이기 때문이 아닐까 생각해본다.

처음 포스팅을 올렸을 땐 멋도 모르고 거의 베껴 쓰느라 무슨 내용인지도 잘 몰랐는데, 실행 컨텍스트, 렉시컬 환경을 다시 공부하고 보니, 완전 새로운 내용처럼 느껴진다. 이래서 선행 학습이 중요하다. ㅠ

참고 자료

https://velog.io/@janeshin059/Javascript-Lexical-environment%EC%99%80-Closure
https://velog.io/@winz/Javascript-%EB%A0%89%EC%8B%9C%EC%BB%AC%ED%99%98%EA%B2%BD%EA%B3%BC-%ED%81%B4%EB%A1%9C%EC%A0%80Closure-Javascript-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-8
https://ko.javascript.info/closure

profile
The best time to plant a tree was twenty years ago. The second best time is now.

0개의 댓글