자바스크립트의 모든 함수는 함수가 선언된 당시의 주변 환경을 기억합니다. 여기서 주변 환경이란 함수가 정의된 상위 스코프, 즉 실행 컨텍스트를 의미합니다. 이는 함수를 호출하는 위치가 아닌, 선언된 위치에 따라 결정되며, 따라서 함수는 언제나 상위 스코프의 식별자를 참조하고, 이에 할당된 값을 변경할 수 있습니다.
클로저란?
클로저는 함수와 해당 함수가 참조하고 있는 주변 환경(렉시컬 환경)과의 조합이다. 다른 말로 하자면, 클로저 기능은 내부 함수에서 외부 함수 스코프에 접근할 수 있게 하는 것이다. 자바스크립트에서 클로저는 함수가 생성될 때마다, 함수가 생성되는 시점에 생성된다.
A closure is the combination of a function bundled together (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. In JavaScript, closures are created every time a function is created, at function creation time.
여기서 '함수가 참조하고 있는 렉시컬 환경과의 조합'이란 말은 과연 어떤 의미일까요?
렉시컬 환경
먼저, 렉시컬(lexical)의 의미에 대해 살펴보도록 하겠습니다. 렉시컬의 사전적 정의는 '어휘의'입니다. 이는 사전을 뜻하는 렉시콘(Lexicon)에서 파생되었는데요. 우리는 사전을 찾아볼 때, 문장의 문맥에 따라 단어의 의미를 찾아보는 것이 아니라, 사전에 정의된 의미를 따라 문장의 의미를 파악합니다. 이것이 바로 렉시컬의 의미입니다.
즉, 렉시컬 환경이란 함수가 호출되는 시점에 상위 스코프가 결정되는 것이 아니라, 함수가 선언되는 시점에 정의되는 것을 의미합니다. 전자의 경우를 동적 스코프(dynamic scope), 후자의 경우를 정적 스코프(static scope)라 하기도 합니다.
자바스크립트는 렉시컬 스코프[정적 스코프]를 따릅니다. 따라서 함수의 호출 위치가 아닌, 함수가 정의된 위치에서 상위 스코프가 결정됩니다.
이제 클로저의 의미를 다음과 같이 풀어서 이해해볼 수 있습니다. '자바스크립트에서는 함수가 생성될 때마다 클로저가 생성되며, 이는 렉시컬 스코프 방식을 따른다. 즉, 클로저란 함수와 함수 생성시 형성되는 함수의 렉시컬 스코프적 조합을 일컫는다.'
클로저 예시
글로 보면 다소 난해하지만, 코드로 보면 보다 쉽게 클로저를 이해할 수 있습니다. 다음은 스택오버플로우에서 가져온 클로저의 예시를 약간 변형한 것입니다.
const outer = function() {
const a = 1;
const inner = function() {
console.log(a);
}
return inner; // this returns a function
}
const fnc = outer(); // execute outer to get inner
fnc(); // 1
위 예시에서는 함수 내에 함수(중첩 함수)가 선언되어 있습니다. fnc()
을 호출하면 a
의 값인 1이 출력됩니다. 클로저가 없는 언어에서는 변수 a
는 가비지 콜렉팅 되어 outer
가 종료될 때 사라집니다. 따라서 fnc
호출 시 a
는 더이상 존재하지 않고, 에러가 발생합니다.
그러나 자바스크립트에서 내부 함수를 리턴하고, 이를 fnc
에 할당하는 위의 경우, outer
함수가 종료된다 해도 지역 변수를 계속해서 사용할 수 있습니다. 클로저의 특성을 따라 inner
가 정의되는 시점에 존재했던 변수가 계속 유지되는 것입니다. 즉, 외부 함수 보다 중첩 함수가 더 오래 생명 주기를 유지하는 경우, 외부 함수의 변수를 참조할 수 있는 것입니다.
하나씩 살펴보자면, a
는 outer
스코프에 속해 있습니다. inner
는 상위 스코프인 outer
스코프를 참조합니다. fnc
는 inner
를 참조하며, a
는 fnc
가 존재하는 한 유효합니다. a
는 해당 클로저 안에 있습니다.
이제, 코드를 조금 수정해보도록 하겠습니다. 다음을 실행하면 콘솔에 어떤 결과가 찍힐까요?
const outer = function() {
let a = 1;
const inner = function() {
a++;
console.log(a);
}
return inner; // this returns a function
}
const fnc = outer(); // execute outer to get inner
fnc(); // ?
fnc(); // ?
정답은 차례로 2와 3입니다. 여기서 '함수를 실행할 때마다 a
가 1로 초기화되어, 두 번째 실행에서 3이 아닌 2가 나와야 하지 않을까?'할 수 있습니다. 그러나 자세히 살펴보면 outer()
의 실행 결과로 inner
함수가 리턴되며, 마지막 fnc()
는 외부에서 중첩 함수를 호출하고 있는 것입니다. 이때, 중첩 함수 inner
는 자신의 상위 스코프인 outer
를 참조하고 있으며, 이를 통해 변수 a
에 접근하고 있습니다. 즉, inner 함수는 상위 스코프인 outer 함수를 기억하고 있으며 따라서 다음과 outer 함수의 변수에 접근할 수 있는 것입니다.
참고 자료:
'개발 > JavaScript' 카테고리의 다른 글
자바스크립트 arguments를 사용한 가변 인자 함수 활용 (0) | 2020.11.14 |
---|---|
자바스크립트 엄격 모드 "use strict" 차이점 설명 (0) | 2020.11.07 |
자바스크립트 타이머 setTimeout / setInterval 설명 (0) | 2020.11.07 |