처음부터 차근차근

[코어 자바스크립트] Closure 본문

Language/JavaScript

[코어 자바스크립트] Closure

HangJu_95 2023. 6. 12. 22:00
728x90

Closure에 대한 정의

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

이걸 알기 위해서는 함수가 선언된 렉시컬 환경에 대하여 자세하게 알 필요가 있다.

아래의 예시를 보자.

const x = 1;

function outerFunc() {
  const x = 10;
  function innerFunc() {
   // x는 어디서 참조할까요??
   // 함수가 선언된 렉시컬 환경!!!
   // 함수가 선언될 당시의 외부 변수 등의 정보!
    console.log(x); // 10
  }

  innerFunc();
}

outerFunc();

흐름을 살펴보면

  • innerFunc() 내부의 console.log(x)에서 참조하고 있는 x 값은
    • 먼저 스코프 내부에서 x 값을 찾는다.
    • 없는 경우 scope chain에 의해 바로 바깥쪽 scope를 찾는다.
      • 실행컨텍스트에서 배웠던 outer를 찾는 것
      • outer는 해당 실행컨텍스트의 생성시점의 LexicalEnvironment를 갖고 있다
    • 그래서 10에 먼저 접근하고, console.log(x)는 10이 출력!

 

만약 아래의 코드와 같다면??

둘 다 1이 출력된다.

// [렉시컬 스코프]
// JS 엔진은 함수를 어디서 '호출'했는지가 아니라
// 어디에 '정의'했는지에 따라서 스코프(상위 스코프)를 결정한다.

// '외부 렉시컬 환경에 대한 참조값' => 실행 컨텍스트의 outer
// 함수 정의가 평가되는 시점!!! Not 호출!!

const x = 1;

// outerFuck 내에 innerFunc가 '호출' 되고 있음에도 불구하고.

// innerFunc()에서는 outerFunc()의 x에 접근할 수 없죠.
// Lexical Scope를 따르는 프로그래밍 언어이기 때문
function outerFunc() {
  const x = 10;
  innerFunc(); // 1
}

// innterFunc와 outerFunc
// 각각 다른 Scope를 가지고 있다.
function innerFunc() {
  console.log(x); // 1
}

outerFunc();

렉시컬 스코프에 대해 좀 더 자세히 알아보자.

 - JS엔진은 함수를 어디서 '호출했는지'가 아니라, 함수를 어디에 '정의했는지'에 따라 상위 스코프를 결정.

const x = 1;

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

function bar() {
  console.log(x);
}

foo(); // 1
bar(); // 1

→ 다시 말하면, '외부 렉시컬 환경에 대한 참조'에 저장할 참조값, 즉 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다. = 렉시컬 스코프

 

그럼 정의된 환경에 대한 정보를 저장하는 곳은?? : outer

const x = 1;

function foo() {
  const x = 10;

  // 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
  // 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.
  bar();
}

// 여기보세요 여기!
// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 저장하여
// 기억한다.
function bar() {
  console.log(x);
}

foo();
bar();

클로저와 렉시컬 환경

외부 함수보다 중접 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 (여전히) 참조할 수 있다. ← 이 개념에서 중첩 함수가 바로 클로저이다.

const x = 1;

// 1
function outer() {
  const x = 10;
  const inner = function () {
    console.log(x);
  };
  return inner;
}

// outer함수를 실행해서 innerFunc에 담는다.
// outer return 부분을 innerFunc에 담는다.
const innerFunc = outer();
// -------------------------- 여기서는.. outer함수의 실행컨텍스트는??
innerFunc();

// 변화해보자.
// const innerFunc = function () {
//    console.log(x);
//  };
  • outer 함수를 호출하면 중첩 함수 inner를 반환(return)해요.
  • 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스탭에서 팝되어 제거된다(역할을 다 했으니깐)
  • inner 함수는 런타임에 평가된다.
  • inner함수가 innerFunc에 전달되었는데, 이는 outer 함수의 렉시컬환경을 (여전히) 참조하고 있다.
  • 즉, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다

어떻게 이게 가능한가?? -> 가비지 컬렉터는 참조 카운트가 0인 렉시컬 환경만 가져가기 때문!

클로저와 클로저가 아닌 것을 구분해보자.

function foo() {
  const x = 1;
  const y = 2;

  // 일반적으로 클로저라고 하지 않아요.
  function bar() {
    const z = 3;

    //상위 스코프의 식별자를 참조하지 않기 때문이죠.
    console.log(z);
  }

  return bar;
}

const bar = foo();
bar();
function foo() {
  const x = 1;

  // bar 함수는 클로저였지만 곧바로 소멸한다.
  // 외부로 나가서 따로 호출되는게 아니라, 선언 후 바로
	// 실행 + 소멸
  // 이러한 함수는 일반적으로 클로저라고 하지 않는다.
  function bar() {
    debugger;
    //상위 스코프의 식별자를 참조한다.
    console.log(x);
  }
  bar();
}

