티스토리 뷰
[JavaScrpit] 렉시컬 환경(Lexical Environment)과 클로저(closure)
보간 2024. 11. 17. 17:53클로저(closure)란?
JavaScript의 매우 강력한 특성으로, 독립적인 변수를 참조하는 함수
클로저의 필요성
JS는 함수 내부에서 함수 외부에 있는 변수에 접근할 수 있다. 만약에 함수 내부에서 전역 외부 변수를 참조했다면, 전역 변수는 언제든지 누구나 접근할 수 있고 변경할 수 있기 때문에, 많은 부작용을 유발해 오류의 원인이 될 수 있다.
따라서 함수 내에서 상태를 유지하는 변수를 사용할 필요성이 있다.다음의 계산기 함수처럼 말이다.
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
그렇다면 이런 함수는 어떻게 동작할까?
렉시컬 환경
실행 컨텍스트 안에 포함된 렉시컬 환경
- Global Execution Context - 프로그램이 시작될 때 생성되는 전역 실행 컨텍스트*
- Lexical Environment(렉시컬 환경) - 식별자와 변수의 스코프(범위)를 정의하는 환경. 실행 컨텍스트에 포함되어 있고 현재 코드의 컨텍스트를 정의한다.
- Environment Record - 변수를 저장하고 관리하는 공간
- Variable Environment - 선언된 변수와 그 상태(스냅샷)을 저장하는 환경
+함수가 실행될 때 마다 Functional Execution Context (함수 실행 컨텍스트)가 만들어진다.
실행 컨텍스트* - 함수나 전역 코드가 실행되는 동안 관련된 변수, 함수, 객체, 스코프 등을 정의하고 관리하는 구조를 제공.
위 그림에서와 같이, 렉시컬 환경 객체는 두 부분으로 구성된다.
- 환경 레코드(Environment Record) – 모든 지역 변수를 프로퍼티로 저장하고 있는 객체. this 값과 같은 기타 정보도 여기에 저장된다.
- 외부 렉시컬 환경(Outer Lexical Environment) 에 대한 참조 – 외부 코드와 연관됨
함수가 실행될 때마다 실행 컨텍스트가 생겨나고, 그 안에서 렉시컬 환경이 생성된다.
그리고 함수가 끝날 때마다 일반적으로 실행 컨텍스트와 그 안의 렉시컬 환경은 소멸한다.
그런데, 클로저에서는 함수가 끝나도 렉시컬 환경을 참조할 수 있다. 어째서일까? 렉시컬 환경이 어떻게 작동하는 지부터 확인해보자.
변수의 렉시컬 환경
'변수’는 특수 내부 객체인 환경 레코드 Environment Record 의 프로퍼티일 뿐이다. 따라서 '변수를 가져오거나 변경’하는 것은 '환경 레코드의 프로퍼티를 가져오거나 변경’함을 의미한다.
아래 두 줄짜리 코드엔 렉시컬 환경이 하나만 존재한다.
- 변수는 특수 내부 객체인 환경 레코드( Environment Record )의 프로퍼티입니다. 환경 레코드 ( Environment Record ) 는 현재 실행 중인 함수와 코드 블록, 스크립트와 연관되어 있다.
- 변수를 변경하면 환경 레코드의 프로퍼티가 변경된다.
내부와 외부 렉시컬 환경
함수를 호출해 실행하면 새로운 렉시컬 환경이 자동으로 만들어진다. 이 렉시컬 환경엔 함수 호출 시 넘겨받은 매개변수와 함수의 지역 변수가 저장된다.
코드에서 변수에 접근할 땐, 먼저 내부 렉시컬 환경을 검색 범위로 잡는다. 내부 렉시컬 환경에서 원하는 변수를 찾지 못하면 검색 범위를 내부 렉시컬 환경이 참조하는 외부 렉시컬 환경으로 확장한다. 이 과정은 검색 범위가 전역 렉시컬 환경으로 확장될 때까지 반복한다.
- 함수 say 내부의 alert에서 변수 name에 접근할 땐, 먼저 내부 렉시컬 환경을 살펴본다. 내부 렉시컬 환경에서 변수 name을 찾았다.
- alert에서 변수 phrase에 접근하려는데, phrase에 상응하는 프로퍼티가 내부 렉시컬 환경엔 없다. 따라서 검색 범위는 외부 렉시컬 환경으로 확장된다. 외부 렉시컬 환경에서 phrase를 찾는다.
(Closure 기능을 가진)
함수를 반환하는 함수
makeCounter()가 실행되는 도중엔 본문(return count++)이 한줄 짜리인 중첩 함수가 만들어진다. 현재는 중첩함수가 생성되기만 하고 실행은 되지 않았다.
여기서 중요한 사실이 하나 있다. 모든 함수는 함수가 생성된 곳의 렉시컬 환경을 기억한다는 점이다. 함수는 [[Environment]]라 불리는 숨김 프로퍼티를 갖는데, 여기에 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장된다.
따라서 counter.[[Environment]]엔 {count: 0}이 있는 렉시컬 환경에 대한 참조가 저장된다. 호출 장소와 상관없이 함수가 자신이 태어난 곳을 기억할 수 있는 건 바로 이 [[Environment]] 프로퍼티 덕분이다. [[Environment]]는 함수가 생성될 때 딱 한 번 값이 세팅되고 영원히 변하지 않는다.
counter()를 호출하면 각 호출마다 새로운 렉시컬 환경이 생성된다. 그리고 이 렉시컬 환경은 counter.[[Environment]]에 저장된 렉시컬 환경을 외부 렉시컬 환경으로서 참조한다.
counter()를 여러 번 호출하면 count 변수가 2, 3으로 증가하는 이유가 바로 여기에 있다.
가비지 컬렉션
함수 호출이 끝나면 관련 변수를 참조할 수 없는 이유
함수 호출이 끝나면 함수에 대응하는 렉시컬 환경이 메모리에서 제거된다. 함수와 관련된 변수들은 이때 모두 사라진다.자바스크립트에서 모든 객체는 도달 가능한 상태일 때만 메모리에 유지된다.
그런데 호출이 끝난 후에도 여전히 도달 가능한 중첩 함수가 있다.(위의 예시) 이때는 이 중첩함수의 [[Environment]] 프로퍼티에 외부 함수 렉시컬 환경에 대한 정보가 저장되고 도달 가능한 상태가 된다.
함수 호출은 끝났지만 렉시컬 환경이 가비지 컬렉터에 수집되지 않고 메모리에 유지되는 이유는 바로 이 때문이다.
주의점
f()를 여러 번 호출하고 그 결과를 어딘가에 저장하는 경우, 호출 시 만들어지는 각 렉시컬 환경 모두가 메모리에 유지된다. 아래 예시를 실행하면 3개의 렉시컬 환경이 만들어지는데, 각 렉시컬 환경은 메모리에서 삭제되지 않는다.
function f() {
let value = Math.random();
return function() { alert(value); };
}
// 배열 안의 세 함수는 각각 f()를 호출할 때 생성된
// 렉시컬 환경과 연관 관계를 맺습니다.
let arr = [f(), f(), f()];
또한 해당 함수를 호출할 때마다 각 함수의 실행 컨텍스트가 계속 생겨날 것이다.
렉시컬 환경 객체는 다른 객체와 마찬가지로 도달할 수 없을 때 메모리에서 삭제된다. 해당 렉시컬 환경 객체를 참조하는 중첩 함수가 하나라도 있으면 사라지지 않는다. 아래 예시 같이 중첩 함수가 메모리에서 삭제되고 난 후에야, 이를 감싸는 렉시컬 환경(그리고 그 안의 변수인 value)도 메모리에서 제거된다.
function f() {
let value = 123;
return function() {
alert(value);
}
}
let g = f(); // g가 살아있는 동안엔 연관 렉시컬 환경도 메모리에 살아있습니다.
g = null; // 도달할 수 없는 상태가 되었으므로 메모리에서 삭제됩니다.
+
따라서 클로저는 가비지컬렉션의 수집대상이 되지 않고 독립적인 변수를 참조할 수 있는 함수를 말한다.
클로저에 정의된 함수는 그것이 작성된 환경을 '기억'한다.
참조 - javascript.info
'Programming > Javascript' 카테고리의 다른 글
Vanila 프로젝트 후기 (1) | 2024.12.31 |
---|---|
[Javascript] Throttle과 Debounce (1) | 2024.12.15 |
[JavaScript] Custom Element 번역 - 모던 JavaScript 튜토리얼 (0) | 2024.12.08 |
[Javascript] 비동기 통신 - AJAX와 fetch (1) | 2024.12.01 |
[JavaScript]호이스팅(Hoisting) (0) | 2024.11.11 |