코딩/Javascript

[JS-기초편] 9.클로저(Closures)

드리프트 2020. 12. 17. 21:16
728x170

 

 

지금 쯤이면 함수와 함수의 기능에 대해 많이 알고 계실 거라고 생각합니다.

 

JavaScript의 함수 관련에서 가장 중요한 부분은 아마 클로저라고 생각합니다.

 

클로저는 함수(function)와 변수 유효 범위(variable scope)가 교차하는 교집합이라고 볼 수 있습니다.

 

 

 

 

 

이제는 클로저에 대해 말로만 할게 아니라 이제는 코드를 보면서 설명하겠습니다.

 

클로저가 무엇인지 설명하기 위해 여기서 더 얘기한들 더 혼란스러울 뿐입니다.

 

다음 섹션에서는 익숙한 영역에서 시작해서 클로저랑 관계있는 좀더 어려운 영역으로 진행하면서 설명해 보겠습니다.

 

 

함수 내 함수

 

첫 번째로 할 일은 함수 내에 함수가 있고 내부 함수가 반환될 때 어떤 일이 발생하는지 자세히 살펴 보는 것입니다.

 

일단 먼저 함수의 역할을 다시 한번 되새겨 봅시다.

 

다음 코드를 한번 보시죠.

 

function calculateRectangleArea(length, width) {
    return length * width;
}

var roomArea = calculateRectangleArea(10, 10);
alert(roomArea);

 

calculateRectangleArea 함수는 두 개의 인수를 사용하여 해당 인수를 곱한 값을 반환합니다.

 

위 코드에서 calculateRectangleArea를 호출하는 역할은 변수 roomArea에 의해 제어됩니다.

 

이 코드가 실행 된 후 roomArea 변수에는 10과 10을 곱한 결과가 저장됩니다.

 

 

 

아시다시피 함수가 반환하는 것은 무엇이든 될 수 있습니다.

 

이 경우 숫자를 반환했습니다.

 

텍스트 (일명 문자열), undefined 값, 사용자 정의 객체 등 거의 모든 걸 쉽게 반환할 수 있습니다.

 

함수를 호출하는 코드가 호출되는 함수가 뭘 반환하는지 알고, 또 반환값으로 뭘 해야 하는지 안다고 할때, 프로그래머는 무엇이든지 할 수 있습니다.

 

심지어 반환할 때 다른 함수 자체를 반환 할 수도 있습니다. 좀 더 깊게 알아봅시다.

 

아래 코드는 방금 말한 얘기의 예제입니다.

 

function youSayGoodBye() {

    alert("Good Bye!");

    function andISayHello() {
        alert("Hello!");
    }

    return andISayHello;
}

 

내부에 함수를 포함하는 함수가 있을 수 있고 만들수 있습니다.

 

이 예에는 alert 함수와 andISayHello라는 또 다른 함수가 포함 된 youSayGoodBye 함수가 있습니다.

 

 

 

 

흥미로운 부분은 youSayGoodBye 함수가 호출 될 때 반환하는 것이 andISayHello 함수입니다.

 

function youSayGoodBye() {
...
    return andISayHello;
}

 

계속해서이 이 예제를 실행 해 보겠습니다.

 

이 함수를 호출하려면 youSayGoodBye를 가리키는 변수를 만들어야 합니다.

 

var something = youSayGoodBye();

 

 

위 코드가 실행되는 순간 youSayGoodBye 함수 내의 모든 코드도 실행됩니다.

 

즉, Good Bye!라는 대화 상자 (alert 덕분에)가 표시됩니다.

 

 

 

실행 완료의 일부로 andISayHello 함수가 생성 된 다음 반환됩니다.

 

이 시점에서 우리의 something 변수는 한 가지에만 눈을 돌립니다. 그 것은 andISayHello 함수입니다.

 

 

 

something 변수의 관점에서 보면 외부 함수인 youSayGoodBye 는 바로 사라집니다.

 

something 변수가 이제 함수를 가리 키기 때문에 일반적으로 함수를 호출하는 것처럼 열기와 닫기 괄호를 사용하여 이 함수를 호출 할 수 있습니다.

 

var something = youSayGoodBye();
something();

 

이렇게 하면 반환 된 내부 함수 (일명 andISayHello)가 실행됩니다.

 

이전과 마찬가지로 대화 상자가 표시되지만 이 대화 상자에는 Hello! 라고 내부 함수 내에서 지정한 alert 함수의 문구가 표시됩니다.

 

 

 

