Lexical Environment(렉시컬 환경) 은 특정 코드가 작성, 선언(정의)된 환경을 의미하며, 객체이다. Lexical이라는 단어 자체의 뜻이 '사전의, 본질적인, 정의된' 이므로 이와 연관시켜 생각할 수도 있다.
모든 함수, 코드 블록 {}
, 스크립트는 렉시컬 환경을 가진다.
그런데 렉시컬 환경을 왜 알아야 하는 것일까?
왜냐하면 내가 사용하고자 하는 변수, 함수 등이 어떤 렉시컬 환경에 속해있는지에 따라 이용 가능한 변수가 달라지기 때문이다. 즉 어떤 변수나 함수의 값은 이를 '어디에서 호출했는지' 가 아니라, '어디에서 선언했는지' 즉 렉시컬 환경이 어디인지에 따라서 결정된다. 이를 Lexical scoping이라 한다.
Lexical environment object는 두 개의 파트로 나눠져 있다.
this
의 값 등의 다들 정보들) 를 property로 저장하는 객체함수가 실행될 때, call의 시작에서, call의 로컬 변수와 파라미터들을 저장하기 위해 렉시컬 환경이 자동으로 생성된다.
여기서 보면, 사각형 안은 Environment record(1)를 의미하고 사각형에서 뻗어나가는 화살표는 외부 렉시컬 환경에 대한 참조(2)를 의미한다.
이 함수가 실행되는 동안 여기에는 내부와 외부, 두 개의 렉시컬 환경이 존재한다. 코드가 변수에 접근할 때, 내부 렉시컬 환경이 먼저 탐색되고 그 이후 외부 렉시컬 환경이 탐색된다.
When the code wants to access a variable - the inner Lexical environment is searched first, then the outer one, then the more outer one so on until the global one.
다음과 같은 예제를 살펴보자.
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
여기서도 내부와 외부, 두 가지의 렉시컬 환경 객체들이 존재한다.
모든 함수들은 자신이 생성된 렉시컬 환경을 기억한다. 정확하게 말하면, 모든 함수들은 hidden property인 [[Environment]] 를 가지고 있다. 이는 함수가 생성된 렉시컬 환경의 reference를 저장한다.
즉 counter.[[Environment]]
는 {count: 0}
이라는 렉시컬 환경의 reference 를 가지고 있다. 이는 함수가 어디서 called되었는지와 상관없이 '어디서 생성되었는지' 를 기억하는 원리이다. [[Environment]]
reference는 함수가 생성될 때 같이 정해진다.
그 이후, counter() 가 call 될 때 새로운 렉시컬 환경이 이를 위해 생성되고, 이것의 외부 렉시컬 환경이 counter.[[Environment]]
로부터 reference를 가져온다.
이러한 렉시컬 환경에 대한 이해는 자바스크립트의 중요한 개념인 Closure 를 이해하는 데 필수적이다.
클로저는 일반적으로, 어떤 함수가 자신의 외부에서 선언된 변수에 접근하는 것을 뜻한다. 자바스크립트에서, 모든 함수는 클로저인데 이 이유는 클로저는 함수가 생길 때마다 함수가 생성되는 순간에 자동으로 생기기 때문이다.
클로저는 자바스크립트 고유의 개념이 아니라 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어(Functional Programming language: Scala, Erlang, etc..)에서 사용하는 중요한 특성이다.
MDN 에서의 클로저의 정의는 다음과 같다.
A closure is the combination of a function enclosed with references to its surrounding state(the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function.
클로저의 예를 보자.
function convertUsdToKrw(dollar) {
const rate = 1113.5;
return dollar * rate;
}
convertUsdToKrw(5); //5567.5
const rate = 1113.5;
function convertUsdToKrw(dollar) {
return dollar * rate;
}
convertUsdToKrw(5); //5567.5
이렇게 자바스크립트에서 함수는 매개 변수와 로컬 변수 뿐만이 아니라 외부에서 선언된 변수도 자유롭게 접근을 할 수 있다. 그리고 이렇게 함수가 자신의 밖에서 선언된 변수에 접근하는 것을 클로저라고 한다.
function signUp(username, password, email, phone){
const createUser = () {
console.log(`${username}: ${password}`);
}
const sendNotifications = () {
console.log(`${email}, ${phone}`);
}
createUser();
sendNotifications();
}
signUp()
함수는 4개의 매개 변수를 받고 있는데, createUser()
함수와 sendNotifications()
함수 입장에서 보면 모두 외부에서 선언된 변수들이다. createUser()
함수는 외부에서 선언된 username, password 변수에 접근하고 있고, sendNotifications()
함수는 외부에서 선언된 email과 phone에 접근하고 있다. 다시 말해, signUp()
함수 내부에는 2개의 클로저가 있는 것이다.
만약 createUser() 함수와 sendNotifications() 함수를 signUp()함수 외부로 빼내게 된다면, createUser() 함수와 sendNotifications() 함수에는 매개 변수가 필요하게 되고, signUp() 함수 내애서 호출할 때 인자를 넘겨줘야 한다. 이와 같이 클로저를 활용하면 어떤 함수 내부에서만 사용되는 일회성 함수(정의 후 바로 호출되는) 의 매개 변수를 생략할 수 있다.
클로저가 유용하게 사용되는 상황에 대해 살펴보자.
클로저가 가장 유용하게 사용되는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것이다.
<html>
<body>
<button class="toggle">toggle</button>
<div class="box" style="width: 100px; height: 100px; background: red;"></div>
<script>
var box = document.querySelector('.box');
var toggleBtn = document.querySelector('.toggle');
var toggle = (function () {
var isShow = false;
// ① 클로저를 반환
return function () {
box.style.display = isShow ? 'block' : 'none';
// ③ 상태 변경
isShow = !isShow;
};
})();
// ② 이벤트 프로퍼티에 클로저를 할당
toggleBtn.onclick = toggle;
</script>
</body>
</html>
이처럼 클로저는 현재 상태(위 예제의 경우 .box 요소의 표시 상태를 나타내는 isShow 변수)를 기억하고 이 상태가 변경되어도 최신 상태를 유지해야 하는 상황에 매우 유용하다. 만약 자바스크립트에 클로저라는 기능이 없다면 상태를 유지하기 위해 전역 변수를 사용할 수 밖에 없다. 전역 변수는 언제든지 누구나 접근할 수 있고 변경할 수 있기 때문에 많은 부작용을 유발해 오류의 원인이 되므로 사용을 억제해야 한다.
클로저를 과용하거나 오용하게 되면 코드 품질 측면에서 부정적인 영향을 미칠 수 있다. 왜냐하면 클로저가 많아지게 되면 코드가 읽거나 고치기가 어려워지고 버그가 발생하기 쉬워지기 때문이다. 또한 자신이 생성될 때의 렉시컬 환경을 기억해야 하므로 메모리 차원에서 손해를 볼 수 있다.
즉 최대한 클로저가 중첩되지 않도록 해주는 것이 필요하다.
참고:
https://javascript.info/closure,
https://poiemaweb.com/js-closure