클로저
자바스크립트에 대해 조금이라도 관심을 가진 사람들은 클로저라는 난해한 개념에 대해 들어보셨을 겁니다. 사실 대부분의 사람들은 사전지식없이 클로저를 먼저 공부하고는 합니다. 클로저는 렉시컬 환경과 렉시컬 스코프에 대한 사전 지식이 필요합니다. 먼저 MDN 에서 정의된 클로저의 정의에 대해서 이야기 해보도록 하겠습니다.
MDN 에서 클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합 이라고 이야기하고 있습니다. 함수는 잘 알고 있지만, 함수가 선언된 렉시컬 환경은 딱 와닿지가 않습니다. 먼저 렉시컬 스코프에 대해서 알아보겠습니다.
렉시컬 스코프
자바스크립트 엔진은 함수의 상위스코프를 결정할 때 호출한 위치가 아닌 함수의 정의 위치에 따라 결정합니다. 이러한 특징을 렉시컬 스코프라고 부릅니다. 아래 예제를 통해 확인해보겠습니다.
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // 1
bar(); // 1
foo 함수와 bar 함수는 둘다 전역에서 정의된 전역 함수입니다. 렉시컬 스코프에 따르면, 함수는 정의된 공간에서 상위 스코프가 결정되는 정적 결정 방식입니다. 따라서 foo, bar 함수 모두 전역을 상위 스코프로 가지고 foo 함수 내부에 있는 x = 10 이 아닌 전역의 x = 1 을 출력합니다.
이전에 실행 컨텍스트에서 살펴봤듯이, 스코프는 실행 컨텍스트의 렉시컬 환경입니다. 렉시컬 환경은 외부 렉시컬 환경에 대한 참조로 상위 렉시컬 환경과 연결되고 이를 스코프 체인이라고 불렀습니다.
즉, 상위 스코프 결정이라는 말은, 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 값을 결정한다는 것과 같습니다. 외부 렉시컬 환경에 대한 참조에 저장할 참조값은 상위 렉시컬 환경에 대한 참조(이를 상위 스코프라고 부릅니다.)이기 때문입니다.
렉시컬 스코프란, 렉시컬 환경에서 외부 렉시컬 환경에 대한 참조에 저장할 참조값이 함수 정의가 평가되는 시점에 함수 정의 위치에 따라 정적으로 결정되는 것을 말합니다.
함수 객체의 내부 슬롯 [[Environment]]
함수 정의 환경과 함수 호출 환경은 다를 수 있습니다. 렉시컬 스코프가 가능하려면 호출되는 위치에 상관없이 자신이 정의된 환경(상위 스코프) 을 기억해야 합니다. 이를 위해 함수는 내부 슬롯 [[Environment]] 에 상위 스코프의 참조를 정합니다.
함수 객체가 생성되면 내부 슬롯 [[Environment]] 에 상위 스코프의 참조를 저장합니다. 만약 전역 실행 컨텍스트가 실행되고 있다면 이 때 전역에 있는 함수들을 정의하게 됩니다. 이를 통해 함수 객체가 생성되고 이때 실행 중인 실행 컨텍스트가 함수의 상위 스코프가 됩니다.
클로저와 렉시컬 환경
실행 컨텍스트에서 살펴봤듯이, 더 이상 실행할 코드가 없다면 실행 컨텍스트 스택에서 pop 되어 제거되게 됩니다. 이때 컨텍스트는 제거되지만 렉시컬 환경이 참조 되고 있다면 사라지지 않는다(가비지 컬렉팅의 대상이 되지 않는다) 라고 했습니다. 아래 예제를 보도록 하겠습니다.
const x = 1;
// ①
function outer() {
const x = 10;
const inner = function () { console.log(x); }; // ②
return inner;
}
// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer(); // ③
innerFunc(); // ④ 10
outer 함수가 호출되면 중첩 함수인 inner 를 반환하고 outer 실행 컨텍스트는 실행 컨텍스트 스택에서 pop 되게 됩니다. 이때 outer 는 생명 주기를 마감합니다. 따라서 outer 함수 내부에 선언된 inner 함수가 참조하고 있는 const x = 10 을 더이상 참조할 수 없을 것처럼 보입니다.
하지만 전역의 맨 마지막 코드인 innerFunc() 를 호출하면 놀랍게도 10 이라는 실행 결과가 나옵니다. 이는 outer 의 렉시컬 환경을 inner 함수가 참조하고 있기 때문에 가비지 컬렉팅의 대상이 되지 않아 outer 실행 컨텍스트가 제거되도 사라지지 않는 것입니다.
이처럼 자신을 포함하는 외부 함수보다 중첩 함수가 더 오래 유지되면 외부 함수의 밖에서 중첩 함수를 호출시에 외부 함수의 지역 변수에 접근이 가능합니다. 이러한 함수를 클로저라고 부릅니다. 일반적으로 중첩 함수가 상위 스코프의 식별자를 참조하고, 중첩 함수가 외부 함수보다 더 오래 유지되는 경우를 한정해서 클로저라고 부릅니다.
사실 이러한 부분은 불필요하게 메모리를 점유하고 있다고 생각할 여지를 남겨놓습니다. 하지만 모던 자바스크립트의 엔진에서는 최적화를 통해 상위 스코프의 식별자 중에서 필요한 식별자만 기억하도록 구현되어 있습니다. 즉, 클로저의 메모리 낭비는 걱정할 정도로 크지 않습니다.
클로저의 활용
클로저는 상태를 안전한게 변경, 유지하기 위해 사용합니다. 즉, 상태를 안전하게 은닉하고 특정 함수의 경우에만 상태 변경을 허용하기 위해서 사용합니다. 아래와 같은 예제를 통해서 자세히 알아보도록 하겠습니다.
// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태를 1만큼 증가 시킨다.
return ++num;
};
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
위 코드는 정상적으로 동작합니다. increase() 를 호출해서 num 을 증가시키는 원하는 결과를 도출했습니다. 하지만 num 은 increase() 호출뿐만 아니라 다른 방식으로도 변경이 가능하고 접근이 가능합니다. 카운트인 num 이 전역으로 선언되어 있기 때문입니다.
그렇다면 카운트인 num 을 전역이 아닌 지역으로 만들기 위해서 함수 내부로 넣어준다면 해결 되지 않을까요?
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태 변수
let num = 0;
// 카운트 상태를 1만큼 증가 시킨다.
return ++num;
};
// 이전 상태를 유지하지 못한다.
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
정말 아쉽게도 함수 내부에 변수 선언시 함수가 호출될 때마다 let num = 0; 문이 실행됩니다. 즉, 변경되기 이전의 상태를 유지하지 못한다는 것입니다.
그렇다면 이전 상태 유지를 위해서 클로저를 사용해보도록 하겠습니다.
// 카운트 상태 변경 함수
const increase = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저
return function () {
// 카운트 상태를 1만큼 증가 시킨다.
return ++num;
};
}());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
코드가 어려울 수도 있지만 결국 const increase 에는 return 값으로 function 이 들어가게 됩니다. 반환한 클로저(외부 함수의 지역 변수인 num 을 참조하고 있고, increase 에 할당되어 외부 함수보다 오래 살아남습니다.)는 상위 스코프(즉시 실행 함수의 렉시컬 환경) 을 기억하고 있습니다. 언제든지 num 을 호출할 수 있고, 참조, 변경이 가능합니다.
즉시 실행 함수이기 때문에 num 변수는 한 번만 초기화 되고 increase 함수를 제외하고는 접근할 방법이 없어 안정적인 환경에 놓여있다고 얘기할 수 있습니다.
클로저를 사용하면 이처럼 상태의 의도치 않은 변경을 막아 안전하게 은닉이 가능하고 특정 함수에게만 상태 변경을 허용해 상태를 안전하게 변경, 유지가 가능해집니다.
마무리
클로저는 생각보다 어려운 내용은 아닙니다. 클로저를 이해하기 위한 실행 컨텍스트의 내용이 어렵기 때문에 이해하기 어려워 보이는 것 뿐입니다. 실행 컨텍스트를 제대로 이해하고 렉시컬 환경과 상위 스코프에 대한 이해가 명확하다면 클로저를 이해하는데에 큰 문제는 없을 거라고 생각합니다.
보다 더 자세한 내용은 아래 링크에서 확인 가능하고, 저 또한 내용들을 공부하며 정리하기 위한 목적으로 글을 남기는 것을 알려드립니다. 읽어주셔서 감사합니다!
'자바스크립트 > 자바스크립트 정리' 카테고리의 다른 글
[자바스크립트] 정렬 구현해보기 (0) | 2021.06.01 |
---|---|
[자바스크립트] ES6 의 함수 (0) | 2021.05.30 |
[자바스크립트] 실행 컨텍스트 (2) (0) | 2021.05.29 |
[자바스크립트] 실행 컨텍스트 (1) (0) | 2021.05.29 |
[자바스크립트] 자바스크립트에서의 this (0) | 2021.05.29 |