일단은 그 함수가 뭘 리턴한다는 것에 의미를 둘 필요가 없고 단지 뭘 반환했는지 즉, 반환 된 값만 중요하게 생각해야 합니다.

 

여기까지는 어렵지 않지만 좀더 어려운 클로저 쪽으로 가까이 가고 있습니다.

 

다음 섹션에서는 예제를 약간 비틀어 좀더 깊게 들어가 보겠습니다.

 

 

 

내부 함수가 독립적이 않을 때

 

 

이전 예제에서 andISayHello 내부 함수는 자체적으로 독립적이었으며, 외부 함수의 변수 또는 상태에 의존하지 않았습니다.

 

function youSayGoodBye() {

    alert("Good Bye!");

    function andISayHello() {
        alert("Hello!");
    }

    return andISayHello;
}

 

 

많은 실제 시나리오에서는 이와 같은 경우는 거의 발생하지 않습니다.

 

외부 함수와 내부 함수간에 공유되는 변수와 데이터가 종종 있습니다.

 

그럼 다음 코드를 살펴보십시오.

 

function stopWatch() {
    var startTime = Date.now();

    function getDelay() {
        var elapsedTime = Date.now() - startTime;
        alert(elapsedTime);
    }

    return getDelay;
}

 

위 코드는 무언가를 수행할 때 시간을 재는 가장 기본적인 방식의 코드입니다.

 

stopWatch 함수의 내부에는 startTime 이라는 변수가 있고 그 값은 Date.now() 라는 현재 시간으로 초기화되었습니다.

 

function stopWatch() {
    var startTime = Date.now();
...
}

 

또한 getDelay라는 내부 함수도 있습니다.

 

function stopWatch() {
...

    function getDelay() {
        var elapsedTime = Date.now() - startTime;
        alert(elapsedTime);
    }
...
}

 

getDelay 함수는 앞에 선언된 startTime과 getDelay 함수가 실행할 때의 시간과의 차이를 alert 함수로 보여주는 내부 함수입니다.

 

stopWatch 라는 외부 함수가 마지막으로 하는 일은 종료하기 전에 getDelay 함수를 반환하는 것입니다.

 

보시다시피 위 코드는 이전 예제와 매우 유사합니다.

 

외부 함수도 있고 내부 함수도 있습니다.

 

그리고 내부 함수를 반환하는 외부 함수가 있습니다.

 

이제 stopWatch 함수가 작동하는지 확인하려면 다음 코드를 실행해 봅시다.

 

var timer = stopWatch();

// do something that takes some time
for (var i = 0; i < 1000000; i++) {
    var foo = Math.random() * 10000;
}
    
// invoke the returned function
timer();

 

최종 HTML 코드는 아래와 같습니다.

 

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>Closures</title>

  <style>

  </style>
</head>

<body>
  <script>
    function stopWatch() {
      var startTime = Date.now();

      function getDelay() {
        var elapsedTime = Date.now() - startTime;
        alert(elapsedTime);
      }

      return getDelay;
    }

    var timer = stopWatch();

    // do something that takes some time
    for (var i = 0; i < 1000000; i++) {
      var foo = Math.random() * 10000;
    }

    // invoke the returned function
    timer();
  </script>
</body>

</html>

 

이 예제를 실행하면 타이머 변수가 초기화되고, for 루프가 실행되고, 타이머 변수가 함수로 호출되기까지 걸린 시간 (밀리 초)을 표시하는 대화 상자가 표시됩니다.

 

 

 

 

위 코드는 요약해서 쉽게 설명해 보면, 여기에 우리가 호출한 스톱워치가 있고, 그 스톱워치로 무언가 시간이 오래 걸리는 작업을 한 다음 다시 호출하여 그 작업이 걸린 시간을 확인한 겁니다.

 

우리의 스톱워치 예제가 잘 작동하는 것을 봤으므로, stopWatch 함수로 돌아가서 정확히 무슨 일이 일어나는지 살펴 보겠습니다.

 

일단은 우리가 위에서 살펴본 youSayGoodBye / andISayHello 예제와 유사합니다.

 

이 예제를 다르게 만드는 약간의 트윅이 있으며, 주목해야 할 중요한 부분은 getDelay 함수가 timer 변수로 반환 될 때 무슨 일이 발생하는지 입니다.

 

 

아래 그림은 약간 부족하지만 무엇이 일어나는지 보여줍니다.

 

 

 

 

