ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JavaScript 의 Closure (클로저) 리뷰하기
    JavaScript 2023. 3. 5. 12:55

    드디어 자바스크립트를 처음 배울 때, 그리고 첫 프로젝트를 진행할 때 나를 가장 당황시켰던 클로저에 대해 리뷰할 때가 왔다.

    딥다이브 소개 첫 마디부터 난해하기로 유명한 자바스크립트의 개념이라고 소개하는 클로저 ..

    근데 실행 컨텍스트를 파악하고 나니 왠지 그렇게 어려울 것 같지 않은 자신감이 생긴다 !

    과연 공부를 하면서 자신감이 무너질 지, 아니면 지켜질 지는 두고 봐야 알겠지 ..

     


    출처 : 모던 자바스크립트 Deep Dive: 자바스크립트의 기본 개념과 동작 원리 / 이웅모 지음

     

    클로저

     

    클로저는 사실 자바스크립트 고유 개념은 아니고, 함수형 프로그래밍 언어들에서 사용되는 중요한 특성이다.

    즉, 클로저와 함수형 프로그래밍이 깊게 연관되어 있으니, 프론트엔드의 트렌드인 함수형 프로그래밍을 할 때 클로저의 이해도가 중요하다는 것 !

    실제로도 트랜센던스 프로젝트를 하면서 클로저를 마주할 일이 굉장히 많았다.

     

    일단 너무 난해해섯 무슨 소린지 잘 모르겠지만, 예의상 정의를 한번 짚고 넘어가자.

    A closure is the combination of a function and the lexical environment within which that function was declared.
    클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.

    잘 와닿지도 않고, 무슨 의미인지 뜯어보려고 하면 괜시리 겁이 날 정도다. 그러니까 그냥 그런갑다 하고 넘어가자.

     

    내가 처음 자바스크립트 코드를 짜면서 당황했던 부분이 바로 이제 나온다 . 짜잔 !

    const x = 1;
    
    function outerFunc() {
    	const x = 10;
    	
    	function innerFunc() {
    		console.log(x); // 10
    		// 아니 말도 안돼 ! 외부 함수의 x 에 어떻게 접근이 되는거야 ??
    	}
    	innerFunc();
    }
    
    outerFunc();

    렉시컬 스코프는 이미 잘 알고 자바스크립트 기본이니까 설명은 패스. (대충 함수는 호출 위치랑 관계없이 정의된 위치에 따라 상위 스코프가 결정된다는 개념)

     

    위의 코드는 여태까지 내가 공부한 C++ 사양에서는 있을 수 없는 일이다. 하지만 실행 컨텍스트를 공부하고 나니까 이제 저런게 왜 되는지 이해가 된다 !

    바로, innerFunc() 함수 객체의 내부 슬롯 [[Environment]] 에 상위 스코프의 참조가 저장되어 있다.

    따라서, outerFunc() 코드가 실행되는 시점에 innerFunc() 코드가 평가되어, 현재 실행 중인 outerFunc() 실행 컨텍스트의 렉시컬 환경을 참조해 내부 슬롯 [[Environment]] 에 저장해서, 자신이 존재하는 동안 계속 기억하고 있는 것 !

     

    야호 ! 여기까지 이해가 되었다면, 이를 이용해 더 해괴한 짓도 가능하다.

    const x = 1;
    
    function outer() {
    	const x = 10;
    	const inner = () => { console.log(x); }; // 화살표 함수 모르는 사람 없죠 ?
    	return inner;
    }
    
    // outer 함수를 호출하면 중첩 함수 inner 를 반환한다.
    // 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
    const innerFunc = outer();
    innerFunc(); // 10

    outer 함수를 호출하면, outer 함수는 중첩 함수 inner 를 반환하고 life cycle 을 마감한다. (실행 컨텍스트 스택에서 pop)

    그러면 당연히 outer 안에서 선언한 변수 x 또한 유효하지 않게 된다.

    근데 innerFunc() 의 호출 결과는 10이다 !

    outer 함수의 지역 변수 x 가 예수님이라서 3라인 아래에 부활한 것이 아니다.

    위의 코드를 보면, 외부 함수보다 중첩 함수가 더 오래 유지되고 있다. 이 경우, 중첩 함수는 이미 라이프 사이클이 끝난 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수가 바로 클로저 (closure) 인 것이다 !

     

    이제 클로저의 의미가 와닿기 시작한다. 클로저는 함수(inner) 와, 그 함수가 선언된 렉시컬 환경(outer 실행 컨텍스트의 렉시컬 환경) 과의 조합이다 !

     

    즉, inner함수 객체의 [[Environment]] 슬롯이 outer 함수의 렉시컬 환경을 저장하고 있는 한, outer 함수의 실행 컨텍스트는 스택에서 pop 될지언정, 렉시컬 환경은 소멸되지 않고 남아 있는 것이다. (Javascript 의 Garbage Collector 는 누군가가 참조하고 있는 메모리 공간을 함부로 해제하지 않는다.)

     

    이처럼 자바스크립트의 모든 함수는 [[Environment]] 슬롯에 외부 스코프의 렉시컬 환경을 참조하므로, 이론적으로 모든 함수는 클로저라고 부를 가능성이 있다. 하지만, 실제로 상위 스코프의 식별자를 참조하지 않는 경우에는 모던 브라우저는 최적화를 통해 상위 스코프를 기억하지 않으므로 그 경우엔 클로저가 아니다. 따라서 중첩 함수라고 전부 클로저라고 부르는 것은 아니고, 위의 예시처럼 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 외부 함수보다 더 오래 유지되는 2가지 조건을 만족해야 클로저라고 한다.

     


    그래서 클로저를 언제 써 ?

     

    클로저를 잘 쓰면 state 를 안전하게 은닉하고, 특정 함수에게만 상태 변경을 허용하는 음흉한 짓을 할 수 있다.

    const increase = (() => {
    	let num = 0;
    	
    	return () => ++num;
    })();
    
    console.log(increase()); // 1
    console.log(increase()); // 2
    console.log(increase()); // 3

    위 코드가 실행되면 즉시 실행 함수가 호출되고, 즉시 실행 함수가 반환한 함수 (() => ++num;) 가 increase 변수에 할당된다. 즉시 실행 함수는 호출되고 소멸되었지만, 내부 함수는 클로저로써 increase 변수에 할당되어 호출되고, increase 는 당연히 num 을 기억하고 있으니, increase 클로저는 num 을 조작할 수 있다.

    근데 num 이 선언된 렉시컬 환경을 기억하고 있는 건 increase 뿐이므로, num 은 외부에서는 접근할 수 없고 increase 로만 조작할 수 있는 은닉된 변수가 되는 것 !

     

    자바스크립트는 C++ 의 private 과 같은 접근 제한자를 제공하지 않기 때문에, 이런 방식의 캡슐화와 정보 은닉이 대신 유용하게 쓰일 수 있다.

     

     

    정말 클로저를 이해하는 데 성공했다 ! 하지만 여전히 잘 쓰기는 쉽지 않아 보인다 ..

    댓글

Designed by Tistory.