어제의 나보다 성장한 오늘의 나

[자바스크립트] 클로저 본문

공부/JavaScript && jquery

[자바스크립트] 클로저

NineOne 2021. 4. 24. 01:32

먼저 실행 컨텍스트와 스코프 체인에 대해 모른다면 알고 가도록 하자! 더욱 이해가 쉽다.

개념

자바스크립트의 함수는 일급 객체로 취급된다. 이는 함수를 다른 함수의 인자로 넘길 수도 있고, return으로 함수를 통째로 반환받을 수도 있음을 의미한다. 아래의 코드처럼 가능하다.

        function outerFunc(){
            var x = 10; // x:참조되는 외부변수 (=자유변수)
            
            // innerFunc()은 outerFunc()의 실행이 끝난 후 실행된다
            var innerFunc = function(){
                console.log(x); // 10
            };
            return innerFunc;
        }
        
        var inner = outerFunc();
        inner();
  • 여기서 최종 반환되는 함수가 외부 함수의 지역변수에 접근하고 있다는 것이 중요하다.
  • 이 지역변수에 접근하려면, 함수가 종료되어 외부 함수의 컨텍스트가 반환되더라도 변수 객체는 반환되는 내부 함수의 스코프 체인에 그대로 남아 있어야만 접근할 수 있다. 이것이 바로 클로저다.
  • 조금 쉽게 풀어서 정의하면 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수를 클로저라 한다.
  • 따라서 위 예제에서 outerFunc에서 선언된 x를 참조하는 innerFunc가 클로저가 된다.
  • 그리고 클로저로 참조되는 외부 변수 즉! outerFunc의 x와 같은 변수를 자유 변수라고 한다.

 

아래의 코드 결과 값은 어떻게 될까?

	function outerFunc(arg1, arg2){
            var local=8;
            
            function innerFunc(innerAvg){
                console.log((arg+arg2) / (innerArg+local));
            }
            return innerFunc;
        }
        var exan1=outerFunc(2,4);
        exam1(2);
  • outerFunc() 함수가 호출-> 함수 객체인 innerFunc()가 exam1으로 참조
  • outerFunc()가 실행되면서 생성되는 변수 객체가 스코프 체인에 들어가게 되고, 이 스코프 체인은 innerFunc의 스코프 체인으로 참조된다.
  • 따라서 outerFunc() 함수가 종료되었지만, 여전히 내부 함수 (innerFunc())의 [[scope]]으로 참조되므로 가비지 컬렉의 대상이 되지 않고, 접근이 가능하다.
  • 그럼 exam1(2)를 호출하면 arg1, arg2, local 값은 outerFunc 변수 객체에서 찾고, innerAvg는 innerFunc 변수 객체에서 찾는다.

 

그렇다면 이제 클로저를 활용하는 코드를 살펴보자!

특정 함수에 사용자가 정의한 객체의 메서드 연결하기

       function HelloFunction(func) {  // greeting라는 변수가 있는 함수
            this.greeting = "hello";
        }
        // func 프로퍼티에 참조되는 함수를 call() 함수로 호출한다.
        // func 프로퍼티에 자신이 정의한 함수를 참조시켜 호출할 수 있다.
        HelloFunction.prototype.call = function(func){  // 자신의 지역 변수인 greeting만을 인자로 사용자가 정의한 함수에 넘긴다.
            func ? func(this.greeting) : this.func(this.greeting);
        }
        // userFunc함수를 정의하여 Hello펑션에 참조시킨뒤 HelloFunction() 의 지역 변수인 greeting을 화면에 출력시킨다.
        var userFunc = function(greeting) {
            console.log(greeting);
        }
        var objHello = new HelloFunction();
        objHello.func = userFunc;
        objHello.call();	//hello
        
  • HelloFunc은 greeting 변수가 있다. 
  • 다음은 func 프로퍼티로 참조되는 함수를 call() 함수로 호출한다.
  • 사용자는 func 프로퍼티에 자신이 정의한 함수를 참조시켜 호출할 수 있다.
  • 다만 HelloFunction.prototype.call()을 보면 알 수 있듯이 자신의 지역 변수인 greeting 만을 인자로 사용자가 정의한 함수에 넘긴다.
  • userFunc() 함수를 정의하여 objHello.func()에 참조시킨 뒤, HelloFunc()의 지역 변수인 greeting을 화면에 출력한다.
  • 따라서 결괏값이 'hello'가 나온다.

브라우저에서 onclick, onmouseover와 같은 프로퍼티에 해당 이벤트 핸들러를 사용자가 정의해 놓을 수 있는데, 이 이벤트 핸들러의 형식은 function(event){}이다. 이를 통해 브라우저는 발생한 이벤트를 event 인자로 사용자에게 넘겨주는 방식이다. 여기에 event 외의 원하는 인자를 더 추가한 이벤트 핸들러를 사용하고 싶을 때, 클로저를 적절히 활용한다.

사실 클로저를 잘 활용하려면 경험이 가장 중요하게 작용한다. 그래서 많은 개발 경험을 쌓는 것이 가장 좋은 방법이다.

 

함수의 캡슐화

