[JS] 클로저(closure)란?

jiny·2025년 2월 7일

기술 면접

목록 보기
52/78

🗣️ 클로저에 대해 설명해주세요.

  • 의도: 어려운 개념인 클로저에 대한 지식을 가지고 있는지 확인하는 질문

  • 팁: 굳이 어려운 단어로 설명하지 않아도 됩니다.

  • 나의 답안

    클로저함수가 선언될 때의 환경을 기억해서, 외부 함수의 변수에 접근할 수 있는 함수를 말합니다.
    자바스크립트에서는 함수 안에서 또 다른 함수를 정의할 수 있는데, 이때 내부 함수가 외부 함수의 변수나 매개변수를 계속 참조할 수 있는 구조가 만들어집니다.

    외부 함수의 실행이 끝나도, 그 안에서 정의된 내부 함수가 그때의 변수 상태를 기억하고 유지하는 것이 바로 클로저의 특징입니다.

    예를 들어, 카운터처럼 어떤 값을 계속 증가시키거나 특정 데이터를 은닉해야 할 때 클로저를 이용하면, 외부에서 직접 접근은 막고 내부 함수로만 제어할 수 있게 할 수 있습니다.

    이런 구조 덕분에 클로저는 상태 유지, 정보 은닉, 모듈화 같은 기능을 구현할 때 자주 사용됩니다.
    다만, 변수를 계속 참조하고 있기 때문에 메모리 해제가 늦어질 수 있다는 점은 주의해야 합니다.

  • 주어진 답안 (모범 답안)

    클로저는 자바스크립트의 중요한 개념으로, 함수와 함수가 선언된 렉시컬 환경(Lexical Environment)의 조합을 의미합니다.
    즉, 클로저는 함수가 외부 함수의 변수에 접근할 수 있는 능력을 말합니다.

    자바스크립트는 렉시컬 스코프(Lexical Scope)를 따르기 때문에, 함수가 선언될 당시의 스코프를 기억합니다.
    이로 인해 외부 함수의 실행이 종료된 이후에도 내부 함수가 외부 변수에 접근하거나 조작할 수 있습니다.

    이러한 특징을 활용하여 데이터 은닉화 및 캡슐화를 구현할 수 있습니다.
    다만 남용할 경우 메모리의 누수를 피할 수 없다는 단점도 있습니다.


📝 개념 정리

자바스크립트의 클로저(Closure)는 함수와 해당 함수가 선언된 렉시컬 환경(Lexical Environment)의 조합이다.
클로저는 함수가 외부 함수의 변수에 접근할 수 있도록 하며, 외부 함수의 실행 컨텍스트가 종료된 후에도 변수를 참조하거나 조작할 수 있게 만든다.
이를 통해 상태를 유지하거나 캡슐화를 구현할 수 있다.

🌟 클로저의 구성 요소

  1. 외부 함수 (Outer Function)
    내부 함수가 참조할 수 있는 변수를 정의한다.

  2. 내부 함수 (Inner Function)
    외부 함수의 변수에 접근하거나 값을 변경할 수 있다.

  3. 렉시컬 환경 (Lexical Environment)
    내부 함수가 정의된 위치에 따라 외부 변수에 대한 접근 권한이 결정된다.


🌟 클로저의 동작 방식

자바스크립트는 렉시컬 스코프(Lexical Scope)를 따른다.
즉, 함수가 어디에서 실행되는지가 아니라, 어디에서 선언되었는지에 따라 스코프가 결정된다.
따라서, 내부 함수는 자신이 선언된 위치에서 외부 스코프의 변수들을 기억하고 참조할 수 있다.