foo();
function foo() {
  const x = 1;
  const y = 2;

  // 클로저의 예
  // 중첩 함수 bar는 외부 함수보다 더 오래 유지되며
  // 상위 스코프의 식별자를 참조한다.
  function bar() {
    debugger;
    console.log(x);
  }
  return bar;
}

const bar = foo();
bar();

클로저의 활용

클로저는 주로 '상태를 안전하게 변경하고 유지하기 위해 사용'한다. 의도치 않은 상태의 변경을 막기 위해.

→ 상태를 안전하게 은닉한다(특정 함수에게만 상태 변경을 허용한다)는 표현

// 카운트 상태 변경 함수 #1
// 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터를 구현해요!

// 카운트 상태 변수
let num = 0;

// 카운트 상태 변경 함수
const increase = function () {
    // 카운트 상태를 1만큼 증가시킨다.
    return ++num;
};

console.log(increase()); // 1
num = 100; // 치명적인 단점이 있어요.
console.log(increase()); // 101
console.log(increase()); // 102

// 보완 사항
// 1 카운트 상태(num 변수의 값) => increase 함수가 호출되기 전까지는 변경되면 안됨.
// 2. 카운트 상태는 increase 함수만이 변경할 수 있어야 한다.
// 3. 전역변수 num 이놈이 문제다. -> 지역변수로??
// 카운트 상태 변경 함수 #2
const increase = function () {
   // 카운트 상태 변수
   let num = 0;
 
   // 카운트 상태를 1만큼 증가시킨다.
   return ++num;
 };
 
 // 이전 상태값을 유지 못함
 console.log(increase()); //1
 console.log(increase()); //1
 console.log(increase()); //1

 // 리뷰
 // 1.num 변수는 increase 함수의 지역 변수로 선언 -> 전역에서 변경 방지
 // = num 변수는 increase 함수만 변경할 수 있었음
 // 하지만 increase가 호출될 때마다 num이 초기화되는 이상한 코드
 // 의도치 않은 변경은 방지하면서, 이전 상태를 유지해야 함!

 // 클로저를 사용해보자!
// 카운트 상태 변경 함수 #3
const increase = (function () {
   // 카운트 상태 변수
   let num = 0;
 
   // 클로저
   return function () {
     return ++num;
   };
 })();
 
 // 
 // 이전 상태값을 유지
 console.log(increase()); //1
 console.log(increase()); //2
 console.log(increase()); //3

 // 위 코드 실행되면, '즉시 실행함수'가 호출!!
 // -> 함수가 반환(inner) -> increase에 할당
 // 2. increase변수에 할당된 함수는 자신의 정의된 위치에서 의해서 결정된 상위 스코프인 
 // 즉시 실행 함수의 ' 렉시컬 환경'을 기억하는 클로져 --> let num = 0;을 기억한다
 // 3. 즉시 실행 함수는 -> 즉시 소멸된다!!(outer 함수가 불리자마자 바로 call stack 에서 popup 되는 것과 비슷!!)
 // * 결론 : num은 초기화 X, 외부에서 접근할 수 없는 은닉된 값!! 의도되지 않은 변경도 걱정할 필요가 없다.
 // --> increase에서만 변경할 수 있기 때문에..!!

2. 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때

3. 부분 적용 함수

부분 적용 함수란 n개의 인자를 받는 함수에 미리 m개의 인자를 넘겨 기억시켰다가, 나중에 (n-m)넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수이다.

var partial = function() {
  var originalPartialArgs = arguments;
  var func = originalPartialArgs[0];
  if (typeof func !== 'function') {
    throw new Error('첫 번째 인자가 함수가 아닙니다.');
  }
  return function() {
    var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
    var restArgs = Array.prototype.slice.call(arguments);
    return func.apply(this, partialArgs.concat(restArgs));
  };
};

var add = function() {
  var result = 0;
  for (var i = 0; i < arguments.length; i++) {
    result += arguments[i];
  }
  return result;
};
var addPartial = partial(add, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10)); // 55

var dog = {
  name: '강아지',
  greet: partial(function(prefix, suffix) {
    return prefix + this.name + suffix;
  }, '왈왈, '),
};
dog.greet('입니다!'); // 왈왈, 강아지입니다.

4. 커링 함수

커링 함수 란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말합니다. 커링은 한 번에 하나의 인자만 전달하는 것을 원칙으로 합니다.

var curry5 = function(func) {
  return function(a) {
    return function(b) {
      return function(c) {
        return function(d) {
          return function(e) {
            return func(a, b, c, d, e);
          };
        };
      };
    };
  };
};
var getMax = curry5(Math.max);
console.log(getMax(1)(2)(3)(4)(5));

이러한 콜백지옥을 막기 위해서 ES6 화살표 함수를 쓰면 한 줄로 나타낼 수 있다.

var curry5 = func => a => b => c => d => e => func(a,b,c,d,e);

화살표 순서에 따라 함수에 값을 차례로 넘겨주면 마지막에 func가 호출될 거라는 흐름이 한 눈에 파악된다.

함수형 프로그래밍에서의 지연 실행 : 당장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 하면 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 것