코드 블록
코드 블록 {...} 안에서 선언한 변수는 코드 블록 안에서만 사용할 수 있다.
if (true) {
let message = "안녕하세요!";
alert(message); // 안녕하세요!
}
alert(message); // ReferenceError: message is not defined
if와 같은 조건문만이 아니라 반복문에도 해당한다.
for (let i = 0; i < 10; i++) {
alert(i); // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
}
alert(i); // ReferenceError: i is not defined
중첩 함수
함수 내부에서 선언한 함수를 중첩 함수라고 부르며 중첩 함수는 새로운 객체의 프로퍼티 형태나 중첩 함수 그 자체로 반환될 수 있다는 점에서 장점을 갖는다. 반환된 중첩 함수는 어디서든 사용할 수 있으며 외부 변수에도 접근할 수 있다.
function makeCounter() {
let count = 0;
return function() {
return count++;
}
}
let counter = makeCounter();
alert(counter); // 0
alert(counter); // 1
alert(counter); // 2
관심 없을 수도 있지만, counter와 같이 makeCounter를 호출하는 변수 여러 개를 만들면, 이 함수들은 서로 독립적일까, 함수와 중첩 함수 내에 count 변수는 어떻게 될까 쉽게 예측하지 못할 수 있다. 그에 대한 답을 '렉시컬 환경'이라는 키워드와 함께 설명하고자 한다.
렉시컬 환경
원활한 이해를 위해 단계 별로 진행하려 한다.
1단계: 변수
JavaScript에서 실행 중인 함수, 코드 블록 {...}, 스크립트 전체는 '렉시컬 환경'이라는 내부 숨김 연관 객체를 갖는다. 편하게 이름이 렉시컬 환경인 객체에 속해있다고 이해하면 된다. 렉시컬 환경 객체는 두 부분으로 구성된다.
- 환경 레코드: 모든 지역 변수를 프로퍼티로 저장하고 있는 객체, this값과 같은 기타 정보도 이곳에 저장된다.
- 외부 렉시컬 환경에 대한 참조: 외부 코드와 연관된다.
우리가 선언해주는 변수도 결국 특수 내부 객체인 환경 레코드의 프로퍼티이다. 변수를 가져오거나 변경하는 것도 환경 레코드의 프로퍼티를 가져오거나 변경한다는 의미이다. 아래 이미지에 렉시컬 환경이 하나만 존재하는 것을 확인할 수 있다.
이렇게 스크립트 전체와 관련된 렉시컬 환경을 전역 렉시컬 환경이라고 부른다. Lexical Environment 아래에 감싸고 있는 상자가 환경 레코드이며 키가 phrase, 값이 "Hello"인 프로퍼티가 저장되어 있다. 또한, 전역 렉시컬 환경은 외부 참조를 갖지 않기 때문에 outer 화살표가 null을 가리킨다. 코드를 실행할 때, 한 줄 한 줄 실행 흐름에 따른 렉시컬 환경에 변화를 살펴보자.
우측의 네모 상자들은 전역 렉시컬 환경의 변화이다. 한 줄 한 줄 진행에 따른 전역 렉시컬 환경의 변화를 순서대로 표현하면 다음과 같다.
- 스크립트가 시작되면 스크립트 내에 존재하는 변수 모두 렉시컬 환경의 프로퍼티로 담긴다. 이때, 변수는 특수 내부 상태인 uninitialized 상태이며, 자바스크립트 엔진은 이 변수를 인지할 수 있지만 let phrase;를 만나기 전까지 사용할 수 없다.
- let phrase;를 만나 값을 할당하기 전이므로 undefined이며 phrase를 사용하면 undefined임을 확인할 수 있다.
- 전역 렉시컬 환경 속 환경 레코드의 프로퍼티인 phrase에 값이 할당되어 phrase를 사용하면 Hello임을 확인할 수 있다.
- 전역 렉시컬 환경 속 환경 레코드의 프로퍼티인 phrase에 값이 변경되어 phrase를 사용하면 Bye임을 확인할 수 있다.
다음 단계로 넘어가기 전에 짚고 넘어가자.
- 변수는 특수 내부 객체인 환경 레코드의 프로퍼티. 환경 레코드는 현재 실행 중인 함수, 코드 블록, 스크립트 전체와 연관되어 있다.
- 변수를 변경한다는 건 곧 환경 레코드의 프로퍼티를 변경한다는 의미이다.
2단계: 함수 선언문
렉시컬 환경에서 함수도 변수와 마찬가지로 값이다. 하지만 함수 선언문으로 만든 함수는 변수와 달리 바로 초기화가 되는 차이점이 있다. JavaScript 코드를 실행할 때, 함수가 선언되기 전인데도 사용할 수 있는 것이 바로 이 때문이다. 함수 선언문으로 만든 함수는 렉시컬 환경이 만들어지는 즉시 사용할 수 있기 때문이다. 아래 그림이 이전 이미지에서 함수를 추가했을 때, 초기 전역 렉시컬 환경이 어떤지 보여준다.
여기서 주의해야 할 점은 함수 선언문만 바로 초기화되고 let say = function(name) {...}과 같이 함수를 변수에 할당한 함수 표현식은 해당하지 않는다.
3단계: 내부와 외부 렉시컬 환경
지금부터 본격적으로 시작된다.
함수를 호출해 실행하면 새로운 렉시컬 환경이 만들어지는데, 이곳엔 함수 호출 시 받은 매개변수와 함수의 지역변수가 저장된다.
다음은 say("John"); 코드가 실행됐을 때, 일어나는 변화이다.
함수를 호출하여 실행 중인 동안에는 함수를 위한 내부 렉시컬 환경과 내부 렉시컬 환경이 가리키는 외부 렉시컬 환경이 존재하게 된다.
- 여기서 내부 렉시컬 환경은 현재 실행중인 함수인 say이며, 내부 렉시컬 환경엔 매개변수 name이 프로퍼티로 존재하고 있으며 say("John")으로 호출했기 때문에 값은 "John"이다.
- 내부 렉시컬 환경이 가리키고 있는 외부 렉시컬 환경은 앞서 보았던 전역 렉시컬 환경이며 변수 phrase와 함수 say를 프로퍼티로 갖고 있다.
그리고 내부 렉시컬 환경은 외부 렉시컬 환경에 대한 참조를 갖는다.
JavaScript 코드에서 변수에 접근하는 방법은 먼저, 내부 렉시컬 환경을 검색 범위로 설정하여 찾기 시작한다. 내부 렉시컬 환경에서 원하는 변수를 찾지 못했다면, 검색 범위를 내부 렉시컬 환경이 참조하는 외부 렉시컬 환경으로 확장하며 이러한 과정은 검색 범위가 전역 렉시컬 환경이 될 때까지 반복한다.
변수에 접근하는 과정을 아래의 그림과 함께 살펴보자.
- 함수 say 내부에 alert 함수에서 변수 phrase에 접근하려 한다. 먼저, 내부 렉시컬 환경을 검색 범위로 검색하며 phrase가 존재하지 않으므로 검색 범위를 외부 렉시컬 환경으로 확장한다. 외부 렉시컬 환경에서 phrase를 찾았으므로 다음으로 넘어간다.
- 함수 say 내부에 alert 함수에서 변수 name에 접근하려 한다. name에 접근할 때, 먼저 내부 렉시컬 환경을 검색 범위로 검색하며 name을 찾았으므로 더 이상 나아가지 않고 멈춘다.
4단계: 함수를 반환하는 함수
돌고 돌아 앞서 중첩 함수에서 설명했던 함수를 다시 살펴보자.
function makeCounter() {
let count = 0;
return function() {
return count++;
}
}
let counter = makeCounter();
makeCounter()를 호출하면 호출할 때마다 새로운 렉시컬 환경이 만들어질 것이고, 이 환경에는 makeCounter를 실행하는데 필요한 변수들이 저장될 것이다. 다음 그림과 같이 말이다.
여기서 앞서 예시로 설명했던 say("John")과 makeCounter()에는 주의해야 할 차이점이 존재한다. makeCounter() 실행 도중에는 return count++라는 내용의 중첩 함수가 존재한다는 점이다. 현재 중첩 함수는 생성되기만 하고 실행은 되지 않은 상태다.
마지막으로 딱 한 가지 중요한 게 있다. JavaScript에서 모든 함수는 함수가 생성된 곳의 렉시컬 환경을 기억한다. 함수는 [[Environment]]라고 불리는 숨김 프로퍼티를 통해, 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장된다.
아래 그림과 같이 살펴보면
counter.[[Environment]]에는 count: 0이 프로퍼티로 존재하는 렉시컬 환경에 대한 참조가 저장되며, 호출 장소, 호출 시점과 상관없이 함수 자신이 태어난 곳을 기억하는 것도 이 [[Environment]] 프로퍼티 덕분이다. [[Environment]]는 함수가 생성될 때 딱 한 번 값이 세팅되고 영원히 변하지 않는다. 따라서, counter()로 호출하면 각 호출마다 새로운 렉시컬 환경이 생성되고, 이 렉시컬 환경은 counter.[[Environment]]에 저장된 렉시컬 환경을 외부 렉시컬 환경으로 참조하게 되는 것이다.
실행 흐름이 중첩 함수의 본문이 되면 count 변수를 검색하고 중첩 함수 자체 렉시컬 환경에서부터 변수를 찾기 시작한다. 현재, 익명 중첩 함수엔 매개변수, 지역변수가 없으므로 비어있는 상황(<empty>)이기에 counter()의 렉시컬 환경이 참조하는 외부 렉시컬 환경에서 count를 찾게 된다. 이제, counter++가 실행되고 count 값이 1 증가하는데, 이때, 변숫값 갱신은 변수가 저장된 렉시컬 환경에서 이뤄진다. 실행이 종료되면 다음과 같다.
클로저
클로저란 '외부 변수를 기억하며 이 외부 변수에 접근할 수 있는 함수'를 의미한다. 앞서 공부한 내용을 바탕으로 더 자세하게 설명해보면,
JavaScript에서 실행 중인 함수, 코드 블록, 변수 같은 스크립트 전체는 렉시컬 환경을 갖는다. 이 렉시컬 환경에는 변수, 함수를 프로퍼티로 저장하는 환경 레코드, 외부 렉시컬 환경에 대한 참조가 존재한다. 함수를 호출해 실행하면 실행한 함수를 위한 렉시컬 환경이 생성되며 이곳이 내부 렉시컬 환경이고 기존에 존재하던 스크립트 전체와 관련된 렉시컬 환경은 외부 렉시컬 환경이 된다. 함수는 [[Environment]]라고 불리는 숨김 프로퍼티를 통해, 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장되고, 함수가 외부 변수를 참조할 일이 생기면 실행 중인 함수의 렉시컬 환경인 내부 렉시컬 환경부터 찾으며 내부 렉시컬 환경에 찾는 변수가 없다면, 함수의 [[Environment]]가 참조하는 외부 렉시컬 환경으로 그 검색 범위를 찾을 때까지 넓혀 나가는 것이고 이 검색 범위는 전역 렉시컬 환경까지 넓혀 나가게 되는 것이다.
'JavaScript & TypeScript' 카테고리의 다른 글
CRA없이 Webpack, React, TypeScript로 개발 환경 설정하기 (0) | 2022.08.21 |
---|---|
[JavaScript] 객체(2) - 메서드와 this (0) | 2022.07.10 |
[JavaScript] 객체(1) - 기본 (0) | 2022.06.10 |