본문 바로가기

프로그래밍/js

lessons 18. 클로저

2023.02.18

자바스크립트 스터디 6회차

공부 사이트: https://poiemaweb.com/

 

18. 클로저

1) 클로저의 개념

클로저는 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 매커니즘을 의미한다.

 

① 내부 함수를 외부 함수 내에서 호출하는 경우

 

function outer(){
	var x = 10;
	var inner = function() { console.log(x); };
	inner();
}

outer();  //10

outer()의 내부함수 inner(). inner()는 자신을 포함하고 있는 outer()의 변수 x에 접근할 수 있다. 

렉시컬 스코핑에 의해 inner()는 상위 스코프는 outer()이다.

#렉시컬 스코핑(Lexical scoping): 스코프는 함수를 호출할 때가 아니라 함수를 어디에 선언하였는지에 따라 결정된다.  inner()의 선언위치는 outer()의 내부이기에 inner()의 상위 스코프는 outer()가 되는 것.

#실행 컨텍스트(Execution Context): 코드를 실행하기 위해 코드를 블록으로 나누는데, 코드 블록에 담겨있는 변수/함수/this/arguments .... 등에 대한 정보를 담고 있는 문맥

 

[실행 컨텍스트]

ⓐ내부함수 inner 호출

ⓑ실행 컨텍스트가 실행 컨텍스트 스택에 쌓임

ⓒ변수 객체, 스코프 체인, 바인딩할 객체 결정.

여기서 스코프 체인은 전역 스코프를 가리키는 전역 객체, outer()의 스코프를 가리키는 활성 객체 를 순차적 바인딩.

스코프 체인이 바인딩한 객체 = 렉시컬 스코프

 


[TIP] 실행 컨텍스트

- 전역 컨텍스트(Global Context): 자바스크립트가 코드를 실행할 때 생성. 

- 함수 컨텍스트(Functional Context): 함수가 호출될 때마다 생성.

함수가 호출되면 새로운 실행 컨텍스트가 생성되어 실행 스택의 Top에 배치됨. 함수가 반환되면 스택에서 제거되고 이전 컨텍스트가 다시 시작.


 

▷ 내부 함수가 상위 스코프에 접근 가능한 것은 실행 컨텍스트의 스코프 체인을 자바스크립트 엔진이 검색했기 때문.

▷ 스코프 체인은 렉시컬 스코프의 참조를 순차적으로 저장하고 있다.

 

[스코프 체인 검색 과정]

ⓐ inner() 함수 스코프 내에서 변수 x 검색. → 실패

ⓑ inner() 함수를 포함하는 외부 함수 outer()의 스코프 내에서 변수 x 검색. → 성공

 

 

② 내부 함수를 외부 함수 내에서 반환하는 경우

function outer(){
	var x = 10;
	var inner = function() { console.log(x); };
	return inner;
}

var inner = outer(); 
inner();  //10

외부 함수 outer()는 내부함수 inner()를 반환하고 종료된다.

함수가 반환되면 실행 스택에서 제거된다고 했으니, outer()의 실행 컨텍스트 또한 실행 스택에서 제거될 것이다.

그런데도 내부함수 inner()는 outer()의 변수 x에 접근할 수 있다.

 

외부 함수보다 내부 함수가 오래 살아있는 경우,

외부 함수 밖에서 내부 함수가 호출됐을 때 내부 함수가 외부 함수의 지역변수에 접근할 수 있다. 이러한 함수를 클로저(Closure)라고 부른다.

 

 

③ 결론

클로저란?

- 반환된 내부함수가 자신이 선언됐을 때의 환경인 스코프를 기억

- 자신이 선언됐을 때의 환경 밖에서 호출되어도 그 환경에 접근할 수 있음.

자신이 생성될 때의 환경(Lexical environment)을 기억하는 함수

 

자유 변수

- 클로저에 의해 참조되는 외부함수의 변수

 

실행 컨텍스트 관점

- 내부 함수가 유효한 상태에서 외부 함수가 종료하여 외부 함수의 실행 컨텍스트가 반환되어도

- 외부 함수 실행 컨텍스트 내의 활성 객체는 내부 함수에 의해 참조되는 한 유효하다.

- 그리하여 내부 함수가 스코프 체인을 통해 참조할 수 있다.

 

※ 내부 함수는 외부 함수 내 변수의 복사본이 아니라 실제 변수에 접근한다. (참조 개념)

poiemaweb/js-closure

 

 

 

 

2) 클로저의 활용

① 상태 유지      가장 유용!

