JavsScript를 배우면서 클로저에 관련된 개념은 여러 번 접했었다. 하지만 찾아볼 때는 어렴풋이 이해하면서도, 나중에 다시 접했을 때에는 그게 뭔지 잘 모르는 상황의 반복이었다.
오늘은 클로저와 그 관련된 개념에 대해 확실하게 짚어보려고 한다.
클로저를 이해하기 위해서는 스코프에 대한 지식이 필요하다.
그럼 코드와 함께 알아보자.
var a = 1234; // 전역변수 a
function outter() {
var a = 5678; // outter의 지역변수 a
function inner() {
console.log(a);
}
inner();
}
outter(); // 실행결과 : 5678
outter함수가 실행되면 다음과 같은 일이 일어난다.
outter의 지역변수 a 선언 → inner함수 선언 → inner함수 실행
inner함수가 실행되면 다음과 같은 일이 일어난다.
inner함수 선언 부분을 찾아감 → inner함수 내부의 a 변수를 탐색 → inner함수 내부에 a 변수가 있다면 a 변수 참조, 없다면 → inner함수로부터 가장 가까운 바깥 함수(이 경우 outter)에서 a 변수를 탐색 → outter에서 a 변수 발견 → outter의 a 변수 참조 → console.log(a) 실행 → 종료
알기 쉽게 보려고 알고리즘의 순서도를 만들어 왔다. 물론 실제 코드의 실행과정은 더 복잡하다.
위의 순서도에서 빨간 박스가 inner함수의 스코프를 시각화 한 것이다.
inner함수 내부에서 console.log(a)가 실행되었으니, a라는 변수가 inner함수 내부에 있는지 없는지 찾아본다. 이 때 찾아보는 곳이 inner함수의 [[Scopes]] 프로퍼티, 즉 inner함수의 스코프인 것이다.
[[Scopes]] 프로퍼티는 실제로 inner함수의 프로퍼티로 존재하고 있다.
inner함수의 [[Scopes]] 프로퍼티
inner함수 내부에서 console.log(a)가 실행되면, inner함수의 스코프에서 a를 찾아보고, 없으면 바깥함수의 스코프에서 a를 찾아보고, 없으면 바깥함수의 바깥함수의 스코프에서 a를 찾아보고... 계속 반복하다가 가장 바깥함수인 전역객체의 스코프에서 a를 찾아본다. 전역객체의 스코프에서도 a가 보이지 않는다면 그때는 a가 없다는 에러 메세지를 반환하게 된다. 전역객체의 바깥함수는 존재하지 않으니 말이다.
그리고 중요한 부분은 inner함수가 실행 될 때 inner함수의 선언 부분을 찾아간다는 것이다. 제일 중요한 부분이다. 함수가 실행되면 그 함수의 선언부분을 찾아간다.
위의 상황에서 inner함수가 실행 되면, inner함수의 선언부분을 기준으로 스코프를 참조해나간다.
정말 그런지 확인하기 위해 inner함수의 선언 코드의 위치를 옮겨보자.
var a = 1234; // 전역변수 a
function inner() {
console.log(a);
}
function outter() {
var a = 5678; // outter의 지역변수 a
inner();
}
outter(); // 실행결과 : 1234
이 코드에서 inner 함수가 실행될 때에 다음과 같은 일이 일어난다.
inner함수 선언 부분을 찾아감 → inner함수 스코프에서 a 탐색 → inner함수 스코프에 a가 없음 → 바깥함수인 전역 객체의 스코프 참조 → 전역객체 스코프에서 a 탐색 → 전역객체 스코프에서 a 발견 → 전역객체 스코프의 a 참조 → console.log(a) 실행 → 종료
결과는 예상대로 전역변수 스코프의 a 값인 1234였다.
inner 함수의 실행 부분이 기준이 아니고, inner 함수의 선언 부분을 기준으로 스코프를 참조해 나가는 것이다.
이와 같이 내부 함수의 스코프로부터 바깥 함수의 스코프를 탐색해 나가는 과정을 스코프 체인이라고 한다.
또한 내부 스코프에서 바깥 스코프를 참조하는 건 가능하지만, 그 반대로 바깥 스코프가 내부 스코프를 참조할 수는 없다.
function outter() {
function inner() {
var innerVar = 5678; // 내부 함수에 선언된 innerVar
}
inner();
console.log(innerVar);
}
outter(); // 실행 결과 : innerVar is not defined
이제 클로저에 대해 알아볼 준비가 되었다고 생각한다.
이번에도 코드부터 살펴보자.
function outter() {
var outterVar = 5678;
function inner() {
console.log(outterVar);
}
return inner;
}
var innerFunc = outter();
innerFunc(); // 실행 결과 : 5678
outter함수를 실행하면 inner함수를 정의하고, inner함수를 반환한다. 반환된 inner함수는 innerFunc 변수에 함수 표현식으로 들어가게 된다.
그리고 innerFunc함수를 실행해보면 outterVar 값을 잘 참조하고 있는 것을 볼 수 있다.
주목해야 할 부분은 inner함수를 반환하고 생이 마감된 outter함수 내부의 outterVar에 접근할 수 있는 방법이 없었는데, 반환된 inner함수에서 멀쩡히 outterVar에 접근할 수 있다는 부분이다.
외부 스코프가 내부 스코프를 참조할 수는 없으니까...
이렇게 함수 내부에서 선언된 변수는 바깥에서 읽을 수 없지만, 함수가 반환한 내부 함수를 통해 함수의 지역변수에 접근할 수 있을 경우, 이 내부 함수를 클로저 라고 할 수 있다.
MDN에서는 클로저에 대해 어떻게 정의하고 있을까?
클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.
아니, Lexical scoping이란 당최 무엇인가? 불안해 할 필요가 없다. 우리는 이미 이 개념에 대해 알고 있기 때문이다.
var a = 1234;
function inner() {
console.log(outterVar);
}
function outter() {
var a = 5678;
inner();
}
outter(); // 실행 결과: 1234
이것이 Lexical Scoping이다. 좀 전과 다를 게 없는 코드이다.
함수가 선언되었을 때를 기준으로 스코프를 정하는 방식이 Lexical Scoping, 함수가 호출되었을 때를 기준으로 스코프를 정하는 방식이 Dynamic Scoping 이라고 할 수 있다.
JavaScript는 Lexical Scoping 방식을 따르기 때문에 함수가 선언된 부분을 기준으로 그 함수의 스코프를 정한다.
JavaScript가 Dynamic Scoping 방식을 따랐다면 위의 코드의 실행 결과는 5678이 되었을 것이다.
inner 함수가 호출된 부분을 기준으로 Scope를 정하니까 말이다.
사실 이 내용들은 1년전에 모던 자바스크립트 입문 책을 읽으며 다 접했었던 내용이다. 하지만 그 때는 정말 아무것도 모르고 발을 내딛었던 때라 글을 읽으면서도 전혀 이해하지 못 했었다.
그 뒤로도 클로저와 관련된 뜻하지 않은 오류를 겪으며 찾아봤었지만, 오류만 해결하고 클로저가 무엇인지 제대로 이해하지 않고 넘어갔었다.
그래서 이번 기회에 클로저니 스코프니 하는 녀석들이 대체 뭔지 제대로 이해하려고 글을 쓰게 되었다.
남을 가르치기에는 20년도 이르지만, 가르치는 듯한 느낌으로 글을 작성하면 틀린 것이 없는지 꼼꼼히 확인하게 되고 공부도 더 잘되어서 그런 느낌으로 작성했다.
다음 포스트는 실행 문맥에 관해서 작성하려고 한다. 스코프와 밀접한 관련이 있는 녀석이지만 무엇인지 정확히 알지 못하고 있다.
프로그래밍에서 무언가를 안다고 말 할 수 있는 때는 보이지 않는 추상적인 개념을 구체적으로 설명할 수 있게 되는 때라고 생각한다.
프론트엔드에 대한 개념을 다 안다고 말 할 수 있게 되면 나도 자신감있게 취업문을 두드릴 수 있을 것이다.
12bme님의 자바스크립트 - 성능을 높이는 코드 스타일
이웅모님의 스코프에 대한 정리 글
pa325님의 Javscript 개념 - 스코프
MDN 공식 문서 - 클로저