"i am XXX, i live in XXX, XX year Old"라는 문장을 출력하는데. X부분은 사용자에게 인자로 입력받아 값을 출력하는 함수

        // 전역 변수로서 외부에 노출
        // 다른 함수에서 이 배열에 수비게 접근하여 값을 바꿀 수도 있고, 실수로 같은 이름의 변수를 만들어 버그가 생길 수도 있다.
        var buffAr = [
            'I am',
            '',
            'live in',
            '',
            'You are',
            '',
            'years old'
        ];
        
        function getConpletedStr(name, city, age){
            buffAr[1] = name;
            buffAr[3] = city;
            buffAr[5] = age;
            return buffAr.join('');
        };
                               
        var str = getConpletedStr('zzoon','seoul',16);
        console.log(str);
  • 위 방식대로 하면 buffAr 배열은 전역 변수로서, 외부에 노출되게 된다.
  • 이는 다른 함수에서 이 배열에 쉽게 접근하여 바꿀 수도 있고, 실수로 같은 이름의 변수를 만들어 버그가 생길 수 있다.
  • 또는 다른 코드와의 통합 혹은 이 코드를 라이브러리로 만들려고 할 때, 까다로운 문제를 발생시킬 가능성이 있다.

 

클로저를 활용하여 이 문제를 해결할 수 있다.

	// 변수 getCompletedStr에 익명의 함수를 즉시 실행시켜 반환되는 함수를 할당한다
        var getCompletedStr = (function(){
            // 클로저를 활용하여 buffAr을 추가적인 스코프에 넣고 사용
            var buffAr = [
                'I am',
                '',
                'live in',
                '',
                'You are',
                '',
                'years old'
            ];
            
            // 반환되는 함수가 클로저가 되고. 이 클로저는 자유 변수 buffAr을 스코프 체인에서 참조한다
            return (function(name,city,age){
                buffAr[1] = name;
                buffAr[3] = city;
                buffAr[5] = age;
                return buffAr.join('');
            });
        })();
        
        
        var str = getCompletedStr('zzoon','seoul',16);
        console.log(str);
  • 변수 getCompletedStr에 익명의 함수를 즉시 실행시켜 반환되는 함수를 할당
  • 이 반환되는 함수가 클로저가 된다.
  • 이 클로저는 변수 buffAr을 스코프 체인에서 참고할 수 있다.

 

SetTimeout()에 지정되는 함수의 사용자 정의

  • setTimeout()으로 자신의 코드를 호출하고 싶다면 첫 번째 인자로 해당 함수 객체의 참조를 넘겨주면 되자만 이것으로는 실제 실행될 때 함수에 인자를 줄 수 없다.
  • 이 문제를 클로저로 해결 가능
	function callLater(obj, a, b){
            // 반환되는 함수 : 클로저
            return (function(){
                obj['sum'] = a+b;
                console.log(obj['sum']);
            });
        };
        
        var sumObj = {
            sum:0
        };
        
        // 변수 func에 함수를 반환받아 setTimeout() 함수의 첫 번쨰 인자로 넣어주면 된다
        var func = callLater(sumObj,1,2);
        
        // 웹 브라우저에서 제공하는 함수
        // 첫번째 인자로 넘겨지는 함수 실행의 스케줄링을 할 수 있다
        setTimeout(func,500);

 

클로저를 활용할 때 주의사항!

  • 클로저는 자바스크립트의 강력한 기능이지만, 너무 남발하여 사용하면 안 된다. 몇 가지 사항을 보자

클로저의 프로퍼티 값이 쓰기 가능하므로 그 값이 여러 번 호출로 항상 변할 수 있음을 유의

        function outerFunc(argNum) {
             var num = argNum;
            
             return function (x){
                  num += x;
                  console.log('num : ' + num);
             }
        };
        
        // 호출시마다 자유변수 num의 값은 계속해서 변화한다
        var exam = outerFunc(40);
        exam(5);
        exam(-10);
  • 위 예제처럼 exam 값을 호출할 때마다 자유 변수 num의 값은 계속해서 변화한다.

 

하나의 클로저가 여러 함수 객체의 스코프 체인에 들어가 있는 경우

        function func() {
             var x = 1;
            
             // 반환되는 두 함수 모두 자유변수 x를 참조한다
             // 각각의 함수는 호출될 때마다 x값이 변화한다
             return {
                  func1 : function() {console.log((++x);},
                  func2 : function() {console.log(-x)}    
             };
        };
        var exam = func();
        exam.func1();
        exam.func2();
  • 반환되는 객체에는 두 개의 함수가 정의되어 있는데, 두 함수 모두 자유 변수 x를 참조한다.
  • 따라서 각각의 함수가 호출될 때마다 x 값이 변화하므로 유의하자

 

루프 안에서 클로저를 활용할 때 주의

    function countSeconds(howMany) {
         for(var i = 1 ; i <= howMany; i++) {
              setTimeout(function() {
                   console.log(i);
              },i*1000);
         }
    };
    countSeconds(3);
  • 이 코드는 클로저 설명에서 단골로 나오는 예제이다.
  • 위의 결과는 4가 연속 3번 1초 간격으로 출력된다.
  • 왜 그럴까? setTimeout() 함수의 인자로 들어가는 함수는 자유 변수 i를 참조한다.
  • 하지만 이 함수가 실행되는 시점은 countSecondes() 함수의 실행이 종료된 후이고, i 값은 이미 4가 된 상태이다.
  • 그래서 출력이 모두 4가 나오는 것이다.

 

이를 해결하기 위해서는 루프 i 값 복사본을 함수에 넘겨주고, 즉시 실행 함수를 사용한다.

    function countSeconds(howMany) {
         for(var i = 1 ; i <= howMany; i++) {
             (function(curr){
                 setTimeout(function() {
                      console.log(curr);
                 },i*1000);

             })(i);
         }
    };
    countSeconds(3);

 

 

출처

인사이드 자바스크립트(송형주, 고현준 지음) - 한빛미디어

 

Comments