Closer 아니다
Closure는 폐쇄, 종료라는 뜻의 단어로, 컴퓨터 프로그래밍에서는 아래와 같은 의미로 쓰인다.
“컴퓨터 언어에서 클로저(Closure)는 일급 객체 함수의 개념을 이용해 스코프에 묶인 변수를 바인딩 하기 위한 일종의 기술이다. 클로저는 함수를 저장한 레코드이며, 스코프의 인수들은 클로저가 만들어질 때 정의되며, 스코프 내의 영역이 소멸 되었어도 그에 대한 접근은 복사본인 클로저를 통해 이루어질 수 있다.” - 위키백과
💡 일급 객체 함수 또는 일급 함수는 함수를 일급 객체로 취급하는 것을 의미하며, 함수를 객체와 동일하게 사용(변수에 저장, 매개변수로 전달 등)할 수 있는 것이다.
클로저란 함수에 대한 정보를 기록한 것 정도로 요약할 수 있겠다.
다음으로는 mdn docs의 클로저에 대한 설명이다.
“클로저는 주변 상태(어휘적 환경, Lexical Scope)에 대한 참조와 함께 묶인 함수의 조합입니다. 즉, 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공합니다. JavaScript에서 클로저는 함수 생성 시 함수가 생성될 때마다 생성됩니다.”
이에 대해 더 알아보기 위해 우선 Lexical Scope에 대해서 먼저 알아보겠다.
프로그래밍에서 scope란 변수가 유요성을 갖는 영역을 의미한다. Lexical은 어휘적, 사전적이라는 의미를 가진 단어로, Lexical Scope 는 어휘적 scope라고 직역할 수 있겠다.
Lexical Scope에 대해서 알아보기 전에 JavaScript의 Scope에 대해서 먼저 알아보겠다.
JS에서 함수와 변수에 적용되는 Scope는 아래와 같다.
Global Scope : 함수나 block 외부에 선언된 것으로, 프로그램의 어떠한 곳에서도 접근할 수 있다.
Local Scope : 함수 내부에 선언된 것으로, 해당 함수나 block에서 만 접근할 수 있다.
Nested Scope : 함수 내 정의된 함수는 그 외부 함수의 변수에 접근할 수 있다.
Block Scope : let 또는 const keyword를 통해 선언된 변수는 선언된 block 내에서 만 접근할 수 있다.
각 scope에 대해서 예시를 통해 알아보겠다.
함수나 block 밖에서 선언된 변수는 Global Scope를 가지며, 프로그램 내 어떠한 곳에서도 접근할 수 있는 변수이다.
// Global Scope
let a = "a";
function func1() {
console.log(a);
}
func1(); // print - a
함수 내부에 선언된 변수는 Local Scope를 가지며, 해당 함수에서 만 접근할 수 있는 변수이다.
function func1() {
let a = "a"; // Local Scope
console.log(a);
}
func1(); // print - a
console.log(a); // Uncaught ReferenceError
함수와 Block은 다르며, ES6+ 부터 let / const 키워드를 통해 Block을 Scope로 취급하게 되었다.
기본적으로 JavaScript의 Scope는 Global, Local 둘 뿐이었으며, 추가적으로 Lexical Scope를 활용한 Nested Scope, ES6+의 Block Scope가 있다. ES6 이전의 변수는 var 키워드를 통해서 선언하며, 해당 변수는 선언 위치에 따라 Local(함수 내부 선언) 또는 Global(함수 외부 선언) Scope를 가진다.
어떠한 함수가 다른 함수 내부에서 정의된 경우, 해당 함수(내부, 자식 함수)는 외부 함수(부모 함수)에서 정의된 변수에 접근할 수 있으며, 이를 Nested Scope라 부른다.
function func1() {
let a = "a"; // Local Scope
function func2() {
// Nested Scope
console.log(a);
}
func2(); // print - a
}
func1();
이와 같은 Scope를 구성할 수 있는 이유는 이후 작성할 Lexical Scope와 관련이 있다.
ES6 에서 도입된 let 또는 const 키워드를 통해 선언된 변수에 해당하며, block 내 해당 키워드를 통해 선언된 변수는 해당 block 내부에서 만 접근할 수 있다. var 키워드를 통해 선언된 함수는 선언 위치에 따라 Global Scope 또는 함수 레벨(Function Level)의 Local Scope를 갖는다.
function func1() {
{ // Block
let a = "a"; // Block Scope
console.log(a); // print - a
}
console.log(a); // Uncaught ReferenceError
}
func1();
JavaScript는 기본적으로 함수 레벨(Function Level)의 Local Scope를 지원한다. 함수 내 block에서 선언된 변수가 있어도, 이는 함수 전체에서 접근이 가능한 변수이다. ES6에서 도입된 let 또는 const를 통해서 함수 레벨이 아닌 Block 레벨의 Scope를 구현할 수 있게 되었으며, 두 scope의 차이는 아래 코드를 통해 확인할 수 있다.
function func1() {
{
var a = 2;
console.log(a); // print - 2
}
console.log(a); // print - 2
}
func1();
function func1() {
{
let a = 2;
console.log(a); // print - 2
}
console.log(a); // Uncaught ReferenceError
}
func1();
위에서 알아본 JavaScript의 Scope는 변수의 선언 위치에 따라 해당 변수에 대한 접근, Scope가 정해지는 것을 확인할 수 있었다. Lexical Scope란 Scope를 결정하는 하나의 개념으로, 선언 위치를 기준으로 Scope를 결정하는 방식이다. 다른 방식으로는 Dynamic Scope가 있으며, 이는 호출 시점을 기준으로 Scope를 결정한다.
또한, Dynamic Scope와 달리 runtime에 결정되는 것이 아닌 compile 시 결정되기 때문에 Lexical Scope는 Static Scope(정적 Scope)라고 불리기도 한다.
두 방식의 차이를 간략한 예시를 통해 알아보겠다.
var a = 2;
function func1() {
console.log(a);
}
function func2() {
var a = 3;
func1();
}
func2();
JavaScript는 Lexical Scope를 사용하기 때문에, Dynamic Scope를 구현할 수 없지만, 위 코드를 실행했을 때 2가 출력 되는 것이 Lexical Scope, 3이 출 되는 것이 Dynamic Scope라고 할 수 있다.
Lexical Scope는 func1()의 선언에 맞추어 a를 2로 정하며, Dynamic Scope는 func1()의 호출 시점에 맞추어 func2()의 context에서 a의 값을 찾아 3으로 정한다. 함수의 context와 scope chain 등 관련해서 더 자세한 내용은 추후에 작성해 보도록 하겠다.
클로저에 더 알아보기 전 두 개의 예시 코드를 살펴보겠다.
var a = 10;
function func1() {
var a = 11;
function func2() {
console.log(a);
}
func2();
}
func1(); // print - 11
var a = 10;
function func1() {
var a = 11;
function func2() {
console.log(a);
}
return func2;
}
var func3 = func1(); // closure
func3(); // print - 11
위 두 코드의 실행 결과는 11 출력으로 동일하다. 1번 example에서 func1() 호출과 함께 a 값이 11이 되는 것은 직관적이지만, 2번 example의 경우 조금 다르다. func1()이 호출된 후 일반적인 경우에서 내부 지역변수 a는 접근이 불가능하게 된다.
하지만, 위와 같이 동작이 가능한 이유는 JavaScript가 클로저를 생성하기 때문이다. 앞서 mdn docs에 따르면, 클로저는 주변 상태(Lexical Scope)에 대한 참조와 함께 묶인 함수의 조합이라고 했다. 즉, 2번 예제에서 변수 func3에 할당되는 것은 func1()를 실행할 때 생성된 func2 함수의 Instance 에 대한 참조이며, 이는 실행 시의 주변 환경(Lexical Scope)을 포함하는 클로저라고 할 수 있다.
JavaScript의 변수에 할당되는 모든 함수가 클로저인 것은 아니며, 위 2번 예제와 같이 함수 외부의 scope(주변 환경)에 대한 접근이 있는 함수가 변수에 할당 되는 경우 클로저라고 할 수 있다.

setTimeout() 과 관련하여 클로저로 인해 발생할 수 있는 문제점에 대해서 알아보겠다.
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
위 코드는 예상과 달리 10을 10번 출력한다. 사실 let 쓰면 되기는 하는데
위 코드의 실행 과정을 보다 자세히 보면
console.log(i) 를 실행위 과정에서, 각 함수는 각각의 closure를 갖지만, 모두 i 값을 참조하고 있기에 이미 증가 해 버린 10을 출력하게 된다.
for (var i = 0; i < 10; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 100);
})(i);
}
앞의 코드와 달리, i 값을 함수에 argument로 전달하도록 함수를 작성해 주었다. 위 코드의 실행은
i 값을 j 로 전달하며, i 값을 증가console.log(j)를 실행위와 같이 IIFE를 활용해 Closure가 참조할 Scope를 새로 생성해 문제를 해결할 수 있다.
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
또는 Block Scope를 가지는 let을 통해서 해결할 수 있다. let 키워드로 선언된 변수는 Block Scope를 가진다. 즉, for loop가 반복될 때 마다 새로운 i 값이 생기며 각 Closure는 다른 i 값을 저장하게 된다.