stopWatch 외부 함수는 더 이상 작동하지 않으며 timer 변수는 getDelay 함수에 바인딩됩니다.

 

자, 여기에 트윅이 있습니다.

 

getDelay 함수는 외부 stopWatch 함수의 컨텍스트에 있는 startTime 변수에 의존합니다.

 

function stopWatch() {
    var startTime = Date.now(); // stopWatch 컨텍스트에 있는 startTime 변수

    function getDelay() {
        var elapsedTime = Date.now() - startTime; // 외부 함수에 있는 startTime 변수 액세스
        alert(elapsedTime);
    }

    return getDelay;
}

 

getDelay가 timer 변수로 반환 될 때 외부 stopWatch 함수가 사라지면 다음 줄에서 어떤 일이 발생할까요?

 

function getDelay() {
    var elapsedTime = Date.now() - startTime;  // 여기서만 볼때는 startTime은 undefined?
    alert(elapsedTime);
}

 

getDelay 함수 내부에서만 볼때 지금까지 배운대로는 startTime 변수가 실제로 정의되지 않은 undefined 이어야 하지 않나요?

 

그런데 위의 전체 코드는 잘 작동하고 뭔가 다른것이 무언가가 일어나고 있는 거 같습니다.

 

자바스크립트에서는 그 다른 것을 샤이(shy)하고 미스테리한 클로저라고 합니다.

 

이제 startTime 변수가 undefined 값이 아니고 왜 실제적으로 값을 저장하는지 알아 볼 차례입니다.

 

자바스크립트의 런타임은 모든 변수, 메모리 사용, 참조 등 모든 것을 추적합니다.

 

그 정도로 JavaScript 런타임은 영리합니다. 우리가 보고 있는 이 예제에서는 자바스크립트 런타임은 내부 함수 (getDelay)가 외부 함수 (stopWatch)의 변수에 의존하고 있음을 감지합니다.

 

이 경우 런타임은 외부 함수가 사라지더라도 내부 함수에서 필요한 외부 함수의 모든 변수를 계속 사용할 수 있도록합니다.

 

위 내용을 올바르게 시각화하기 위해 다음은 timer 변수의 메모리 모형화 그림입니다.

 

 

 

 

여전히 getDelay 함수를 참조하고 있지만 getDelay 함수는 외부 stopWatch 함수에 있던 startTime 변수에도 액세스 할 수 있습니다.

 

이 내부 함수는 외부 함수의 관련 변수를 버블 (일명 범위)로 묶었기 때문에 클로저라고 합니다.

 

 

 

 

클로저를 보다 공식적으로 정의해 보면, 변수 컨텍스트(variable context)도 포함하고 있는 새로 생성 된 함수입니다.

 

 

 

클로저를 다시 한번 리뷰해 보면, startTime 변수는 stopWatch 함수가 실행되고 timer 변수가 초기화 될 때의 그 시간을 Date.now() 함수를 통해 저장하는 변수입니다.

 

그리고 stopWatch 함수가 내부 함수 getDelay 를 반환할 때, 외부 함수 stopWatch 함수는 사라집니다.

 

그런데 여기서 사라지지 않는 것은 stopWatch 내부에 있는 공유된 변수 즉 내부함수 getDelay가 의존하는 변수입니다.

 

이 공유된 변수는 파괴되지 않습니다.

 

대신, 그 공유 변수는 클러저라는 내부함수를 둘러싸고 있습니다.

 

 

클로저 요약해보기

 

먼저 예제를 통해 클로저를 살펴보면서 지루하고 따분한 이론, 어려운 의미 등 별로 마음에 들지 않을 겁니다.

 

그러나 진지하게 말하면 JavaScript에서 클로저는 매우 일반적이며 중요합니다.

 

우리는 여러 가지 미묘하고 미묘하지 않은 방식으로 클로저를 자주 만날 것입니다.

 

이 모든 것을 제거 할 수있는 것이 있다면 다음 사항을 기억하십시오.

 

클로저가 하는 가장 중요한 것은 환경이 급격히 변하거나 사라지더라도 함수가 계속 작동하도록 하는 것입니다.

 

함수가 생성 될 때 범위에 있던 모든 변수는 함수가 계속 작동하도록 하기 위해 포함되고 보호됩니다.

 

이 동작은 자주 즉석에서 생성, 수정 및 파괴하는 JavaScript와 같은 매우 동적인 언어에 필수적입니다.

 

지금까지 어려운 클로저를 살펴봤습니다.

 

다음편에서 뵐게요.

 

그리드형