aotoyae

[JS] Closure 클로저, Lexical Environment 렉시컬 환경 본문

JavaScript

[JS] Closure 클로저, Lexical Environment 렉시컬 환경

aotoyae 2024. 1. 5. 01:19

 

 

💡 렉시컬 환경 : 어떤 함수가 선언(생성)될 때 그 당시의 외부 변수가 저장되는 곳!

그 정보들 : 외부 변수 등등

const x = 1; // 전역 스코프

function outerFunc() {
  const x = 10; // outerFunc 스코프
  function innerFunc() {
    console.log("x : " + x); // x : 10
    // (innerFunc 스코프에 x 가 없으니 이 함수가 선언될 때의 LE 인 outerFunc 로 찾으러 감)
  }

  innerFunc();
}

outerFunc();
console.log("x : " + x); // x : 1

 

➕ 함수를 어디서 '호출' 했는지가 아니라,
어디에 '정의' 했는지에 따라 스코프(상위 스코프) 가 결정된다!

outer 내에 inner 가 '호출' 되고 있음에도 불구하고

outer 와 inner 는 서로 다른 scope 를 가지고 있다.

const y = 1;

function outerFunc2() {
  const y = 10;
  innerFunc2(); // inner 함수가 여기서 불려서 y = 10 일 것 같지만
}

// 사실 여기에 위치한 것이라 전역 스코프를 바라본다.
function innerFunc2() {
  console.log("y : " + y); // y : 1
}

outerFunc2();

 

💡 클로저 : 외부 함수보다 중첩 함수가 더 오래 유지되는 경우,
중첩 함수는 이미 생명 주기가 종료된 외부 함수의 변수를 여전히 참조할 수 있다.
그 중첨 합수가 바로 클로저!

const z = 1;

function outerFunc3() {
  const z = 10;
  const inner = function () {
    console.log("z : " + z); // z : 10  ** 3 **
  };

  return inner;
}

const innerFunc3 = outerFunc3(); // ** 1 **
// outerFunc3 를 실행해 innerFunc3 에 담음
// = outerFunc3 의 return 부분을 innerFunc3 에 담는다는 얘기!
// = inner
// = function () {
//   console.log("z : " + z); // z : 10
// };
innerFunc3(); // ** 2 **

 

실행 컨텍스트를 상상해 보자 ~

** 1 ** 에서 outer 가 실행이 되고 없어졌으니 z 가 1이 나올 것 같지만

나중에 inner 에서 사용될 outer 의 변수가 사라지지 않고 유지되어서 값으로 10 이 나온다!

❗️ outer 함수의 LE 를 참조하는 곳(innerFunc3) 이 있으니까 가비지 컬렉터가 냅두는 것!

 

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

function foo() {
  const k = 1;

  // bar 함수는 클로저였지만 곧바로 소멸한다.
  // 외부로 나가 따로 호출되는게 아니라, 선언 후 바로 실행 + 소멸
  // 이런 함수는 일반적으로 클로저라고 하지 않는다!
  function bar() {
    console.log("k : " + k); // k : 1
  }

  bar();
}

foo();

function foo2() {
  const k2 = 3;

  // 클로저의 예
  // 중첩 함수 bar2 는 외부 함수 foo2 보다 더 오래 유지되며
  // 상위 스코프 foo2 스코프의 식별자 k2 를 참조한다.
  function bar2() {
    console.log("k2 : " + k2); // k2 : 3
  }

  return bar2;
}

const bar2 = foo2();
bar2();

 

💡 클로저는 주로 "상태를 안전하게 변경하고 유지하기 위해 사용한다!"

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

// 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터를 구현할래!
let num = 0;

const increase = function () {
  return ++num;
};

console.log(increase()); // 1
num = 100; // 누가 num 을 바꿔버림
console.log(increase()); // 101 => 원치 않던 값 출력
console.log(increase()); // 102 => 원치 않던 값 출력

 

보완해야 할 사항

1. 카운트 상태(num 변수의 값) ➡️ increase 함수가 호출되기 전까지는 변경되지 ❌

2. 이를 위해 count 상태는 increase 함수만이 변경!

3. 전역변수 num 이 문제다! ➡️ 그럼 지역변수로 바꿔볼까..?

const increase = function () {
  let num = 0;
  return ++num;
};

console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1

 

지역변수로 넣어서 변경은 방지했지만! (현재 num 변수는 오직 increase 함수만이 변경할 수 있음)

호출될 때마다 num 이 0 으로 초기화된다. 😶

 

여기서 클로저를 써보자!!

const increase = (function () {
  let num = 0;

  // 클로저
  return function () {
    return ++num;
  };
})();

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3

increase 는 function () { return ++num; } 을 뱉어낸다! = 할당된다!

저 뱉어낸 값은 외부함수(num) 를 참조하고 있으니 num 은 가비지 컬렉터가 가져가지 않고 유지된다.

 

정리하면..

1. 위 코드가 실행되면, '즉시 실행 함수'(function () { let num ~~ ++num; } ; }) 가 호출된다.

➡️ 함수가 반환(그 안에 있는 함수) ➡️ increase 에 할당

2. increase 변수에 할당된 함수는 자신이 정의된 위치에 의해
결정된 상위 스코프린 즉시 실행 함수의 LE 를 기억하는 클로저! ➡️ let num = 0; 을 기억한다.

3. 즉시 실행 함수는 즉시 소멸된다! (outer 함수가 불리자마자 바로 call stack 에서 pop up 되는 것과 비슷..!)

 

결론 : num 은 초기화 ❌ 외부에서 접근할 수 없는 은닉된 값이 되었다! 

의도되지 않은 변경도 걱정할 필요가 없다! increase 에서만 변경할 수 있기 때문에!