[자바스크립트] 클로저란?

chrisjune
6 min readAug 7, 2019

--

자바스크립트의 정적 유효범위(어휘적 스코프)를 이해하고, 이를 응용한 클로저에 대하여 알아봅니다

정적 유효범위란?

자바스크립트에서는 코드가 적힌 순간 변수의 유효범위가 정해집니다. 이것을 정적 유효범위, 또는 멋져보이는 말로 렉시컬(어휘적) 스코프라고 부릅니다.

function main(){
var name = 'chrisjune';
}

var로 선언한 name변수의 유효범위는 함수를 절대로 벗어날 수 없습니다. 즉 함수안에서 선언한 변수는 함수 밖에서 접근할 수 없습니다.

var name = 'chrisjune';
function print(){
console.log(name);
}
function main(){
name = 'choi';
print();
}
main();

위 예의 실행 결과는 choi 입니다. main() 메서드가 실행되고, 전역변수인 name의 값을 choi로 변경하였기 때문에 print()메서드에서 전역변수인 name을 출력하므로 choi를 출력합니다.

var name = 'chrisjune';
function print(){
console.log(name);
}
function main(){
var name = 'choi';
print();
}
main();

위의 실행 결과는 chrisjune입니다. main()메서드의 name 변수는 전역변수인 name변수가 아니고 main()함수안에서 선언되어 함수 밖으로 벗어날 수 없기 때문입니다.

이러한 유효범위는 변수 뿐만아니라 함수에도 동일하게 적용됩니다.

function print(){
console.log('global');
}
function main(){
print();
}
main();

위의 예의 경우, main()메서드에서는 전역함수인 print()를 실행하므로, global을 출력합니다.

function print(){
console.log('global');
}
function main(){
function print(){
console.log('local');
}
print();
}
main();

위의 예에서는, main()메서드에서는 지역함수 print()를 실행하므로, local을 출력합니다.

자바스크립트 언어는 동적언어지만, 유효범위는 코드가 작성되는 순간 정해지는 정적인 특성을 가집니다. 다시한번 정리하면, 함수안에서 정의된 변수는 함수 밖으로 빠져나갈 수 없다는 것이 핵심입니다.

클로저란?

MDN에서는 함수와 함수가 선언된 언어적(정적) 환경의 조합 이라고 정의하고 있습니다. 정의에서 이해가 바로되지 않기 때문에, 예를 통하여 알아보도록 하겠습니다.😄

for (var i=0; i<100; i++){
setTimeout(function(){
console.log(i);
}, i*100);
}

setTimeout메소드는 첫번째 인자로 비동기콜백함수, 두번째인자로 실행할 시간(ms)으로 받는 함수입니다. 0.1초마다 0에서 99까지 출력을 하게하려고 작성을 하였습니다. 하지만 결과는 다음과 같습니다.

100이라는 결과가 100번 출력되었습니다. 그 이유는 반복문이 아래와 같이 실행되기 때문입니다.

setTimeout(function(){
console.log(i);
}, 0)
setTimeout(function(){
console.log(i);
}, 100)
setTimeout(function(){
console.log(i);
}, 200)
...setTimeout(function(){
console.log(i);
}, 9900)

setTimeout비동기 함수가 0.1초 마다 생성되고, 변수 i 를 출력합니다. 실제로 이 비동기 함수가 실행될때는 i가 100이되어 반복문이 종료되었기 때문에, 처음부터 100으로 출력이 됩니다.

좀더 자세하게 설명하자면, 자바스크립트는 단일 스레드언어이기 때문에 콜스택에 작업을 push하고 처리할때마다 하나씩 pop합니다. setTimeout과 같은 비동기작업은, 콜스택이 아닌 이벤트큐에 저장해놓습니다. 그 후 이벤트루프가 콜스택이 비어있는 순간에 이벤트 큐에 쌓여있던 작업을 push하여 콜백함수를 실행시켜줍니다. 위의 예에서는 콜백함수를 실행할때 i는 이미 100이 되어있기 때문에 0이아니라 100이 출력됩니다.

해결방법.

for (var i = 0; i < 100; i++) {
function call(j) {
setTimeout(function () {
console.log(j);
}, j * 100);
}
call(i);
}

변수의 정적 유효범위를 활용하여 비동기함수에서 처리할 변수의 값을 함수로 감싸줍니다. 그러면 i 값이 100이 되어도 비동기 함수안에서 사용하는 j값은 고정되어 변하지 않습니다. 마치 함수로 변수를 고정(폐쇄)한다고 하여 이를 클로져(closure)라고 부릅니다.

위의 반복문 결과는 풀어서 아래와 같이 실행됩니다

function call(j) {
setTimeout(function () {
console.log(j);
}, j * 100);
}
call(0);
function call(j) {
setTimeout(function () {
console.log(j);
}, j * 100);
}
call(1);
...function call(j) {
setTimeout(function () {
console.log(j);
}, j * 100);
}
call(99);

매개변수 또한 지역변수이기 때문에 지역변수가 call함수안에서 고정되어있어 변하지 않습니다.

실행결과는 아래와 같습니다.

많은 경우 반복문과 비동기 함수가 만날 때 클로져 이슈가 자주 발생합니다. 위의 코드는 아래와 같이 즉시실행하는 형식을 더 많이 사용합니다.

for (var i = 0; i < 100; i++) {
(function call(j) {
setTimeout(function () {
console.log(j);
}, j * 100);
})(i);
}

참고자료

--

--