Programming/Javascript

[Javascript] 클로저(Closure) 함수와 스코프 체인(Scope Chain)

WANJIN 2018. 5. 23. 19:13
반응형

자바스크립트를 공부하다보면 클로저 함수라는 놈을 만나게 된다.


그런데 요놈, 한마디로 설명하기엔 내 머릿속에 정리가 잘 안되어 있어서, 이 참에 제대로 짚고 넘어가련다.


그래서 이번 포스트는 요놈, 클로저와 스코프 체인을 다뤄보려 한다.



스코프 체인(Scope chain)?

스코프 체인이 무엇인가 설명하기 전에, 먼저 글로벌 객체와 콜 객체를 알아야 한다.

글로벌 객체 & 콜 객체?

자바스크립트가 실행되면 내부적으로 글로벌 객체를 만든다.
요 글로벌 객체에는 글로벌 변수글로벌 함수가 담겨있다.
 
같은 맥락으로,
자바스크립트의 어떤 함수가 있는데, 요 함수가 실행되면 내부적으로 Call 객체를 만든다.
콜 객체에는 그럼 뭐가 있겠느냐구. 
함수 내에서 정의된 로컬 변수와 내부 함수가 있을 것이다.

콜 객체? 듣보인데? 

콜 객체가 이해가 안가면 Arguments 로 이해하면 좀 감이 온다.
우리가 함수 내부에서 arguments를 통해 인수에 접근할 수 있는 거슨, 바로 arguments가 이 콜 객체의 프로퍼티이기 때문이라는 사실.
요 객체에, 우리가 함수 내에서 정의한 로컬 변수도, 내부 함수도 다 프로퍼티로 저장 된다는 사실.

참고로, 콜 객체는 다른 말로 activation 객체라고도 하니, 구글링할 때 참고하자.




글엏담연. 

이게 대관절 스코프 체인이랑 무슨 관련이 있는건데?


'스코프' 라는 단어에서 우리는 무언가 '범위'를 가지고 장난치나보다, 하고 예상할 수 있다.


똑똑한 우리가 이미 잘 알고 있듯, 변수는 본인이 생성된 블록 범위 안에서만 유효하다.

스코프 체인의 '스코프'는, 바로 이 변수들과 함수들의 유효 범위와 비슷하다.





넘나 친절한 예시를 통해 더 친절하게 설명해보게따-.


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'
 function func1() {...}
 function func2() {...}

 console.log('global name: ', name)
 func1()
 var anony = func2()
 anony()


글로벌 객체

 
{
  name: 'global',
  func1: function() {...},
  func2: function() {...},

  anony: function() {...}


global name:  global 

 func1()

 var name = 'call 1'
 console.log('func1() name: ', name)

 function func1_func1() {...}

 func1_func1()


콜 객체 1

{
  name: 'call 1',
  func1_func1: function() {...}


func1() name:  call 1 

func1_func1()


 var name = 'call 1.1'
 console.log('func1_func1() name: ', name)

 function func1_func1_func1() {...}

 func1_func1_func1()


콜 객체 1.1

{
  name: 'call 1.1',
  func1_func1_func1: function() {...}
}


 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

{
  name: 'call 1.1.1'
}


func1_func1_func1() name:  call 1.1.1 

 func2()

 var name = 'call 2'
 console.log('func2() name: ', name)

 return function() {...}


콜 객체 2

{
  name: 'call 2'
} 


익명 함수는 여기 없음!


 func2() name:  call 2

 anony()

 var name = 'call anonymous'
 console.log('anonymous func() name: ', name)


콜 객체 anonymous

{
  name: 'call 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

 내용

 글로벌/콜 객체

 출력

script(global)

 var name = 'global' 
 function func2() {...}

 console.log('global name: ', name)
 var anony = func2()
 anony()

 anony('abc')

 anony()


글로벌 객체

  

  name: 'global',
  func2: function() {...},

  anony: function() {...}


global name:  global 

 func2()

 var name = 'call 2'
 console.log('func2() name: ', name)

 return function() {...}


콜 객체 2

{ name: 'call 2' } 


 func2() name:  call 2

 3

 anony()

 if(newName != undefined) name = newName
 console.log('anonymous func() name: ', name)


콜 객체 2
{ name: 'call 2' } 


콜 객체 anony

{ name: *(콜 객체 2 name) }


 anonymous func() name:  call 2

 anony('abc')

 if(newName != undefined) name = newName
 console.log('anonymous func() name: ', name)


콜 객체 2
{ name: 'abc' }


콜 객체 anony

name: *(콜 객체 2 name) } 

  anonymous func() name:  abc

 anony()

 if(newName != undefined) name = newName

 console.log('anonymous func() name: ', name)


콜 객체 2
{ name: 'abc' } 


콜 객체 anony

name: *(콜 객체 2 name) } 


  anonymous func() name:  abc



  1.  글로벌 스코프에서 var anony = func2() 라인이 실행되므로, 글로벌 객체의 anony 프로퍼티에 익명함수가 대입된다.

  2.  var anony = func2() 라인이 실행되면서 func2 함수가 실행된다.
    이 때, 새로운 로컬 변수 name이 콜 객체 2에 저장된다.
    그리고 익명함수를 읽어 반환 하는데, 이 때 name 이라는 변수가 필요하다는 것을 알아차린다.
    그래서, 스코프 체인에서 가장 가까운 상위 스코프인 콜 객체 2의 name의 주소를 기억한다.
    (만약 콜 객체 2에 name이 없다면, 그 상위인 글로벌 객체의 name 주소를 기억할 것이다.)
    즉, 외부 함수로부터 생성된 콜 객체 2의 name 프로퍼티를 참조하는 것이다.

  3. anony() 라인이 실행되면, 아까 기억해둔 콜 객체2의 name 값을 가져와 출력하고,

  4. anony('abc') 가 실행되면, 또 아까 기억해둔 콜 객체2의 name 값을 바꾼 후 출력하고,

  5. 마지막 anony() 라인이 실행되면, 또다시 콜 객체2의 name 값을 가져와 출력한다.



따라서 정상적인 실행 결과는 아래와 같다.


>> global name:  global

>> func2() name:  call 2

>> anonymous func() name:  call 2

>> anonymous func() name:  abc

>> anonymous func() name:  abc




클로저 함수를 정리해서 한 마디로 설명하자면,


상위 스코프의 로컬 변수를 참조하는 함수 내의 함수

라고 설명할 수 있겠다.



보통은 함수 내에서 사용된 로컬 변수는, 해당 함수의 실행이 종료되면 파기되는 것이 맞다.


그런데, 이와 같이 클로저 함수에 의해서 계속 참조되고 있는 경우에는, 해당 로컬 변수를 파기하지 않고 계속 보관하는 것이다.










여기까지가 스코프 체인과 클로저 함수에 대한 원리(?)였다.


사실 포스트를 작성하면서 느낀거지만... 


어느 시점에 콜 객체가 생성되고, 또 그것들은 어디에 저장되며, 정확히 언제 파기되는 것인지 등등 더 제대로 알아야 할 것이 많음을 깨닫고 있다시와..ㅜ



그래서 다음엔 '호이스팅'을 좀 다뤄볼까 한다.


다루다 보면, 자연스레 알게될 것들이 많을 것 같다.




근데.. 언제? ...ㅎㅎ.......



반응형