개요
자바스크립트에서 중요한 개념 중 하나인 클로저(Closure)에 대해 알아본다.
들어가기에 앞서
자바스크립트를 배우며 클로저라는 것에 대해 자주 들어보았을 것이다. 다만 정확히 이해하고, 설명하기는 다소 어려운 개념 중 하나이다.
우리가 작성한 코드에도 이미 클로저가 녹아들어가 있고, 자바스크립트의 모든 곳에 존재한다. 우리가 모르는 사이에 클로저를 접하고 있고, 이는 곧 클로저는 새롭게 배워야 하는 특별한 문법이나 패턴이 아니라는 것이다.
이전 글에서 스코프란 선언된 대상(변수, 함수 등)을 찾기 위한 규칙이라고 설명했다. 클로저를 이해하기 위해서는 렉시컬 스코프에 대해 이해하고 있어야 하며 스코프의 동작 체계 즉, 규칙은 클로저를 이해하는 데 있어 필수적이다.
이번 글에서는 이 클로저에 대해 알아볼 예정이고, 스코프에 대해 아직 정리가 잘 되지 않은 상태라면 이전 글을 다시 한 번 보고 정리해볼 수 있도록 하자.
클로저(Closure)
MDN 문서에 따르면, 클로저를 아래와 같이 설명하고 있다.
클로저는 주변 상태(어휘적 환경)에 대한 참조와 함께 묶인(포함된) 함수의 조합입니다.
즉, 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공합니다.
JavaScript에서 클로저는 함수 생성 시 함수가 생성될 때마다 생성됩니다.
설명이 어려워 보일 수 있는데, 크게 걱정하지 말자. 우리는 은연중에 이미 클로저를 접하고 있다.
아래 예제 코드를 먼저 살펴보자.
function foo() {
var value = 0;
function bar() {
console.log(value);
}
bar();
}
foo();
코드의 실행 결과는 0이며, 코드의 실행 과정을 스코프를 기준으로 정리해보자.
foo
함수 스코프 내에 value
가 선언되어 있고, bar
함수 내에서는 스코프 체이닝을 통해 상위 스코프(foo
)에 선언된 value
를 참조한다.
코드를 실행했을 때의 호출 스택(Call Stack)을 시각화하면 아래와 같다.
이 예제에서도 우리가 모르는 사이 클로저가 사용되었다. value
변수를 유심히 살펴보자.
foo
함수 스코프에 선언된 변수이지만, bar
함수 스코프 내에서 참조하고 있다. 이는 렉시컬 스코프 동작에 따라 설명 가능함과 동시에 bar
함수가 foo
함수 스코프에 대한 클로저를 갖기 때문이라고 설명할 수도 있다.
그럼 렉시컬 스코프의 동작과 클로저가 동일한 의미를 갖는 것일까?
렉시컬 스코프는 클로저의 일부일 뿐 동일하지는 않다. 즉, 렉시컬 스코프만으로는 클로저에 대해 설명하기엔 부족하다.
여전히 클로저가 무엇인지, 어느 것을 이야기하는 것인지 이해가 잘 되지 않을 것이다. 조금 더 구체적으로 클로저 개념에 대해 확인할 수 있는 아래 예제를 살펴보자.
function createLogger(tag) {
function logger(...messages) {
return `[${tag}] ${messages.join(' ')}`;
}
return logger;
}
const systemLogger = createLogger('SYSTEM');
systemLogger('Hello, world!');
createLogger
함수는 하나의 매개변수 tag
를 갖고 있고, 함수 내에는 logger
라는 함수가 정의되어 있다.
여기서 주의 깊게 살펴볼 부분은 아래 두 가지이다.
logger
함수 내에서createLogger
함수의 매개변수tag
를 참조하는 부분logger
함수가createLogger
함수 내부에 선언되어 있지만, 선언된 범위 밖에서 호출하는 부분
위 코드를 실행했을 때의 호출 스택을 시각화하면 아래와 같다.
코드를 실행해 보면 "[SYSTEM] Hello, world!"가 콘솔에 출력된다.
결과를 보면 createLogger
함수가 호출 스택에서 빠져나갔음에도 logger
함수 내에서 tag
를 정상적으로 참조하고 있는 모습을 확인할 수 있다. 이것이 바로 클로저의 마법이다.
단순히 생각해 보면 createLogger
함수가 종료되어 호출 스택에서 제거될 경우 자바스크립트 엔진의 GC(Garbage Collector)로 인해 관련된 메모리 정보가 소멸될 것이라고 생각할 수 있다. 그러나 createLogger
함수의 스코프는 logger
함수가 여전히 사용하고 있기 때문에 소멸되지 않고 유지된다.
"createLogger
함수의 스코프를 logger
함수가 사용하고 있다"의 기준은 렉시컬 스코프와 동일하게 코드의 위치 즉, 우리가 작성한 코드를 기준으로 결정된다. 코드를 확인해 보면 logger
함수 내에서 createLogger
함수 스코프의 변수 tag
를 참조하고 있다는 것을 명확하게 확인할 수 있고 이것이 곧 기준이 된다.
처음에 확인했던 클로저의 정의에 대해 다시 살펴보자.
클로저는 주변 상태(어휘적 환경)에 대한 참조와 함께 묶인(포함된) 함수의 조합입니다.
즉, 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공합니다.
JavaScript에서 클로저는 함수 생성 시 함수가 생성될 때마다 생성됩니다.
어휘적 환경은 렉시컬 스코프를 의미하며 앞서 살펴본 예제처럼 함수가 어느 위치에서 호출되던 외부 함수의 스코프에 접근할 수 있도록 참조를 유지하는 것이 클로저라는 말이다.
마무리하며
이번 글에서는 클로저에 대해 살펴보았다. 클로저는 자바스크립트를 배우는 데 있어 꽤나 난해한 주제 중 하나이다.
글에서도 이야기했지만 클로저는 새롭게 배워야 하는 특별한 문법이나 패턴이 아니며, 렉시컬 스코프에 대해 이해하고 있다면 생각보다 클로저는 어려운 개념이 아니라는 것을 깨달았을 것이다.
클로저는 자바스크립트가 어떻게 동작하는지 이해하는 데 있어 중요한 개념이니 숙지하고 있길 바란다.