<!DOCTYPE html>
<html>
<body>
  <button class="toggle">toggle</button>
  <div class="box" style="width: 100px; height: 100px; background: red;"></div>

  <script>
    var box = document.querySelector('.box');
    var toggleBtn = document.querySelector('.toggle');
	
	// 즉시 실행 함수 toggle
    var toggle = (function () {
      var isShow = false;

      // ① 클로저를 반환
      return function () {
        box.style.display = isShow ? 'block' : 'none'; //display: none이면 감추기/block이면 보이기
        // ③ 상태 변경
        isShow = !isShow;
      };
    })();

    // ② 이벤트 프로퍼티에 클로저를 할당
    toggleBtn.onclick = toggle;
  </script>
</body>
</html>

- 함수를 반환한 후 소멸하는 즉시 실행 함수.

- 렉시컬 환경의 변수인 isShow를 기억해야 하기 때문에, 즉시 실행 함수는 클로저를 반환한다.

- 이벤트 핸들러인 클로저를 제거하지 않는 한, 클로저가 기억하는 isShow는 소멸하지 않는다. (현재 상태 기억)

 

결론: 클로저는 현재 상태를 기억하고 이 상태가 변경되더라도 최신 상태를 유지해야 하는 경우 유용하다.

 

 

② 전역 변수의 사용 억제

전역 변수는 누구나 접근할 수 있고 변경할 수 있기 때문에 사용을 최소화 해야 한다. (버그 발생 가능)

전역 변수 대신 클로저를 사용하자.

전역 변수와 다르게 클로저가 기억하는 변수는 외부에서 직접 접근할 수 없는 private 변수이므로, 의도치 않은 변경을 방지할 수 있다.

 

 

③ 정보 은닉

function Counter(){
	var counter = 0;
	
	// 클로저
	this.increase = function() {
		return ++counter;
	};
	
	// 클로저
	this.decrease = function() {
		return --counter;
	};
}

const c = new Counter();

console.log(c.increase());  //1
console.log(c.decrease());  //0
console.log(c.counter);     //undefined

생성자 함수 Counter

increase와 decrease 메소드는 렉시컬 환경인 생성자 함수 Counter의 스코프에 속한 변수 counter를 기억하는 클로저이다.

생성자 함수가 생성한 객체(인스턴스)의 메소드는 객체의 프로퍼티에만 접근할 수 있는게 아니라 자신이 기억하는 렉시컬 환경의 변수에도 접근할 수 있다.

 

counter는 this에 바인딩된 프로퍼티가 아니라 변수임

만약 this에 바인딩된 프로퍼티라면 public 프로퍼티이므로, 인스턴스를 통해 counter에 접근할 수 있어야 함. 그러나 undefined

 생성자 함수 내에 선언된 counter는 외부에서 접근 불가능하다. 클로저는 접근 가능. (기억하기 때문)

▷ 클로저의 특징을 사용해서 클래스 기반 언어의 private 키워드를 흉내낼 수 있다.

 

 

④ 자주 발생할 수 있는 실수

(예시)

var arr = [];

for (var i = 0; i < 5; i++) {
  arr[i] = function () {
    return i;
  };
}

for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}

생각: arr[0] ~ arr[4]에 함수를 할당. 각 함수는 i를 반환함. 순차적으로 0 ~4를 반환하겠지?

 현실: 5 5 5 5 5 

 이유: i가 전역변수이기 때문이다. 최종적으로 i는 5가 되며, arr에 저장된 함수를 실행할 때 i를 반환하기 때문에 5가 5번 반환된다.

 

// 클로저를 사용하여 해결
var arr = [];

for (var i = 0; i < 5; i++){
  arr[i] = (function (id) { 
    return function () {
      return id; 
    };
  }(i));
}

for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}

[ 코드 설명 ]

- 즉시 실행 함수는 매개변수 id에 인수 i를 전달받은 후 클로저를 반환.

- 즉시 실행 함수는 종료되고, 매개변수 id는 자유변수. 

- 클로저 내용: id를 반환. 상위 스코프의 자유변수이므로 값이 유지됨.

 

#너무 어렵다............

let 키워드 사용하면 문제는 해결됨. let 키워드는 블록 레벨 스코프를 지원하기 때문에..

# let 쓰자


[TIP] 즉시 실행 함수 문법

var result = (function(n1,n2){
	return n1 + n2;
}(12,34));

console.log(result);

function 예약어 옆 괄호에 매개변수, 함수 끝 괄호에 실행 시 사용할 인수 전달


 

'프로그래밍 > js' 카테고리의 다른 글

lessons 20. 빌트인 객체  (0) 2023.02.15
lessons 19. 객체지향 프로그래밍  (0) 2023.02.14
lessons 17. this 키워드  (0) 2023.02.13
lessons 16. Strict mode  (0) 2023.02.12
lessons 15. 스코프  (0) 2023.02.12