상세 컨텐츠

본문 제목

#13. Closure

내일배움캠프 학습/JavaScript

by 남민우_ 2024. 11. 19. 15:47

본문

클로저 Closure

클로저란 함수와 그 함수가 선언된 렉시컬 환경(outer - scope chaning) 과의 조합을 말한다.

그럼 이 렉시컬 환경이란 무엇일까?

렉시컬 환경

const x = 1;

function outerFunc() {
  const x = 10;
  function innerFunc() {
    console.log(x); // 10
  }

  innerFunc();
}

outerFunc();

먼저 각자의 스코프를 살펴보자

 

const x = 1; 은 전역 스코프

const x = 10; 은 outerFunc() 의 스코프이다.

 

그럼 console.log(x) 의 x는 이 값을 어디서 참조할까?

먼저 처음에는 이 값을 내부, innerFunc() 에서 참조하는데 이 함수에는 x에 대한 정의가 없다.

해서 그 바로 바깥, outerFunc() 에서 참조한다.

outerFunc() 가 실행될 때의 환경 정보를 참조하는 것이다.

 

이 환경 정보를 '함수가 선언된 렉시컬 환경', 즉 렉시컬 환경 이라고 부를 수 있다.

쉽게 풀어서, 함수가 선언될 당시의 외부 변수 등에 대한 정보를 말한다.

 

다른 예시를 하나 더 들어본다.

const x = 1;

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

function innerFunc() {
  console.log(x); // 1
}

outerFunc();

이 코드의 출력은 '1' 이 나온다.

outerFunc() 를 실행하면 그 안에서 innerFunc() 를 실행한다.

하지만 이 둘의 관계는 서로 다른 스코프에서 정의되었기에, 내부에서 호출하는 관계라고 해도 이는 단순한 호출의 관계일 뿐 스코프에 영향을 미치지 않는다.

스코프를 결정짓는 것은 정의의 위치인 것이다.

이 관계를 '렉시컬 스코프' 라고 한다.

 

외부 렉시컬 환경에 대한 참조값은 outer, 즉 함수 정의가 평가되는 시점에 결정된다.

 

다만 하나 더 참고해야 할 점은, 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 여전히 참조할 수 있다는 것이다.

말이 길어서 하나씩 끊어서 다시 설명한다.

 

1. 외부 함수보다 중첩 함수가 더 오래 유지되는 경우

2. 중첩함수는 종료된 외부 함수의 변수를 참조할 수 있다.

 

코드로 살펴보자.

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() 의 실행 컨텍스트는 종료
// 하지만 출력은 10 <- outer 의 실행 컨텍스트를 '여전히' 참조
innerFunc();

코드를 해석해보면, 먼저 innerFunc 변수에 outer() 의 리턴값을 할당하였다.

// (1)
const innerFunc = inner;
// (2)
const innerFunc = function() { console.log(x); }

먼저 1번이라고 설명할 수 있고, 1번은 다시 2번이라고 설명할 수 있다.

따라서 이 innerFunc 안에는 x에 대한 값이 들어가있지 않고, x는 outer() 의 스코프에서 선언되었지만, 이미 outer() 는 종료된 상태이다.

하지만 출력값은 10으로, outer() 의 실행 컨텍스트를 여전히 참조하고 있다.

 

이 과정이 어떻게 가능한 것일까?

이전에 가비지 컬렉터에 대해 설명하면서, 참조 카운트가 0인 메모리들을 삭제하여 메모리 공간을 확보한다고 했었다.

이 덕분에 실행 컨텍스트가 종료된 상황의 변수들은 삭제되는 것인데, 여기서 outer 함수의 렉시컬 환경은 innerFunc 에서 참조하고 있다.

참조카운트가 0이 아니기에 메모리가 삭제되지 않고, 따라서 x는 10이라는 값을 유지할 수 있는 것이다.

 

예시 코드들을 몇가지 첨부한다.

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());
// num = 100; // 치명적인 단점이 있어요.
console.log(increase());
console.log(increase());

출력 : 1 2 3

다만 이 코드에서의 num 변수는 '보호되지 않았기 때문에' 주석의 num = 100; 처럼 외부에서 접근할 위험이 있다.

 

그럼 어떻게 보완해야 할까?

세가지 단계를 밟아볼 수 있다.

1. 카운트 상태(num 변수) 는 increase() 함수가 호출되기 전까지는 변경되지 않아야 한다.

2. 이를 위해서 num 은 increase() 함수로만 변경할 수 있어야 한다.

=> num 이 전역 변수라서 접근이 가능하니 지역 변수로 바꿔볼까?

이 과정을 먼저 진행해보자.

// 카운트 상태 변경 함수 #2
const increase = function () {
  // 카운트 상태 변수
  let num = 0;

  // 카운트 상태를 1만큼 증가시킨다.
  return ++num;
};

// 이전 상태값을 유지 못함
console.log(increase()); //1
console.log(increase()); //1
console.log(increase()); //1

출력 : 1 1 1

호출할 때마다 num 을 0으로 초기화 하고 ++ 증가를 실행하기 때문에 매번 1이 출력되어 카운트의 의미가 사라진다.

 

이를 통해 위의 세 단계를 피드백을 해볼 수 있다.

1. num 변수의 외부에서의 변경은 방지하였다.

2. 하지만 increase() 의 호출마다 초기화된다.

 

이 1번은 유지하되 2번은 수정하는 방향으로 진행하기 위해, 즉 의도치 않은 변경은 방지하면서 이전의 상태값을 유지하기 위해 '클로저'를 사용하는 것이다.

// 카운트 상태 변경 함수 #3
const increase = (function () {
  // 카운트 상태 변수
  let num = 0;

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

// 이전 상태값을 유지
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3

이 코드에서 increase() 함수는

(function () {
	// 카운트 상태 변수
	let num = 0;
  
	// 클로저
	return function () {
	  return ++num;
	};
  })

이 과정까지만 수행한다.

위에서 설명한 방식과 동일하게 increase() 를 호출한 곳은 저 return function() { ~ } 을 불러오는 것과 같은 것이다.

이 return 은 클로저에 의해 let num = 0; 을 참조하기 때문에 num 의 값은 유지된다.

 

출력 : 1 2 3

 

간단히 설명하자면,

1. 위 코드 실행 시, increase(즉시 실행 함수) 를 호출한다. 이는 increase 변수에  function() { ~ } 이 부분을 할당하는 것이다.

2. 이 할당된 함수는 자신이 정의된 위치에 의해서 결정된 상위 스코프인 즉시 실행 함수, 이 increase 의 렉시컬 환경을 기억하는 클로저 기법이 적용되었다.

즉 let num = 0; 을 기억한다는 것이다.

3. 이후 즉시 실행 함수(increase) 는 소멸한다. callStack 에서 popUp 된다 라고도 말할 수 있다.

 

따라서 클로저 사용으로 인해 호출마다 초기화되지 않으면서 외부로부터 보호받는 환경을 만들 수 있다.

'내일배움캠프 학습 > JavaScript' 카테고리의 다른 글

#12. DOM + Class  (1) 2024.11.18
#11. 콜백 함수 (Call Back Func)  (6) 2024.11.12
#10. 3주차 숙제  (1) 2024.11.08
#9. This  (0) 2024.11.08
#8. 실행 컨텍스트  (3) 2024.11.07

관련글 더보기