ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코어 자바스크립트 - 클로저
    코어 자바스크립트 2025. 6. 25. 21:51

     

    클로저 개념

     

    지금까지 클로저는 외부 함수의 변수를 참조하는 내부함수 정도로만 이해하고 있었다. 이 정도 설명으로는 개념이 확 와 닿지가 않는다. 실제 예제를 통해 좀 더 명확히 밝혀보자.

     

    var outer = function () {
      var a = 1;
      var inner = function () {
        return ++a;
      };
      return inner;
     };
     var outer2 = outer();
     console.log(outer2()); // 2
     console.log(outer2()); // 3

     

    inner 함수 자체를 반환했다. outer2를 호출하면 inner 함수가 실행된다. inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmentReference가 outer 함수의 LexicalEnvironment가 담기게 된다. 스코프 체이닝에 따라 outer 변수 a에 접근해서 1만큼 증가시킨 뒤 2를 반환하고, inner 함수의 실행 컨텍스트가 종료 된다.

     

     

    개발자 도구 결과

     

    여기서 의문점이 있다. inner 함수의 실행 시점에서 outer 함수는 이미 실행이 종료 된 상태인데 어떻게 outer 함수의 LexicalEnvironment에 접근할 수 있었을까?

     

    이는 가비지 컬렉터의 동작 방식 때문이다. 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다. 이를 바탕으로 정의를 다시 고쳐보면, 클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수를 외부로 전달할 경우 함수 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상을 말한다.

     

     

     

    여기서 한 가지 주의할 점이 있다. '외부로 전달'이 return만을 의미하는 것은 아니다. 콜백으로 전달하는 경우도 포함이다.

    (function () {
      var a = 0;
      var intervalId = null;
    
      var inner = function () {
        if (++a >= 10) {
          clearInterval(intervalId);
        }
        console.log(a);
      };
    
      intervalId = setInterval(inner, 1000);
    })();
    • 외부 함수: function () { ... } (즉시 실행 함수)

    • 내부 함수: inner

     

    inner 함수는 외부 함수의 지역 변수인 a와 intervalId를 참조하고 있다. 외부 함수가 즉시 실행되어 실행 컨텍스트가 사라진 이후에도 setInterval로 인해 내부 함수는 계속 실행되면서 변수들을 사용하고 있다. 

     

     

     

     

    (function () {
      var count = 0; // 외부 함수의 지역 변수
      var button = document.createElement('button');
      button.innerText = 'click';
      
      button.addEventListener('click', function () { // 내부 함수
        console.log(++count, 'times clicked');
      });
      
      document.body.appendChild(button);
    })();

     

    • 외부 함수 : function () { ... } (즉시 실행 함수)

    • 내부 함수 : button.addEventListener 안의 익명 함수

     

    즉시 실행 함수가 끝난 후에도, button은 여전히 클릭할 수 있다. 즉, 외부 함수는 끝났지만 count 변수를 사용하고 있다.

     

     

     

     

     

     

    클로저 메모리 관리

    클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리로 소모하도록 함으로써 발생한다. 그렇다면 그 필요성이 사라진 시점에는 더는 메모리를 소모하지 않게 해주면 된다. 참조 카운트를 0으로 만들면 가비지 컬렉터가 언젠가 수거하고, 소모됐던 메모리도 회수될 것이다. 참조 카운트를 0으로 만드는 방법은 식별자에 기본형 데이터(보통 null이나 undefined)를 할당하면 된다.

     

    // 메모리 해제 코드
    
    (function () {
      var count = 0; 
      var button = document.createElement('button');
      button.innerText = 'click';
    
      var clickHandler = function () {
        console.log(++count, 'tiems clicked');
        if (count >= 10) {
            button.removeEventListener('click', clickHandler);
            clickHandler = null; // clickHandler 식별자의 함수 참조를 끊음
        }
      };
      
      button.addEventListener('click', clickHandler);
      document.body.appendChild(button);
    })();

     

    클로저 변수가 불필요하게 계속 살아 있을 때 메모리 정리가 필요하다. (ex) 타이머 함수가 계속 참조 중 일 때, 클로저가 DOM 요소를 계속 참조 중 일 때)

     

     

     

     

     

    클로저 활용 사례

    // 이벤트 리스너
    
    function setupButton(name) {
        var button = document.createElement('button');
        button.innerText = name;
    
        button.addEventListener('click', function () {
            alert(name + ' 버튼 클릭!');
        });
    
        document.body.appendChild(button);
    }
    
    setupButton('save');
    setupButton('delete');

     

     

     

     

    - 접근 권한 제어 (정보 은닉)

    정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 중요한 개념 중 하나이다.

    function createPlayer(name) {
        let health = 100; // 은닉된 상태
        let score = 0;    // 은닉된 상태
    
        return {
            getStatus: function () {
                console.log(`${name} | Health: ${health} | Score: ${score}`);
            },
            takeDamage: function (damage) {
                health = Math.max(0, health - damage);
            },
            gainScore: function (points) {
                score += points;
            }
        };
    }
    
    // 플레이어 생성
    const player1 = createPlayer('Knight');
    
    // 외부에서 직접 접근 불가
    console.log(player1.health); // undefined
    console.log(player1.score);  // undefined
    
    // 메서드를 통해서만 조작 가능
    player1.getStatus();  // Knight | Health: 100 | Score: 0
    player1.takeDamage(30);
    player1.gainScore(50);
    player1.getStatus();  // Knight | Health: 70 | Score: 50

     

     

     

     

     

     

    - 부분 적용 함수

    여러개의 인자를 받는 함수에 미리 일부분의 인자만 남겨 기억시켰다가, 나중에 남은 인자들을 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수이다. 

    function greet(greeting) {
        return function (name) {
            console.log(`${greeting}, ${name}!`);
        };
    }
    
    const sayHello = greet('Hello');
    const sayHi = greet('Hi');
    
    sayHello('Alice'); // Hello, Alice!
    sayHi('Bob');      // Hi, Bob!

     

     

     

     

    실무에서 부분 함수를 사용하기에 적합한 예로 디바운스(debounce)가 있다. 디바운스는 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것으로, 프론트엔드 성능 최적화에 큰 도움을 주는 기능 중 하나이다. scroll, wheel, mousemove, resize 등에 적용하기 좋다.

    var debounce = function (eventName, func, wait) {
      var timeoutId = null;
      return function (event) {
        var self = this; // 현재 DOM 요소 저장하기 위해 사용
        console.log(eventName, 'event 발생 테스트');
        clearTimeout(timeoutId);
        timeoutId = setTimeout(func.bind(self, event), wait);
      };
    };
    
    var moveHandler = function (e) {
      console.log('move event 구현 코드');
    };
    var wheelHandler = function (e) {
      console.log('wheel event 구현 코드');
    };
    
    document.body.addEventListener('mousemove', debounce('move', moveHandler, 500));
    document.body.addEventListener('mousewheel', debounce('wheel', wheelHandler, 700));

     

    • 부분 적용 함수 : debounce('move', moveHandler, 500)

    부분 적용 함수 debounce는 클로저로 처리 되는 timeoutId 변수를 계속 기억하고 있다. 최초 이벤트가 발생하면 timeoutId에 'wait 시간 뒤에 func를 실행 할 것'이라는 내용이 담긴다. 그런데 wait 시간이 경과하기 이전에 다시 동일한 이벤트가 발생하면 6번째 줄인 clearTimeout에 의해 앞서 저장했던 timeoutId가 초기화되고, 다시 7번째 줄에서 새로운 timeoutId을 등록한다. 

    결국 마지막 이벤트만 초기화 되지 않고 무사히 실행될 것이다. 

     

    개발자 도구에서 확인 결과

     

     

     

     

     

     

     

    - 커링 함수

    여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말한다. 부분 적용 함수와 비슷해보이지만 몇 가지 다른 점이 있다. 커링은 한 번에 하나의 인자만 전달하는 것을 원칙으로 한다. 또한 중간 과정상의 함수를 실행한 결과는 그다음 인자를 받기 위해 대기만 할 뿐, 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않는다.

     

    var curry = function (func) {
        return function (a) {
            return function (b) {
                return func(a, b)
            };
        };
    };
    
    var getMaxWith10 = curry(Math.max)(10)
    getMaxWith10(8) // 10
    getMaxWith10(25) // 25

     

     

     

     

    ES6 화살표 함수를 써서 단 한줄에 표기 할 수 있다. 

    var curry = (func) => (a) => (b) => func(a, b);

     

     

     

    프로젝트 내에서 자주 쓰이는 함수의 매개변수가 항상 비슷하고 일부만 바뀌는 경우에도 적절한 후보가 될 것이다.

    var getInformation = baseUrl => path => id => fetch(baseUrl + path + '/' + id);
    
    var imageUrl = 'http://imageAddress.com/';
    
    // 이미지 요청 준비
    var getImage = getInformation(imageUrl);
    var getEmoticon = getImage('emoticon') // http://imageAddress.com/emoticon
    
    // 실제 요청
    var emoticon1 = getEmoticon(101); // http://imageAddress.com/emoticon/101
    var emoticon2 = getEmoticon(102); // http://imageAddress.com/emoticon/102

     

    보통 REST API를 이용할 경우 baseUrl은 몇 개로 고정되지만 나머지 path와 id는 매우 많을 수 있다. 이런 상황에서 공통적인 요소는 먼저 기억시켜두는게 개발 효율성이나 가독성 측면에서 더 좋을 것이다.

     

     

     

    정리

    클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.

     

     

     

     

     

    코어 자바스크립트를 읽고 정리한 글입니다.

     

    https://product.kyobobook.co.kr/detail/S000001766397

     

    코어 자바스크립트 | 정재남 - 교보문고

    코어 자바스크립트 | 자바스크립트의 근간을 이루는 핵심 이론들을 정확하게 이해하는 것을 목표로 합니다!최근 웹 개발 진영은 빠르게 발전하고 있으며, 그 중심에는 자바스크립트가 있다고

    product.kyobobook.co.kr

     

     

     

    댓글

Designed by Tistory.