0%

[Javascript] 크롬 개발자 도구를 이용해서 알아보는 클로저

클로저의 대표적인 개념은 다음과 같습니다.

  1. 클로저는 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 현상이다.
  2. 클로저는 선언될 당시의 정보를 담는다.
  3. 선언될 당시의 정보를 외부로 노출시킬 수 있는 유일한 방법은 함수 내부에서 return하는 것이다.

처음 자바스크립트를 배울 때 이 개념이 너무 어려웠었던 기억이 납니다. (물론 지금도 어렵…)
그래서 클로저 예제를 크롬 개발자 도구로 디버깅해서 어떻게 동작하는지 확인해봅시다.

1
2
3
4
5
6
7
8
9
10
var outer = function () {
var count = 1;
var inner = function () {
return ++count;
};
return inner;
};
var outer2 = outer();
console.log(outer2());
console.log(outer2());

검색하면 흔하게 볼 수 있는 클로저 예제입니다.
outer 함수에서 inner 함수를 반환하고, 그걸 outer2 변수에 담아 실행하는 예제입니다. 선언된 당시의 정보인 inner를 함수 내부에서 return 하여 외부로 노출(개념 3번)시키고 있습니다.

이때 outer() 함수의 실행은 끝났다고 볼 수 있습니다. 하지만 outer2 변수가 참조하고 있죠.
따라서 outer2는 실행이 끝나버린 outer() 함수의 inner에 접근할 수 있습니다.
그런데 어떻게 실행이 끝나버린 함수의 내부에 접근할 수 있을까요?

이는 실행이 끝났어도 그것을 참조하는 대상이 하나라도 있는 경우 가비지 컬렉션의 대상이 되지 않아서입니다.
그래서 클로저를 더이상 실행하지 않을 경우 참조를 다 끊어버려 가비지 컬렉터가 수거해가게 만들면 메모리 관리를 효율적으로 할 수 있겠죠.
아무튼 이렇게 글로만 봐서는 잘 안 와닿을 수 있습니다.

2020-07-26-debug-closure/2020-07-26_1.58.25.png

해당 부분에 breakpoint를 지정합니다. breakpoint를 지정하면 해당 파일을 실행할 때 breakpoint에서 실행이 멈추게 됩니다.
breakpoint를 지정하면 우측 breakpoints 패널에 현재 지정한 breakpoint들이 나옵니다.
지정이 끝났으면 새로고침을 해봅시다.

2020-07-26-debug-closure/2020-07-26_1.59.00.png

8번 라인이 실행된 상태로 실행이 중단됩니다.
우선 콜 스택 부분을 봅시다. 처음에는 전역 컨텍스트가 실행되기 때문에 anoymous 함수가 쌓입니다. 실행 컨텍스트를 설명하는 다른 글에서는 main 함수가 쌓인다고도 하는데 anoymous 함수나 main 함수나 동일합니다. 단지 표현의 차이일 뿐입니다. (크롬은 anoymous로 표현)
전역 컨텍스트이기 때문에 스코프는 글로벌이 됩니다.

2020-07-26-debug-closure/2020-07-26_2.00.20.png

Step(빨간 박스) 버튼을 눌러 다음 실행으로 이동합니다.

2020-07-26-debug-closure/2020-07-26_2.01.52.png

outer 함수가 실행되면서 콜스택에 쌓입니다.

이때 함수가 호출된 상태이기 때문에 자바스크립트 엔진이 outer에 대한 환경 정보를 수집해서 실행 컨텍스트를 생성합니다. 환경 정보는 함수 내에 있는 모든 것(변수, 함수 선언, 스코프, this)이라 생각하면 편합니다. 그래서 scope에 count, inner, this가 생성되는 것입니다. 위에서 클로저는 선언된 당시의 정보를 담는다고 그랬죠?(개념 2번) outer 스코프 내에 있는 count와 inner, this가 바로 outer 함수의 정보라 할 수 있습니다.

참고로 count와 inner의 값이 undefined인 이유는 아직 할당 단계에 다다르지 않았기 때문입니다. 실행 컨텍스트에서 변수는 선언 단계 → 초기화 단계 → 할당 단계를 거쳐 값이 할당되게 되는데, var 키워드로 선언된 변수의 경우 선언 단계와 초기화 단계가 한번에 이루어지게 됩니다. 이 상태에서 count 변수나 inner 함수에 접근하면 undefined를 반환하는데요, 이게 그 유명한 호이스팅입니다.

잠시 딴 길로 샜는데요. Step 버튼을 눌러 다음 라인으로 가봅시다.

2020-07-26-debug-closure/2020-07-26_2.18.18.png

변수 count는 할당 단계를 거쳐 1을 할당받았습니다.

한번 더 Step 버튼을 눌러봅시다.

2020-07-26-debug-closure/2020-07-26_2.19.55.png

inner는 할당 단계를 거쳐 익명 함수(function() {return ++count;})를 할당받았습니다.
이때 outer 함수의 스코프에는 return value가 생깁니다. 이때의 return value는 inner겠죠?
return value가 함수로 나오는 이유는 inner가 익명 함수를 할당받았기 때문입니다.
다음 Step으로 가봅시다.

2020-07-26-debug-closure/2020-07-26_2.25.15.png

outer 함수의 실행이 종료되었습니다. 우측 콜 스택 패널을 보면 outer 함수가 사라진 것을 확인할 수 있습니다.

2020-07-26-debug-closure/2020-07-26_2.26.10.png

9번 라인에서 outer2()가 실행된 모습입니다. 위에서 클로저는 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 현상(개념 1번)이라고 했었습니다. outer2()는 이미 생명 주기가 끝난 outer() 함수의 변수 inner를 참조하고 있기 때문에 4번 라인으로 이동하게 됩니다.

outer2로 inner 함수를 실행한 상태이기 때문에 콜 스택에는 inner가 쌓이게 됩니다.
우측 스코프 패널을 보시면 스코프에 outer 클로저가 보입니다. 이때 count의 값은 1입니다.

2020-07-26-debug-closure/2020-07-26_2.35.36.png

전위 연산을 하고 있기 때문에 count는 1이 증가된 2로 반환됩니다.

2020-07-26-debug-closure/2020-07-26_2.36.58.png

inner() 함수가 종료되며 콜 스택에서 사라졌습니다.

2020-07-26-debug-closure/2020-07-26_2.38.10.png

그리고 콘솔에는 2가 출력이 됩니다.
10번째 줄을 실행할 차례입니다. 9번 라인을 실행했을 때와 동일합니다.

2020-07-26-debug-closure/2020-07-26_2.39.16.png

여기서 한가지 주의깊게 보아야 할 것은, 10번 라인을 실행할 때 9번 라인에서 실행한 값을 기억하고 있다는 것입니다. outer2에서 값을 참조하고 있기 때문이죠.
우측 scope 패널의 count를 보면 값이 2인 것을 확인할 수 있습니다.

2020-07-26-debug-closure/2020-07-26_2.40.36.png

전위 연산이 되어 count의 return value는 3이 됩니다.

2020-07-26-debug-closure/2020-07-26_2.41.25.png

inner() 함수가 종료되며 콜 스택에서 사라졌습니다.

2020-07-26-debug-closure/2020-07-26_2.42.14.png

마지막으로 3을 출력한 모습입니다.