클로저에 대해서 설명해주세요 -자바스크립트-

park.js·2024년 12월 23일
1

FrontEnd Develop log

목록 보기
37/37


모던 자바스크립트 Deep Dive 서적을 참고했습니다.

사실 클로저는 자바스크립트 고유의 개념이 아니다.
클로저의 정의가 ECMAScript사양에 등장하지 않는다.

MDN(신뢰할 수 있는 개발자 문서. 공식문서는 아님) 曰: “클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.”

하... 렉시컬은 또 무엇일까? 이왕 하는 김에 쉽고 빠르게 보고 오자. ⇒ 렉시컬 빠르게 집어넣기 클릭

클로저 이해를 돕기 위한 개념부터 슥

렉시컬 스코프 개념을 이해했다면 술술 읽힐겁니다.

함수는 '태어난 곳'과 '일하는 곳'이 다를 수 있다. 그래서 함수는 '일하는 곳'과 관계없이 자신이 '태어난 곳'의 환경을 기억해야 하는데, 이때 '태어난 곳'(정의 된 위치, 환경)이 바로 상위 스코프이다. => 기억해야 한다.

다시! 상위 스코프란?: 함수의 정의가 위치하는 스코프

이를 위해 함수는 자신의 내부슬롯[[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다.

함수의 [[Environment]]가 어떻게 상위 스코프를 결정하는지 보면:

  • 상위 스코프 결정 시점
    • 함수 정의가 평가될 때(코드가 실행되기 전) 상위 스코프가 결정된다.
      • 이것이 바로 '렉시컬 스코프'.
    • [[Environment]]의 역할
      • 함수가 자신이 태어난(정의된) 환경을 기억하게 해준다.

예제 코드로 이해하기

자바스크립트 deep dive 24-04

const x = 1;

function foo() {
    const x = 10;
    bar();
}

function bar() {
    console.log(x);
}
  • bar 함수는 전역에서 정의되었으므로 전역 스코프를 기억한다.
  • 그래서 bar 함수가 어디서 호출되든 상관없이 전역의 x값인 1을 참조한다.
  • foo 안에서 호출되어도 마찬가지이다.

즉, 함수의 상위 스코프는 함수를 어디서 호출하는지가 아니라, 함수를 어디서 정의했는지에 따라 결정된다. 이것이 바로 렉시컬 스코프의 핵심이다.


본격적으로 클로저하게 클로저 보기

이 코드하나 머리에 박아놓고 클로저하면 떠올리기

   function foo() {
       const x = 1;
       const y = 2;
   
       function bar() {
           console.log(x); // bar는 foo의 x에 접근 가능
       }
       return bar;
   }
   
   const bar = foo();  // foo를 실행하고 bar 함수를 반환받음
   bar();  // bar 실행
   

생명 주기 관점에서 보기

   function outer() {
       const name = "철수";  // 원래는 outer 함수 종료시 사라져야 할 변수
   
       function inner() {
           console.log(name);  // 하지만 inner가 name을 기억함
       }
       return inner;  // inner 함수 반환
   }
   
   const savedFunc = outer();  // outer는 실행 종료됨
   savedFunc();  // 여전히 "철수" 출력 가능!
   
  • 보통의 경우: outer 함수가 끝나면 그 안의 변수들(name)은 사라져야 함
  • 클로저의 경우: inner 함수가 name을 참조하고 있어서 사라지지 않고 유지됨

여기에서 의문이 들 수도 있다! (의문 없다면 pass)

“엥? outer() 함수는 일급객체로써 변수 savedFunc에 넣었기때문에 savedFunc(); 를 실행하면 outer(); 가 실행되니까 애초에 이 결과가 당연한거 아니야?” 라고 생각이 들었다면

그렇게 오해할 수 있지만 실제로는 다르게 작동한다.

     function outer() {
         const name = "박지성";
     
         function inner() {
             console.log(name);
         }
         return inner;  // inner 함수를 '반환'
     }
     
     // 여기서 잘 보면
     const savedFunc = outer();  // (1) outer 함수가 실행되고 inner 함수를 반환
     savedFunc();  // (2) 저장된 inner 함수를 실행
     

const savedFunc = outer()에서 일어나는 일:

  1. outer 함수가 실행됨
  2. inner 함수가 생성됨
  3. inner 함수가 반환되어 savedFunc에 저장됨
  4. outer 함수의 실행은 여기서 완전히 종료됨

savedFunc()에서 일어나는 일:

  • 저장된 inner 함수가 실행됨
  • outer 함수가 다시 실행되는 것이 아님!
  • 이전에 저장된 inner 함수가 실행되는 것

다시 보자면

 // 이렇게 생각이 들 수 있는데
 const savedFunc = outer;  // outer 함수 자체를 저장
 savedFunc();  // outer 함수를 실행
 
 // 실제로는 이렇게 동작함
 const savedFunc = outer();  // outer()의 반환값(inner 함수)을 저장
 savedFunc();  // 저장된 inner 함수를 실행
 

비유하자면

  • outer()는 "조리법"을 주는 게 아니라
  • "이미 완성된 요리(inner 함수)"를 주는 것이다.
  • 그래서 나중에 savedFunc를 실행할 때는 처음부터 다시 요리하는 게 아니라
  • 이미 만들어진 요리(inner)를 사용한다.

"일찍 소멸된다"는 의미

 function foo() {
     const x = 1;
     const y = 2;
 
     // 케이스 1: 클로저가 모든 변수 유지
     function bar() {
         console.log(x, y);  // x와 y 모두 필요
     }
 
     // 케이스 2: 최적화된 클로저
     function optimizedBar() {
         console.log(x);  // x만 필요
         // y는 사용하지 않아서 메모리에서 일찍 제거됨
     }
 }
 

이게이게 자바스크립트 엔진의 최적화를 도와준다.

  • optimizedBar는 x만 사용하므로 y는 메모리에서 제거
    • 불필요한 메모리 점유를 줄임

클로저 때문에 가능한 것들(많이 쓰이는 방법)

function counter() {
    let count = 0;  // 외부에서 직접 접근 불가능한 변수

    return {
        increase() { count++; },
        decrease() { count--; },
        getCount() { return count; }
    };
}

const myCounter = counter();
myCounter.increase();  // count는 private하게 보호됨
console.log(myCounter.getCount());  // 1

메모리 관점에서의 이해

function bigFunction() {
    const bigData = new Array(10000);  // 큰 데이터
    const smallData = "hello";         // 작은 데이터

    return function() {
        console.log(smallData);  // smallData만 사용
        // bigData는 사용하지 않으므로 메모리에서 제거됨
    };
}
  • 최적화 전: bigData와 smallData 모두 메모리 유지
  • 최적화 후: smallData만 메모리에 유지, bigData는 일찍 소멸
  • 이렇게 클로저는 함수가 자신이 생성된 환경의 변수를 기억하되, 실제로 사용하는 변수만을 메모리에 유지하는 최적화된 방식으로 동작한다.
    • 메모리 효율성 & 코드의 캡슐화를 동시 달성!

결론

그래서 자바스크립트에서 클로저가 뭐냐고 물어보면 뭐라고 답해야하지..?!

  • 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상.
  • 내부함수가 외부함수의 생명 주기가 종료된 후에도 외부함수의 변수를 참조(이게 핵심)할 수 있는데, 이때 해당 변수들 중 내부함수가 실제로 사용하는 변수만을 선택적으로 기억하여 메모리 효율을 높임.

아니 그럼 렉시컬 스코프와 클로저가 뭐가 다른거지?

렉시껄 스코프는 "함수를 어디에 선언했는지"에 따라 상위 스코프를 결정하는 규칙이다.

 const x = 1;
 
 function foo() {
     const x = 10;
     bar();
 }
 function bar() {
     console.log(x); // 1
 }
 foo();
 
  • bar가 전역에서 선언되었으므로 전역 스코프의 x를 참조

클로저는 이 렉시컬 스코프의 규칙을 기반으로, "이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 현상"이다.

 function foo() {
     const x = 1;
     return function bar() {
         console.log(x); // 1
     }
 }
 
 const savedBar = foo(); // foo는 실행 종료
 savedBar(); // 하지만 여전히 x에 접근 가능
 
  • foo 함수는 종료됐지만 반환된 bar가 x를 여전히 참조 가능

즉:

  • 렉시컬 스코프: 상위 스코프를 결정하는 규칙
  • 클로저: 이 규칙을 활용해 이미 종료된 함수의 변수를 참조하는 현상
  • 클로저는 렉시컬 스코프라는 규칙이 있기에 가능한 현상이라고 볼 수 있다.
profile
참 되게 살자

0개의 댓글