🌟 클로저 예제

  1. 기본적인 클로저 예제

    function outerFunction() {
      let count = 0; // 외부 함수의 변수
      
      function innerFunction() {
        count++; // 외부 변수에 접근하여 변경
        console.log(`Count: ${count}`);
      }
      
      return innerFunction; // 내부 함수를 반환
    }
    
    const closureExample = outerFunction(); // outerFunction 실행
    closureExample(); // Count: 1
    closureExample(); // Count: 2
    closureExample(); // Count: 3
    • innerFunctioncount라는 외부 함수의 변수를 참조하고 있다.

    • outerFunction이 실행된 후에도 내부 함수 innerFunctioncount를 기억하고 사용할 수 있다.

      outerFunction 실행이 끝난 후에도 innerFunctioncount를 기억하고 사용할 수 있을까?

      1. 자바스크립트의 실행 컨텍스트와 렉시컬 환경
        • 자바스크립트에서는 함수가 호출될 때, 실행 컨텍스트(Execution Context)가 생성된다.
        • 실행 컨텍스트는 함수 내부의 변수와 선언된 스코프를 관리하는데, 이때 렉시컬 환경(Lexical Environment)이라는 데이터 구조가 사용된다.
        • outerFunction이 호출되면
          💖 outerFunction의 실행 컨텍스트가 생성되고, count라는 변수가 렉시컬 환경에 저장된다.
          💖 innerFunctionouterFunction 내부에 정의되었기 때문에, innerFunction의 렉시컬 환경에는 outerFunction의 렉시컬 환경이 참조된다.
      2. 함수와 렉시컬 환경의 연결
        • 자바스크립트에서는 함수가 선언될 때, 그 함수는 자신이 선언된 렉시컬 환경을 기억한다. 이를 통해 함수가 실행될 때 외부 스코프의 변수에 접근할 수 있다.
        • innerFunctionouterFunction 내부에서 선언되었기 때문에, outerFunction의 렉시컬 환경을 클로저 형태로 캡처한다.
        • 결과적으로, innerFunction이 반환된 이후에도 count 변수가 렉시컬 환경이 계속 유지된다.
      3. outerFunction의 실행 컨텍스트 종료 후에도 렉시컬 환경이 유지되는 이유
        • innerFunctionouterFunction의 반환값으로 외부에 전달된다.
        • 반환된 함수(innerFunction)는 여전히 count 변수에 접근해야 하기 때문에, 자바스크립트 엔진은 outerFunction의 렉시컬 환경을 GC(Gargabe Collection)로 제거하지 않는다.
        • 따라서, innerFunction이 호출될 때마다 outerFunction의 렉시컬 환경에 저장된 count에 접근하고, 그 값을 수정할 수 있다.

      한 단계씩 동작 분석

      1. const closureExample = outerFunction();
        • outerFunction이 호출되면서 count = 0이 생성된다.
        • innerFunctioncount에 접근할 수 있는 클로저를 생성한다.
        • outerFunction은 종료되지만, innerFunction이 반환되면서 count를 포함하는 렉시컬 환경은 유지된다.
      2. closureExample()
        • innerFunction이 호출되면, outerFunction의 렉시컬 환경에 저장된 count에 접근한다.
        • count++로 값을 증가시키고, console.log를 통해 출력한다.
      3. 이후 closureExample()을 반복 호출할 때마다 동일한 렉시컬 환경에 저장된 count 값을 수정하고 출력한다.

      중요한 개념 요약

      1. 렉시컬 스코프
        함수는 선언된 위치에서의 스코프를 기억한다.
      2. 렉시컬 환경 캡처
        클로저는 외부 함수의 변수와 렉시컬 환경을 기억하고 유지한다.
      3. GC 방지
        반환된 함수가 외부 변수를 참조하고 있으면, 자바스크립트 엔진은 렉시컬 환경을 메모리에서 제거하지 않는다.

      비유로 이해하기
      outerFunction을 하나의 방이라고 생각하고, count는 그 방 안에 있는 물건이라고 가정해보자.
      innerFunction은 이 방의 열쇠를 가진 사람이다.

      • outerFunction이 끝나더라도, 열쇠를 가진 innerFunction이 있으면 방은 여전히 열 수 있고, 방 안의 물건(count)에 접근할 수 있다.
      • 열쇠(innerFunction)가 사라지지 않는 한 방은 지워지지 않는다.
  2. 클로저로 상태 캡슐화
    클로저는 변수를 외부에서 직접 접근하지 못하도록 보호하면서, 필요한 작업을 수행할 수 있는 방법을 제공한다.

    function createCounter() {
      let count = 0; // private 변수
      
      return {
        increment() {
          count++;
          return count;
        },
        decrement() {
          count--;
          return count;
        },
        getCount() {
          return count;
        }
      };
    }
    
    const counter = createCounter();
    console.log(counter.increment()); // 1
    console.log(counter.increment()); // 2
    console.log(counter.decrement()); // 1
    console.log(counter.getCount()); // 1
    • countcreateCounter 내부에 선언된 변수로 외부에서 직접 접근할 수 없다.

    • increment, decrement, getCount 메서드는 클로저를 통해 count에 접근하고 조작할 수 있다.

      코드 분석

      1. createCounter 함수
        • createCountercount라는 변수를 포함한 객체를 반환하는 팩토리 함수이다.
        • 이 함수 내부의 count 변수는 createCounter 함수가 실행될 때 생성된다.
        • 반환된 객체의 메서드(increment, decrement, getCount)는 모두 count에 접근할 수 있다.
        • 하지만 count 자체는 함수 외부에서 직접 접근할 수 없으므로, 안전하게 보호된다.
      2. count 변수
        • countcreateCounter 내부에 선언된 지역 변수(local variable)로, 외부에서 접근할 수 없다.
        • 그러나, 반환된 객체의 메서드들이 count를 참조하고 있기 때문에, 클로저를 통해 count를 유지하고 조작할 수 있다.
      3. 반환된 객체
        • 반환된 객체는 세 개의 메서드를 포함하고 있다.
          1) increment: count 값을 1 증가시키고 반환
          2) decrement: count 값을 1 감소시키고 반환
          3) getCount: 현재 count 값을 반환
        • 이 객체를 사용하면, count 값을 안전하게 읽거나 수정할 수 있지만, 직접적으로 변경하는 것은 불가능하다.

      코드 동작 과정

      1. const counter = createCounter();
        • createCounter 함수가 호출되면서 count 변수가 생성되고 초기값 0으로 설정된다.
        • count 변수는 createCounter 함수가 종료된 이후에도 반환된 객체의 메서드가 이를 참조하고 있으므로, 메모리에 유지된다.
        • 반환된 객체에는 increment, decrement, getCount 메서드가 포함되어 있다.
      2. counter.increment();
        • increment 메서드가 호출되면서 count++이 실행되고 count 값이 1로 증가한다.
        • 변경된 값을 반환한다.
      3. counter.increment();
        • 다시 increment가 호출되면 count가 1 증가하여 값은 2가 된다.
      4. counter.decrement();
        • decrement 메서드가 호출되면서 count--이 실행되고 count 값이 1로 감소한다.
      5. counter.getCount();
        • getCount 메서드는 현재 count 값을 반환한다. 현재 값은 1이다.

      클로저의 역할
      이 코드에서 클로저는 count 변수를 외부로부터 보호하고, 반환된 객체의 메서드들만이 count를 읽거나 수정할 수 있도록 한다.

      1. increment, decrement, getCount는 모두 클로저이다.
        • 이 메서드들은 createCounter 실행 시 생성된 렉시컬 환경을 기억한다.
        • 따라서 함수 외부에서 count를 직접 수정하거나 읽는 것은 불가능하지만, 제공된 메서드를 통해서만 count를 안전하게 조작할 수 있다.
      2. 클로저 덕분에 캡슐화(encapsulation)가 이루어진다.
        • 외부에서 count를 직접 수정하거나 오용할 가능성을 차단한다.
        • 예를 들어, counter.count = 100;과 같은 직접적인 접근은 불가능하다.
  1. 클로저와 반복문 문제
    클로저를 올바르게 사용하지 않으면 의도치 않은 동작이 발생할 수 있다.

    • 문제 예시

      const functions = [];
      for (var i = 0; i < 3; i++) {
        functions.push(function () {
          console.log(i); // i는 반복문이 끝난 후의 값에 접근
        });
      }
      
      functions[0](); // 3
      functions[1](); // 3
      functions[2](); // 3

      문제 상황 분석

      1. var의 함수 스코프 특성
        • 반복문에서 i를 선언할 때, var는 블록 스코프를 가지지 않는다. 대신 함수 스코프를 가진다.
        • 따라서, 반복문이 실행될 때 i는 매 반복마다 새로 선언되지 않고 같은 변수를 참조한다.
        • 반복문이 종료된 후, i의 최종 값은 3이다.
      2. 클로저로 인해 i가 참조됨
        • 반복문 내부의 functions.push는 익명 함수를 배열에 저장하고 있다.
        • 이 익명 함수는 클로저이다. 즉, 함수 내부에서 사용된 i는 반복문이 실행될 당시의 값을 복사하는 것이 아니라, 변수 i 자체를 참조한다.
        • 반복문이 종료되면 i의 값은 3으로 최종적으로 업데이트되므로, 배열에 저장된 모든 함수는 같은 i(최종 값인 3)을 참조하게 된다.
      3. 문제가 발생하는 원리
        • functions[0](), functions[1](), functions[2]()를 호출하면 각각의 함수는 동일한 i를 참조한다.
        • i는 반복문이 종료된 이후의 값인 3이므로, 모든 함수 호출 결과가 3이 출력된다.

      문제가 발생한 이유

      1. var가 블록 스코프를 가지지 않음
        • 반복문 내에서 i가 블록마다 고유한 스코프를 가지지 않고, 반복문 전체에 대해 하나의 스코프를 공유한다.
      2. 클로저가 변수의 값이 아닌 참조를 기억
        • 반복문이 실행될 때마다 함수가 생성되지만, 이 함수들은 i라는 변수 자체를 기억한다.
        • 따라서 반복문이 종료된 후에도 최종 i 값(즉, 3)만 참조한다.
    • 해결 방법 1: 즉시 실행 함수(IIFE: Immediately Invoked Function Expression) 사용

      const functions = [];
      for (var i = 0; i < 3; i++) {
        (function (j) { // IIFE를 선언하고 즉시 실행
          functions.push(function () {
            console.log(j); // 현재 반복의 j 값을 캡처(기억)
          });
        })(i); // 현재 i 값을 즉시 실행 함수에 인자로 전달
      }
      
      functions[0](); // 0
      functions[1](); // 1
      functions[2](); // 2
      • ES6 이전에는 즉시 실행 함수를 활용해 각 반복마다 새로운 스코프를 생성했다.

      • 이 방법은 현재 반복문의 i 값을 클로저로 캡처한다.

        IIFE(즉시 실행 함수)란?
        즉시 실행 함수는 정의되지마자 즉시 실행되는 함수를 의미한다. 형식은 다음과 같다.

        (function () {
          // 코드 실행
        })();

        또는

        (() => {
          // 코드 실행
        })();

        IIFE는 한 번만 실행되며, 내부의 변수는 외부에서 접근할 수 없는 독립적인 스코프를 갖는다.

        실행 과정

        • i = 0일 때
          1. (function (j) { ... })(0);이 실행된다.
          2. 즉시 실행 함수가 실행되며 j = 0이 전달된다.
          3. 내부에서 functions.push(function () { console.log(j); });가 실행된다.
          4. 클로저가 j = 0을 기억한 채로 저장된다.
        • i = 1일 때
          1. (function (j) { ... })(1);이 실행된다.
          2. 즉시 실행 함수가 실행되며 j = 1이 전달된다.
          3. 내부에서 functions.push(function () { console.log(j); });가 실행된다.
          4. 클로저가 j = 1을 기억한 채로 저장된다.
        • i = 2일 때
          1. (function (j) { ... })(2);가 실행된다.
          2. 즉시 실행 함수가 실행되며 j = 2가 전달된다.
          3. 내부에서 functions.push(function () { console.log(j); });가 실행된다.
          4. 클로저가 j = 2를 기억한 채로 저장된다.

        실행 결과
        반복문이 종료된 후, functions 배열에는 각각 다른 값을 기억하는 함수들이 저장된다.
        각 함수는 자신이 생성될 당시의 j 값을 캡처하고 있으므로, 각각 고유한 값을 출력한다.

        핵심 원리: 변수 j의 독립적인 스코프 생성

        1. 즉시 실행 함수(IIFE) 내부에서 매개변수 j를 생성
          • 즉시 실행 함수가 호출될 때, 현재 i 값을 j에 복사한다.
          • j는 즉시 실행 함수의 지역 변수이므로, 반복마다 새로운 j가 생성된다.
        2. 클로저가 j 값을 유지
          • 내부 함수 console.log(j);j를 참조하는 클로저이다.
          • j는 즉시 실행 함수가 실행될 때 생성된 값으로 고정된다.
          • 따라서, 이후 함수가 호출될 때도 j의 값이 변하지 않는다.
    • 해결 방법 2: let 사용

      const functions = [];
      for (let i = 0; i < 3; i++) {
        functions.push(function () {
          console.log(i); // let은 블록 스코프를 가지므로 해결됨
        });
      }
      
      functions[0](); // 0
      functions[1](); // 1
      functions[2](); // 2
      • ES6의 let블록 스코프를 가지므로, 반복문 내에서 매 반복마다 새로운 i를 생성한다.

      • 이를 통해 각 함수는 해당 블록에서의 i 값을 기억한다.

        let을 사용하면 왜 해결될까?
        let블록 스코프를 가지므로, for 루프의 각 반복마다 새로운 i 변수가 생성된다.

        1. for 루프의 매 반복마다 새로운 i가 생성됨
          • i = 0일 때, 새로운 i 변수가 생성된다.
          • i = 1일 때, 새로운 i 변수가 생성된다.
          • i = 2일 때, 새로운 i 변수가 생성된다.
        2. i는 독립적인 값으로 저장됨
          • 반복이 진행될 때마다 i가 별도의 메모리 공간을 차지하게 된다.
          • functions.push()에 의해 내부 함수가 생성될 때 각각의 i 값을 캡처한다.
        3. 클로저 문제 해결
          • 각 반복마다 새로운 i가 생성되므로 클로저가 각기 다른 i 값을 유지하게 된다.
          • 결과적으로, functions[0]()을 호출하면 0, functions[1]()을 호출하면 1, functions[2]()를 출력하면 2가 출력된다.

        실행 과정

        • 첫 번째 반복 (i = 0)
          1. let i = 0이 블록 스코프에서 생성된다.
          2. functions.push(function () { console.log(i); })가 실행된다.
          3. 현재의 i = 0이 캡처된다.
        • 두 번째 반복 (i = 1)
          1. let i = 1이 블록 스코프에서 새롭게 생성된다.
          2. functions.push(function () { console.log(i); })가 실행된다.
          3. 현재의 i = 1이 캡처된다.
        • 세 번째 반복 (i = 2)
          1. let i = 2가 블록 스코프에서 새롭게 생성된다.
          2. functions.push(function () { console.log(i); })가 실행된다.
          3. 현재의 i = 2가 캡처된다.

        let을 사용한 해결 방법의 장점

        • 즉시 실행 함수(IIFE) 없이 간결하게 해결이 가능하다.
        • 블록 스코프를 활용하여 메모리 관리가 쉽다.
        • 클로저의 개념을 깊이 이해하지 않아도 직관적으로 동작한다.

🌟 클로저의 활용

  1. 상태 유지
    예: setTimeout의 지연 작업이나 비동기 요청에서 상태를 유지한다.

  2. 데이터 캡슐화
    예: private 변수를 유지하여 외부에서 직접 접근을 방지한다.

  3. 커링(Currying)
    함수를 반환하여 특정 파라미터를 고정한 새로운 함수를 생성한다.

  4. 함수형 프로그래밍
    고차 함수(High-Order Function)에서 동작을 확장하거나 데이터를 전달한다.


🌟 주의사항

  1. 메모리 누수
    클로저는 참조를 유지하므로, 불필요한 클로저를 생성하거나 장시간 유지하면 메모리 누수가 발생할 수 있다.

  2. 성능
    클로저는 변수를 계속 참조하기 때문에, 불필요한 클로저를 생성하면 성능에 영향을 줄 수 있다.

0개의 댓글