본문 바로가기

Javascript

[Javascript] Closure (클로져)

What is Closure?

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function - MDN

 

클로져란 개념이 일급함수로 인해 파생되기 때문에, 자바스크립트를 처음 접하는 개발자들 중 상당수가 이 개념을 낯설어 합니다. 사실 자연스럽게 사용하고 있지만, 인지 못하는걸 수 있습니다. MDN의 정의에 따르면, 클로져가 내부함수에서 외부함수로의 접근을 허용하게 한다 합니다.

음? 저는 가장 먼저 떠오르는게, 전역 변수네요.

const globalVariable = 3;
function outerFunc(msg){
	const varInsideOuterScope = 1;
	function innerFunc(msg2){
    	console.log(varInsideOuterScope , globalVaribale); // 1 3
    }
}

단, lexical environment(문맥 환경)을 기준으로 클로져가 생성된다고 합니다. 이게 대체 뭘까요?

 

 

Lexical Environment

문맥 환경이란, 말그대로, 물리적으로 코드의 위치, 즉 전적으로 개발자에 의해 결정되는 환경입니다.

이렇게 말하면 잘 이해가 안가니, 코드를 직접 보면서 이야기하자면,

const tmpVar = 'hello';
function aFunctionPlacedOnRootLevel(){ // 얘는 root level에 선언
	function funcPlacedInsideRootLevelFunc1(){ // 얘는 root level -> lev 1 에 선언
        function funcPlacedInsideLevel1Func(){ // 얘는 root -> 1 -> lev 2 에 선언

        }
    }
    function funcPlacedInsideRootLevelFunc2(){ // 얘는 root -> lev 1 에 선언
    
    }
}

말 그대로, 개발자가 어떻게 선언하느냐에 따라 달라집니다.

 

아! 그러면 결국, 클로져는 대충 블록 안에 있는것 끼리 대충 이러쿵 저러쿵 같은 scope환경에서 함수나 객체가 선언되거나 하면, 외부 환경을 참조할 수 있군! 맞는 말입니다만, 아직 클로져에 대해 명확하기 설명하지 못하기에, 동작원리에 대해 알아보겠습니다.

 

Clousure : Execution Context 

그 악명 높은 클로져도, execution context관점에서 이야기 해보면 굉장히 당연하게 느껴집니다.

 

아래의 예시로 설명해보겠습니다.(MDN 예시)

 

function makeSizer(size) {
  return function () {
    document.body.style.fontSize = `${size}px`;
  };
}

const size12 = makeSizer(12);
const size14 = makeSizer(14);
const size16 = makeSizer(16);


size12()

 

자바스크립트 컴파일러 기준 먼저 해당 파일을 실행시킵니다.

 

1. Global Execution Context(GEC)가 생성됩니다.

- GEC의 생성시점 때,

-- 1) function :: makeSizer로의 메모리 할당이 일어납니다.

-- 2) size12 , size 14 , size 16의 호이스팅 (메모리 할당 - undefined)

-- 3) Global Object (window | Global)이 생성됩니다

-- 4) Global Object 로의 참조 'this'가 생성됩니다

-- 5) varibale environment가 생성됩니다

 

2. GEC가 실행됩니다

3. makeSizer(12)의 호출이 일어납니다

- 해당 과정에서 makeSizer(12)에 대한 execution context가 생성됩니다.

-- 1) 외부 환경(outer environment)으로의 참조가 생성됩니다. (상위 외부 환경 : global)

-- 2) object 'this'가 생성됩니다

-- 3) variable environment가 생성됩니다.

-- 4) 해당 execution context생성 시, 익명 함수의 메모리 할당이 일어납니다

-- 5) argument 'size'의 메모리 할당이 일어나고 해당 size는 variable environment에 놓입니다.

- 익명 함수를 반환 합니다.

- 해당 execution context가 execution stack에서 제거됩니다.

 

4. makeSize(14)의 호출이 일어납니다.

-- 3과 동일

5. makeSize(16)의 호출이 일어납니다. 

-- 3과 동일

 

6. size12() 가 호출 됩니다.

- 해당 과정에서 size12에 대한 execution context가 호출 됩니다.

- 외부 환경으로의 참조를 통해 size : 12px를 참조합니다. (Clousure) (해당 함수가 상위 스코프의 size: 12px를 참조하고 있기에, GC에 의해 수거되지 않습니다. 그렇기에, 비록 execution context(makeSize(12))가 제거되었더라도 참조할 수 있습니다.

 

In Practical

// umd module
// 여러 모듈들이 쓰는 패턴인데, 이왕 보시는김에 한번 이게 뭔지 맛보면 좋을거 같아 예시로 삼았습니다.(조금 과한 느낌이 없지않아있지만..)
// 보는 순서 (숫자를 따라가셔요)
(function(gObj, factoryFunc){
  if (typeof define === 'function' && define.amd) {
    define(['module1' , 'module2'] , factoryFunc);
  }else if(typeof module === 'object' && module.exports){
  
  	//3. factory 함수 시행 결과로, sum함수가 module.exports에 반환됨
    module.exports = factory([require('module1'),require(
      'module2'
    )]);
    //4. 이후 import (sum) -> sum 시행시, 클로져로 인한 dep1 ,dep2 참조가능
  }else{
    gObj.annonymousName = factoryFunc([gObj.module1, gObj.module2])
  }
})(typeof self !== 'undefined' ? self : this , function(...dependency){
	//1. 함수 정의부
  const [dep1 , dep2] = dependency
  function sum(arg1 , arg2){
  	//2. dep1 , dep2에 대한 참조
    return dep1(arg1) + dep2(arg2)
  }
  return {
    sum
  }
})