클로저(closure)가 뭘까?

윤희준·2022년 3월 23일
0

JS

목록 보기
2/4

MDN Web Docs에 따르면, 클로저(closure)란 함수와 함수가 선언된 어휘적 환경의 조합 이다.
더 쉽게 말하면, 클로저는 외부 함수 호출이 종료되더라도 외부 함수의 지역 변수를 내부 함수가 사용할 수 있는 구조 라고 할 수 있겠다.

사전 지식

클로저에 대해 설명하기 전에, 아래 내용을 먼저 이해하고 넘어가자.

Lexical scope

아래 코드를 실행하면 어떤 일이 일어날까?

function init() {
    function displayName() { 
      var name = "Look at me!";
      alert(name); 
    }
    displayName();
  }
  init();

이 경우, init()"Look at me!" 라는 메시지를 alert한다.

자바스크립트 파서는 함수가 선언될 때 해당 함수의 스코프 를 결정한다. 이걸 Lexical scoping 이라고 한다.

자바스크립트 엔진은alert(name) 을 실행하기 위해 displayName() 의 스코프를 탐색해 name = "Look at me!"라는 값을 찾아내고, alert로 출력한다.

그럼 아래와 같은 경우는 어떨까?

var x = 1;

function first() {
  var x = 10;
  second();
}

function second() {
  console.log(x);
}

first(); 
second(); 

101이 출력될 것이라 생각할 수 있지만, 사실 위의 코드는 11을 출력한다. 이는 second()first()안에서 호출되었지만, 그와 상관없이 second()global 범위에 선언되어 있으므로 global의 x값인 1을 출력한 것이다.

클로저가 기억하는 '환경' 이란, 바로 Lexical scope를 말한다.

스코프 체인

그렇다면 아래 코드를 실행하면 어떤 일이 일어날까?

function init() {
    var name = "Mozilla";
    function displayName() { // displayName() 은 내부 함수이며, 클로저다.
      alert(name); // 부모 함수에서 선언된 변수를 사용한다.
    }
    displayName();
  }
  init();

이 경우, init()"Mozilla" 라는 메시지를 alert한다. 왜 위 코드와 다른 결과가 나왔을까?

자바스크립트 엔진은alert(name) 을 실행하기 위해 displayName() 의 스코프를 탐색한다. 하지만 name을 찾을 수가 없다!

이런 경우, 자바스크립트는 스코프 체인 을 따라 상위 스코프에서 변수를 찾는다.

클로저 예시

아래 코드에서 일어나는 일을 생각해 보자.

var color = 'red';
function foo() {
    var color = 'blue'; // 2
    function bar() {
        console.log(color); // 1
    }
    return bar;
}
var baz = foo(); // 3
baz(); // 4
  1. barcolorconsole 출력하는 함수로 선언되었다.
  2. 선언과 동시에 bar의 스코프가 결정된다.
  3. global에서 baz(=bar)를 호출했다.
  4. bar는 미리 결정된 자신의 스코프에서 color를 찾는다.
  5. 찾을 수 없다. 상위 스코프를 찾는다.
  6. 상위 스코프인 foo의 스코프를 뒤져 color='blue'를 찾는다.

위 코드에서 baz(=bar)는 클로저이기 때문에 global에서 호출되었음에도 불구하고 호출된 위치와 관련 없이 foo에서 color를 탐색한다.

아래와 같은 코드에서는 어떤 일이 벌어질까?

function count() {
    var i;
    for (i = 1; i < 10; i += 1) {
        setTimeout(function timer() {
            console.log(i);
        }, i*100);
    }
}
count();

놀랍게도 1,2,3,4,...9가 0.1초마다 출력되는 대신, 10이 9번 출력된다. 앞선 예제와 같이 코드에서 벌어지는 일을 해석해 보자.

  1. timeri를 출력하는 함수로 선언되었다.
  2. 선언과 동시에 timer의 스코프가 결정된다.
  3. setTimeout으로 인한 0.1초의 대기시간이 지날 동안, for문이 종료되고 i = 10이 된다.
  4. 첫 0.1초가 지나고 timer가 호출된다.
  5. timer는 자신의 스코프에서 i를 찾는다.
  6. 찾을 수 없다. 상위 스코프를 찾는다.
  7. 상위 스코프인 foo의 스코프를 뒤져 i=10을 찾는다.
  8. 다음 0.1초가 지나고 time이 호출된다. 위의 과정을 반복하여 i=10을 찾는다.

그럼 의도대로 1~9를 출력하고 싶다면 어떤 방법을 사용해야 할까?

첫 번째로, 새로운 스코프를 추가하여 반복 시마다 각각 따로 값을 저장할 수 있다.

 function count() {
     var i;
     for (i = 1; i < 10; i += 1) {
         (function(countingNumber) {
             setTimeout(function timer() {
                 console.log(countingNumber);
             }, i * 100);
         })(i);
     }
 }
 count();

위 코드에서, timer는 즉시 실행 함수(IIFE)의 스코프에서 countingNumber를 찾는다. timer는 IIFE의 스코프에서 i의 값이 저장된 countingNumber를 찾아 출력한다.

두 번째로, var 대신 let을 사용할 수 있다.

 function count() {
     'use strict';
     for (let i = 1; i < 10; i += 1) {
         setTimeout(function timer() {
             console.log(i);
         }, i * 100);
     }
 }
 count();

위 코드에서는 var 대신 let이 사용되었다. var함수 레벨 스코프를 따르지만, let은 블록 레벨 스코프를 따른다. var를 사용했을 때, i의 스코프는 count()함수 내부가 된다. 그러나 let을 사용했을 때, i의 스코프는 for문 내부가 된다.

달리 말해, for문이 돌면서 i=0인 스코프, i=1인 스코프, i=2인 스코프...를 생성하고, timer()가 실행될 시 각각 해당하는 스코프에서 i를 찾아 출력하기에 1~9가 출력된다.

성능 관련 고려 사항

각각의 클로저는 환경을 기억한다. 그 말은 메모리가 소모된다는 뜻이다. C++에서 동적 할당으로 생성한 객체를 delete하듯이, 사용이 끝난 클로저는 아래와 같이 참조를 제거해야 한다.

function myName(name) {
  var _name = name;
  return function() {
    console.log('my name is ' + _name);
  };
}

var myName1 = myName('철수');
var myName2 = myName('영희');

myName1(); // 'my name is 철수'
myName2(); // 'my name is 영희'

// 메모리 release
myName1 = null;
myName2 = null;

참고:

profile
블로그 이전 작업 중입니다!

0개의 댓글