[Javascript] 클로저(Closure) 함수와 스코프 체인(Scope Chain)
자바스크립트를 공부하다보면 클로저 함수라는 놈을 만나게 된다.
그런데 요놈, 한마디로 설명하기엔 내 머릿속에 정리가 잘 안되어 있어서, 이 참에 제대로 짚고 넘어가련다.
그래서 이번 포스트는 요놈, 클로저와 스코프 체인을 다뤄보려 한다.
스코프 체인(Scope chain)?
글로벌 객체 & 콜 객체?
글엏담연.
이게 대관절 스코프 체인이랑 무슨 관련이 있는건데?
'스코프' 라는 단어에서 우리는 무언가 '범위'를 가지고 장난치나보다, 하고 예상할 수 있다.
똑똑한 우리가 이미 잘 알고 있듯, 변수는 본인이 생성된 블록 범위 안에서만 유효하다.
스코프 체인의 '스코프'는, 바로 이 변수들과 함수들의 유효 범위와 비슷하다.
넘나 친절한 예시를 통해 더 친절하게 설명해보게따-.
var name = 'global' function func1() { var name = 'call 1' console.log('func1() name: ', name) function func1_func1() { var name = 'call 1.1' console.log('func1_func1() name: ', name) function func1_func1_func1() { var name = 'call 1.1.1' console.log('func1_func1_func1() name: ', name) } func1_func1_func1() } func1_func1() } function func2() { var name = 'call 2' console.log('func2() name: ', name) //익명함수 return function() { var name = 'call anonymous' console.log('anonymous func() name: ', name) } } console.log('global name: ', name) func1() var anony = func2() anony()
위 코드의 마지막 줄에,
func1_func1_func1()
라는 코드를 넣고 실행하면, 당연히 우리는
ReferenceError: func1_func1_func1 is not defined
라는 에러를 마주하게 된다.
왜?
func1_func_func1 함수의 유효 범위, 즉 스코프를 벗어난 엉뚱한 데에서 실행을 해버렸으니까.
이렇듯, 자바스크립트의 모든 변수와 함수들은 스코프를 가지고 있고,
이 스코프는 무엇으로 인해 정해지냐 하면-.
바로바로 아까 위에서 설명한 '글로벌 객체'와 '콜 객체'에 저장되어 있는 변수와 함수들에 의해 정해지는 것이다.
위의 예시 코드를 스코프 별로 구분하여 어떤 객체가 생성되는가 보면, 아래와 같다.
scope | 내용 | 생성 객체 | 출력 |
script(global) | var name = 'global' | 글로벌 객체 anony: function() {...} | global name: global |
func1() | var name = 'call 1' | 콜 객체 1 | func1() name: call 1 |
func1_func1() | var name = 'call 1.1' | 콜 객체 1.1 | func1_func1() name: call 1.1 |
func1_func1_func1() | var name = 'call 1.1.1' console.log('func1_func1_func1() name: ', name) | 콜 객체 1.1.1 | func1_func1_func1() name: call 1.1.1 |
func2() | var name = 'call 2' | 콜 객체 2
| func2() name: call 2 |
anony() | var name = 'call anonymous' | 콜 객체 anonymous | anonymous func() name: call anonymous |
친절미 넘치게
보기 좋은 그림으로까지 또 보여주자면, 아래와 같은 그림을 만나볼 수 있다.
이거슬 글로 정리하자면,
글로벌 객체와, 콜 객체가 생성된 순서대로 연결된 것을 스코프 체인이라 한다.
아까 위에서 func1_func1_func1() 를 맨 아랫줄에 넣고 실행하면, 오류를 뱉는다고 했는데 그 이유는,
'func1_func1_func1'을 실행하라는 명령을 만나면, 해당 라인이 실행되고 있는 스코프( =글로벌객체)에 'func1_func1_func1'가 있는지를 찾아보는데,
온데간데 없기 때문에 에러를 뱉는 것이다.
반대로 'func1_func1' 함수 내에서 'func1_func1_func1' 함수가 실행 가능한 이유는,
그렇지만 'func1_func1' 함수의 스코프( =Call 1.1 객체)에는 'func1_func1_func1' 프로퍼티가 존재하므로 실행이 가능한 것이다.
당연하게 여겨왔던 것들에 대한 경이로움을 느끼는 순간이 아니라고 말할 수 없을 것이다아-.
요까지가 스코프 체인에 대한 설명이었다.
근데 설명을 보다가 요놈 익명함수가 좀 거슬렸을 거신데.
요게 바로 클로저 함수를 설명하기 위한 키 포인트이므로 아래로 ㄱㄱ
클로저(Closure) 함수
var name = 'global' function func2() { var name = 'call 2' console.log('func2() name: ', name) //익명함수 return function(newName) { if(newName != undefined) name = newName console.log('anonymous func() name: ', name) } } console.log('global name: ', name) var anony = func2() anony() anony('abc') anony()
이 친구의 출력 결과는, 아마도
>> global name: global >> func2() name: call 2 >> anonymous func() name: global >> anonymous func() name: abc >> anonymous func() name: abc |
를 예상했을 수 있겠지만, 안타깝게도 틀렸다.
이 친구도 흐름표를 통해 만나러 가즈아-!
# | scope | 내용 | 글로벌/콜 객체 | 출력 |
1 | script(global) | var name = 'global' anony('abc') anony() | 글로벌 객체 anony: function() {...} | global name: global |
2 | func2() | var name = 'call 2' | 콜 객체 2 | func2() name: call 2 |
3 | anony() | if(newName != undefined) name = newName | 콜 객체 2 콜 객체 anony { name: *(콜 객체 2 name) } | anonymous func() name: call 2 |
4 | anony('abc') | if(newName != undefined) name = newName console.log('anonymous func() name: ', name) |
콜 객체 anony { name: *(콜 객체 2 name) } | anonymous func() name: abc |
5 | anony() | if(newName != undefined) name = newName console.log('anonymous func() name: ', name) | 콜 객체 2 콜 객체 anony { name: *(콜 객체 2 name) } | anonymous func() name: abc |
- 글로벌 스코프에서 var anony = func2() 라인이 실행되므로, 글로벌 객체의 anony 프로퍼티에 익명함수가 대입된다.
- var anony = func2() 라인이 실행되면서 func2 함수가 실행된다.
이 때, 새로운 로컬 변수 name이 콜 객체 2에 저장된다.
그리고 익명함수를 읽어 반환 하는데, 이 때 name 이라는 변수가 필요하다는 것을 알아차린다.
그래서, 스코프 체인에서 가장 가까운 상위 스코프인 콜 객체 2의 name의 주소를 기억한다.
(만약 콜 객체 2에 name이 없다면, 그 상위인 글로벌 객체의 name 주소를 기억할 것이다.)
즉, 외부 함수로부터 생성된 콜 객체 2의 name 프로퍼티를 참조하는 것이다. - anony() 라인이 실행되면, 아까 기억해둔 콜 객체2의 name 값을 가져와 출력하고,
- anony('abc') 가 실행되면, 또 아까 기억해둔 콜 객체2의 name 값을 바꾼 후 출력하고,
- 마지막 anony() 라인이 실행되면, 또다시 콜 객체2의 name 값을 가져와 출력한다.
따라서 정상적인 실행 결과는 아래와 같다.
>> global name: global >> func2() name: call 2 >> anonymous func() name: call 2 >> anonymous func() name: abc >> anonymous func() name: abc |
클로저 함수를 정리해서 한 마디로 설명하자면,
상위 스코프의 로컬 변수를 참조하는 함수 내의 함수
라고 설명할 수 있겠다.
보통은 함수 내에서 사용된 로컬 변수는, 해당 함수의 실행이 종료되면 파기되는 것이 맞다.
그런데, 이와 같이 클로저 함수에 의해서 계속 참조되고 있는 경우에는, 해당 로컬 변수를 파기하지 않고 계속 보관하는 것이다.
여기까지가 스코프 체인과 클로저 함수에 대한 원리(?)였다.
사실 포스트를 작성하면서 느낀거지만...
어느 시점에 콜 객체가 생성되고, 또 그것들은 어디에 저장되며, 정확히 언제 파기되는 것인지 등등 더 제대로 알아야 할 것이 많음을 깨닫고 있다시와..ㅜ
그래서 다음엔 '호이스팅'을 좀 다뤄볼까 한다.
다루다 보면, 자연스레 알게될 것들이 많을 것 같다.
근데.. 언제? ...ㅎㅎ.......