ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Notes from reading 🔖 You Don't Know JS Yet - 13
    JavaScript 2026. 5. 25. 20:59

    ### 파트2 - Chapter 7 클로저 사용법 / 클로저 생명주기 & 가비지 컬랙션

     

    • 마지막 함수 참조가 삭제되면, 변수에 대한 클로저가 삭제되고, 변수는 GC 처리됨
    • 클로저는 변수의 GC를 예기치 않게 막아 메모리 사용량을 급증시킬 위험성이 있다 !!
      • 필요하지 않은 함수 참조는 적절하게 제거해줘야 한다. (복잡한 내용. 예제를 잘 봐야 할듯)
    • 클로저는 변수를 기준으로 작동하지 스코프를 기준으로 작동하지 않는다.

     

    • 현대의 많은 JS 엔진들은 명시적으로 참조되지 않는 변수를 클로저 스코프에서 제거하는 최적화 방식을 채택함
    • 다만, eval등을 사용했을 때 등 특정 상황에서는 해당 최적화가 진행될 수 X => 따라서 모든 변수가 클로저 스코프에 남아있을 수 있다.
    • >>> 클로저 구현은 스코프 단위 O 함수가 정의된 스코프를 포함, 외부 스코프의 모든 변수에 접근할 수 있다.
    • 현대 JS 엔진이 클로저를 최적화해서 실제 함수 내에서 참조되는 변수만 클로저에 포함시켰을 뿐. (필요하지 않은 변수를 제거해 메모리 최적화, 클로저를 포함하는 스코프의 크기를 최적화)
    • 다만, 해당 최적화가 모든 환경에서 적용될거라고 생각하면 X 수동으로 값을 버리는 편이 안전. 클로저 최적화나 GC에 의존하는 것은 좋지 않다.
    • =>> 클로저가 어디에 있고 어떤 변수를 포함하는지 파악하는 것은 중요.

     

    • JS에서 함수는 다른 값들 처럼 전달될 수 있는 일급 객체 => 클로저는 함수가 어디로 이동하던 해당 함수를 외부 스코프에 있는 변수와 연결해주는 연결고리
    • 클로저는 함수가 정의된 원래 스코프에 대한 링크를 제공 => 클로저는 외부 스코프의 변수에 접근 O
    • JS는 함수에서 비원시값과 마찬가리조 참조에 의해 저장되고 참조 복사를 통해 할당 전달 되는 사실을 알자
      • 해당 관점에 집중해서 생각하면, 함수 인스턴스에 대한 참조가 존재하면, 함수 인스턴스와 그 전체 스코프 환경 & 스코프 체인을 유지하게 함 => 클로저

     

    • 클로저를 잘 사용하면, 프로그램 내에서 좀 더 적절하게 변수의 노출 범위를 조절할 수 있다. (전역으로 사용하는 대신)
    • 클로저를 사용한 대표적인 함수형 프로그래밍 패러다임의 기법 => 부분 적용 & 커링
    • 여러 입력이 필요한 함수의 모양을 바꿔 미리 혹은 나중에 입력하는 기법 (초기 입력을 클로저가 기억) => 내부 정보를 캡슐화 & 의미있는 네이밍 가능
    • 일부 함수를 분리하여 로직을 공통화 할 수도 있음 (함수 정의의 재사용 => 전문화된 함수 생성)

     

    /**
     * Chapter 7 - 클로저 사용법 / 클로저 생명주기 & 가비지 컬렉션
     *
     * 실행 방법:
     * node closure-lifecycle.js
     *
     * 각 예제를 순서대로 실행하며 클로저의 특징을 보여준다.
     */
    
    console.log("===== Example 1. 클로저 기본 개념 =====");
    
    /**
     * 클로저는 함수가 선언된 당시의 외부 변수에 접근 가능하게 한다.
     */
    function createCounter() {
      let count = 0;
    
      return function increase() {
        count++;
        console.log("count:", count);
      };
    }
    
    const counter = createCounter();
    
    counter(); // 1
    counter(); // 2
    counter(); // 3
    
    console.log("\n===== Example 2. 함수 참조가 남아있으면 클로저도 살아있다 =====");
    
    /**
     * inner 함수가 outer 함수의 변수(message)를 계속 참조한다.
     * 함수 참조가 살아있는 동안 message도 GC 대상이 되지 않는다.
     */
    function outer() {
      const message = "나는 클로저에 의해 유지된다";
    
      return function inner() {
        console.log(message);
      };
    }
    
    let savedFunction = outer();
    
    /**
     * outer 함수 실행은 끝났지만,
     * savedFunction이 살아있기 때문에 message도 유지된다.
     */
    savedFunction();
    
    /**
     * 마지막 함수 참조 제거
     * 이제 inner 함수와 관련 스코프는 GC 대상이 될 수 있다.
     */
    savedFunction = null;
    
    console.log("savedFunction 참조 제거 완료");
    
    console.log("\n===== Example 3. 클로저로 인해 메모리가 유지될 수 있음 =====");
    
    /**
     * 큰 데이터를 클로저가 참조하면 메모리 사용량이 유지될 수 있다.
     */
    function createHeavyClosure() {
      const hugeArray = new Array(1_000_000).fill("🔥");
    
      return function getFirstItem() {
        return hugeArray[0];
      };
    }
    
    let heavyFn = createHeavyClosure();
    
    console.log("클로저가 hugeArray를 유지중:", heavyFn());
    
    /**
     * 함수 참조 제거
     * hugeArray도 GC 대상이 될 가능성이 생김
     */
    heavyFn = null;
    
    console.log("heavyFn 제거 완료");
    
    console.log("\n===== Example 4. 클로저는 '변수'를 기억한다 =====");
    
    /**
     * 클로저는 값(snapshot)이 아니라 변수 자체를 참조한다.
     */
    function createUpdater() {
      let value = 1;
    
      return {
        increase() {
          value++;
        },
    
        print() {
          console.log("value:", value);
        },
      };
    }
    
    const updater = createUpdater();
    
    updater.print(); // 1
    updater.increase();
    updater.print(); // 2
    updater.increase();
    updater.print(); // 3
    
    console.log("\n===== Example 5. 잘못된 클로저 사용 예시 (var) =====");
    
    /**
     * var는 함수 스코프이므로
     * 모든 함수가 같은 i 변수를 참조한다.
     */
    function wrongLoopExample() {
      const functions = [];
    
      for (var i = 0; i < 3; i++) {
        functions.push(function () {
          console.log("wrong i:", i);
        });
      }
    
      return functions;
    }
    
    const wrongFns = wrongLoopExample();
    
    wrongFns[0](); // 3
    wrongFns[1](); // 3
    wrongFns[2](); // 3
    
    console.log("\n===== Example 6. 올바른 클로저 사용 예시 (let) =====");
    
    /**
     * let은 블록마다 새로운 변수 생성
     */
    function correctLoopExample() {
      const functions = [];
    
      for (let i = 0; i < 3; i++) {
        functions.push(function () {
          console.log("correct i:", i);
        });
      }
    
      return functions;
    }
    
    const correctFns = correctLoopExample();
    
    correctFns[0](); // 0
    correctFns[1](); // 1
    correctFns[2](); // 2
    
    console.log("\n===== Example 7. 부분 적용 (Partial Application) =====");
    
    /**
     * 일부 인자를 먼저 고정
     * 나머지 인자를 나중에 받는다.
     */
    function multiply(a, b) {
      return a * b;
    }
    
    function partialMultiply(a) {
      return function (b) {
        return multiply(a, b);
      };
    }
    
    const double = partialMultiply(2);
    const triple = partialMultiply(3);
    
    console.log("double(10):", double(10)); // 20
    console.log("triple(10):", triple(10)); // 30
    
    console.log("\n===== Example 8. 커링 (Currying) =====");
    
    /**
     * 여러 인자를 각각 나눠서 받는 방식
     */
    function curryAdd(a) {
      return function (b) {
        return function (c) {
          return a + b + c;
        };
      };
    }
    
    console.log("curryAdd(1)(2)(3):", curryAdd(1)(2)(3));
    
    console.log("\n===== Example 9. 캡슐화 =====");
    
    /**
     * 외부에서 직접 접근 불가능한 private 변수
     */
    function createBankAccount(initialBalance) {
      let balance = initialBalance;
    
      return {
        deposit(amount) {
          balance += amount;
          console.log(`${amount}원 입금`);
        },
    
        withdraw(amount) {
          balance -= amount;
          console.log(`${amount}원 출금`);
        },
    
        getBalance() {
          return balance;
        },
      };
    }
    
    const account = createBankAccount(1000);
    
    account.deposit(500);
    account.withdraw(200);
    
    console.log("현재 잔액:", account.getBalance());
    
    /**
     * 직접 접근 불가능
     */
    console.log("account.balance:", account.balance); // undefined
    
    console.log("\n===== Example 10. 불필요한 참조 제거 =====");
    
    /**
     * 이벤트 핸들러, 캐시, 타이머 등에서
     * 클로저 참조를 오래 유지하면 메모리 누수 위험 존재
     */
    
    function setupListener() {
      const largeData = new Array(500_000).fill("DATA");
    
      function listener() {
        console.log("listener 실행:", largeData[0]);
      }
    
      return listener;
    }
    
    let eventListener = setupListener();
    
    /**
     * 어딘가에 등록되었다고 가정
     */
    eventListener();
    
    /**
     * 사용 종료 후 참조 제거
     */
    eventListener = null;
    
    console.log("eventListener 제거 완료");
    
    console.log("\n===== 모든 예제 종료 =====");
Designed by Tistory.