Vue.js 공식 홈페이지에서는 단위 테스팅(Unit Testing)을 위한 테스트 러너로써 Karma를 적극 권장하고 있다.

 

어떤건지 궁금해서 한번 써본 적은 있지만, 실무에 적용해보기에 앞서 너란 놈에 대해 깊은 공부가 필요함을 느끼고...

 

정말 오랜만의 포스팅 주제로 삼았다.

 

 

KARMA

공식 홈페이지에 가보면, 이렇게 소개하고 있다.

 

A tool which spawns a web server that executes source code against test code for each of the browsers connected.

 

위의 영어가 너무 어려워서, 아래처럼 아름다운 한글로 풀어봤다.

 

카르마는 어떤 웹 서버를 생성하는 도구이다.

이 웹 서버는, 다양한 브라우저들과 연결되어 있다.

그리고, 이 웹서버는 연결되어 있는 각 브라우저 위에서 테스트를 수행한다.

 

그렇다. 이 친구의 목적은 하나다.

개발자들이 다양한 브라우저 환경에서 단위 테스트를 손쉽게 할 수 있는 아름다운 테스팅 환경을 제공하는 것.

 

그렇다면, 브라우저가 연결되어있어야 테스트가 가능한데,

카르마가 생성한 테스팅을 수행하는 웹서버는 어떻게 브라우저를 감지할까?

 

 

브라우저 감지

아래 두 가지의 방법으로 브라우저를 감지한다.

  1. 개발자가 손수 브라우저를 열어준다.
  2. 자동으로 감지하도록 할 브라우저를 카르마 config 파일에 지정해준다.

1. Manually

첫 번째 방법, 즉 손수 진행하려면 

테스트를 수행하려는 브라우저를 열고, 카르마 웹 서버가 돌고 있는 URL을 찍어준다.

 

http://<hostname>:<port>/ 

이렇게 주소를 찍고 들어올텐데, 여기서 <hostname>은  카르마 서버가 돌고 있는 그것일테고,

<port>는 그 서버가 listening하고 있는 그것임.

 

근데 로컬에서 띄우고 별 일 없으면 그 주소는 그냥 http://localhost:9876/ 일 것이다.

 

이렇게 손수 브라우저를 띄우는 방법은, pc가 아니라 모바일 환경에서 테스팅할 때 유용하다.

(물론 카르마 서버랑 같은 네트워크여야 하겠지)

 

2. Auto

그러나 웹 개발자라면, 손쉽게 자동으로 감지하도록 카르마에게 시키는 것이 아름다워 보인다.

그러려면 configuration 파일에 자동으로 띄울 브라우저 리스트를 지정해주어야 하는데, 아래처럼 하면 된다.

browsers: ['Chrome']

이렇게 하면 카르마는 알아서 자동으로 브라우저를 감지하고, 테스트를 모두 수행하면 자동으로 죽인다.

(configuration 파일에 대한 자세한 내용은, 이 포스트에서는 다루지 않기로 한다.)

 

 

이 때, 카르마는 브라우저 런처(browser launcher)를 이용해서 브라우저를 띄우는데,

사용 가능한 브라우저 런처 리스트는 아래와 같다.

대부분의 이 브라우저 런처들은 플러그인 형태로 끼워지는 것이다.

그러므로, 우선적으로 런처가 설치되어야 한다. 아래 예시처럼.

# Install the launcher first with NPM:
$ npm install karma-firefox-launcher --save-dev

 

이거슨 누군가가 미리 만들어놓은 브라우저 런처 플러그인이고, 우리가 손수 커스텀 플러그인을 만들어도 된다. 응 그냥 갖다 쓸래

 

 

그럼 이제 자동으로 띄운 브라우저에서, 어떻게 테스트 명령을 내리고 또 테스트를 실행한 결과를 받아오는지 알아보자.

 

 

웹 소켓을 통한 연결

카르마는 웹 소켓을 이용해 브라우저가 카르마 웹 서버와 커넥션을 가지도록 한다.

 

카르마는 config 에 정의된 브라우저들을 띄운 후, 시작 페이지를 카르마 서버의 URL 로 설정한다.

그리고 이 페이지가 브라우저에서 실행될 때, 웹 소켓을 통해 서버에 연결되는 것이다.

 

카르마 서버는 웹 소켓 연결을 확인하면, 클라이언트 페이지에게 테스트 실행을 지시한다. 

 

웹 소켓으로 연결되어 있기 때문에 언제든지 테스트 수행 명령을 내릴 수 있는데,

이는 카르마가 테스팅 환경으로써 넘나 훌륭한 이유라고 할 수 있다.

클라이언트 페이지의 요청이 없어도, 언제든지 카르마 서버의 필요에 의해 테스트 수행 명령을 내릴 수 있기 때문이다.

덕분에 파일의 변화가 일어날 때마다 자동으로 테스트가 실행될 수 있다.

 

 

파일 변화 자동 감지

카르마는 FS Model이라는 놈을 가지고 있다. 

소스코드 파일의 가장 마지막 버전의 타임스탬프라고 보면 된다.

 

그리고 또, FS Watcher 라는 놈도 있다.

이거슨 테스트 중인 프로젝트의 파일들에 변화가 일어나는지 감시하고, 이를 FS Model에 반영하는 역할을 한다.

 

시나리오는 대충 이렇다.

 

1. 소스코드에 변화가 일어난다.

2. FS Watcher는 이를 감지하여, FS Model의 API를 이용해 FS Model을 업데이트한다.

3. FS Model은 변경 사항이 생기면 이벤트를 발생시킨다.

4. 카르마 웹 서버는 FS Model의 변경 이벤트를 Listen하고 있다가, 변경 이벤트가 발생하면 context.html을 재생성한다.

5. 그리고 config에 restartOnFileChange가 true인 경우, 웹 소켓 커넥션을 통해 클라이언트 페이지에게 테스트 재시작을 지시한다.

 

이는 매번 소스코드에 변경이 일어날때마다 reload를 위해 해야 했던 많은 번거로운 작업들을 줄여준다.

진정한 의미의 테스트 자동화라 할 수 있겠다.

 

 

IFrame 기반 테스트

설명을 위해 위의 시나리오를 이어나가 보겠다.

 

6. 서버로부터 테스트 실행 지시를 받은 클라이언트 페이지는 iframe을 연다.

7. 그리고 서버에 context.html 을 요청해서 받아와, iframe 안에 페이지를 생성한다.

 

context.html테스트 프레임워크 어댑터소스코드(테스트의 대상), 그리고 테스트코드를 포함한다.

즉, 테스트에 필요한 모든 요소가 iframe 안에 들어간다고 보면 된다.

(테스트 프레임워크 어댑터는 밑에서 다시 설명하겠음.)

 

테스트 실행 명령이 떨어질 때마다, 클라이언트 페이지는 이 iframe을 다시 로드한다.

즉, 서버는 매번 바뀐 내용을 감지하여 context.html을 생성할테고, 바뀐 내용을 가지고 테스트를 실행하게 되는 것이다.

 

8. context.html 페이지는 로드가 끝나면, onload 이벤트를 발생시킨다.

9. 컨텍스트 페이지(context.html)의 onload 이벤트를 감지한 클라이언트 페이지는, postMessage를 통해 컨텍스트 페이지(context.html)를 클라이언트 페이지에 연결(Listen 이벤트 등록)한다.

 

 

<참고1 - postMessage>

window.postMessage() 메소드는 Windows 오브젝트 사이에서 안전하게 cross-origin 통신을 할 수 있게 한다.

예로, 페이지와 생성된 팝업 간의 통신이나, 페이지와 페이지 안의 iframe 간의 통신에 사용할 수 있다.


이렇게 연결이 되고나면, 이제 남은 일은 테스트를 실행하는 것. 그리고 그 결과를 잘 보여주는 것.

 

글탐 그 일은 누가 하느냐?

컨텍스트 페이지(context.html)에 포함된 테스트 프레임워크 어댑터를 기반으로 테스트 코드가 실행되고,

테스트 프레임워크 리포터가 결과를 전달받아 예쁘게 잘 보여준다.

 

밑으로 고고.

 

 


<참고2 - 테스트 프레임워크 & 테스트 러너>

자알~ 가다가, 갑자기 '테스트 프레임워크' 하니까 갑자기 헷갈리고, 급 의욕 떨어질 것 같아서 설명한다.

아니, 카르마는 테스트 프레임워크가 아닌건가??
흠. 위에 설명을 보니, 카르마는 테스트 러너라네.
읭? 테스트 러너가 테스트 프레임워크 아녔어? 먼솔?

카르마는 테스트 러너로, 자동화된 테스트를 보다 간단하고 빠르게 만들어주는 테스트 환경 툴이다.
그리고 앞으로 언급할 테스트 프레임워크는 Mocha, Jasmine과 같이 자동화된 테스트를 지원해주는 도구이다.
테스트 러너 위에서 테스트 프레임워크를 이용한 테스트가 이뤄진다고 볼 수 있겠다.

읭? 그런데 우리는 이전에 카르마 없이도 Mocha와 같은 테스트 프레임워크로 테스트 잘만 하였는데?

그럴 것이다. 왜냐면 우리가 잘 아는 테스트 프레임워크들도 기본적으로 테스트 러너를 제공하기 때문이다.

그러나.
그 제공한다는 러너가 빈약하기 짝이 없다.

예를 들어 Mocha의 경우, Node 기반의 러너를 제공한다.
Node 테스트만 진행한다면 문제 없겠지만 브라우저 테스트의 경우에는 얘기가 다르다.
DOM 과 같은 브라우저 API는 없기 때문에 cross-browser 문제를 이 방식으로 테스트 하는 데에는 한계가 있다.

물론, 대부분의 테스트 프레임워크에서 사용하는 HTML 러너와 같은 방식으로 브라우저에서도 실행할 수 있지만, 개발자는 테스트를 실행하려면 매번 브라우저를 열고, 러너를 다시 로드해야 하는 불편함을 감수해야 한다.
우리는 이런거 안하려고 카르마 쓰는 거고.

 

테스트 프레임워크 어댑터

Mocha, Jasmine과 같은 기존의 테스트 프레임워크를 사용하려면,

해당 프레임워크를 카르마에서 사용하기 위한 어댑터가 필요하다.

 

각 테스트 프레임워크에는 테스트를 실행하고 결과를 보고하기 위한 서로 다른 API가 있다.

어댑터는 기본적으로 테스트 프레임워크와 카르마 클라이언트 매니저 API 간의 통신을 변환하는 역할을 한다.

(클라이언트 매니저 설명은 밑에 있음)

 

카르마 config 파일에 원하는 테스트 프레임워크 어댑터를 꽂아주면 된다.

물론, 당연히 해당 플러그인이 설치되어 있어야 할 것이다.

// karma.conf.js
module.exports = function(config) {
  config.set({
    basePath: '../..',
    frameworks: ['jasmine'],
    //...
  });
};

 

 

클라이언트 매니저

클라이언트 매니저 카르마 서버와 테스트 프레임워크 어댑터가 통신하기 위한 API를 제공한다.

 

읭? 

혼돈의 카오스를 막기 위해 그림을 제공하겠다.

클라이언트 구조

카르마 서버(A)   <----> 클라이언트 매니저(B) <--(iFrame)--> 테스트 프레임워크 어댑터(C) <----> 테스트 프레임워크(D)

 

매니저(B)는 서버(A)가 프레임워크(D)와 통신하기 위한 통로이고,

어댑터(C)는 매니저(B)와 프레임워크(D)가 서로 알아들을 수 있도록 통신 내용을 변환하는 통역자 역할이라고 이해하면 된다.

 

이렇게 매니저를 통해서 클라이언트 페이지로부터 테스트 수행 지시가 떨어지면,

10. 컨텍스트 페이지의 프레임워크 어댑터는 테스트를 수행 지시를 프레임워크에 전달한다.

11. 테스트 프레임워크는 테스트를 수행한다.

12. 성공/실패 여부는 이벤트를 발생시킴으로써 클라이언트 페이지로 postMessage를 통해 전달된다.

13. 클라이언트 페이지는 다시 웹 소켓을 통해 카르마 서버에 결과를 전달한다.

 

 

테스트 프레임워크 리포터

14. 서버는 클라이언트로부터 메시지를 받으면, '브라우저' 이벤트를 발생시킨다.

15. 리포터는 서버의 '브라우저' 이벤트를 감지하여 결과 데이터를 얻는다.

 

리포터는 데이터를 인쇄하거나 파일로 저장하거나, 다른 서비스로 데이터를 전달할 수도 있다.

 

여기서 중요한 점은, 어댑터와 리포터는 거의 항상 쌍으로 제공된다는 점이다.

 

카르마는 사용자가 어떤 테스트 프레임워크를 사용할지 알 수 없고, 알 필요도 없다.

해당 프레임워크의 데이터 포맷 역시 알지 못한다.

 

결과 데이터는 테스트 프레임워크에 의해 생성되는 것이고,

어댑터에 의해 전달되는 것이고,

리포터에 의해 표현되는 것이다.

 

어떤 프레임워크를 사용하느냐에 따라 어댑터와 리포터는 거의 쌍으로 결정된다고 생각하면 된다.

 

 

 

마치며...

그냥 카르마는 대충 이런거다 설명하려다보니 생각보다 깊게 들어간 것 같다.

이렇게까지 깊게 알 필요는 없었는데;

 

웹 어플리케이션 단위 테스트를 알아보다 보니, 너무나도 많은 테스팅 툴이 있는데

어떤 툴은 다른 툴을 내부적으로 사용하고 있고,

어떤 애들은 비교당하고 있고...

 

도대체 뭐가 뭔지 뒤죽박죽이여서 카르마부터 파보자, 하고 알아봤더니

전체적인 그림이 그려지는 것 같다.

 

굿굿.

하.. 그래서 난 무슨 프레임워크 쓰지.

 

 

 

 

- end 

 



뷰 최신 버전인 2.5.17 버전에서 트랜지션을 사용해보아따.


참고로 그 사용 방법은 Vue.js 공식 페이지에 기가막히게 잘 나와있다.




뷰 트랜지션을 써보다가 마주친 문제를 설명하기에 앞서, 그놈을 어떤식으로 사용하는지 알아보즈아~.




뷰(Vue) 트랜지션 사용하기


뷰 트랜지션을 이용하여, 로고 3개를 차례로 뿅뿅뿅 나타나게하는 효과를 적용해보려고 한다.


뷰 템플릿(template) 작성

 
<transition-group name="fade" v-on:after-enter="fadeNext" tag="div" class="text-center">
     <img v-show="fade[0]" key="0" class="logo" src="../assets/logo.png">
     <img v-show="fade[1]" key="1" class="logo" src="../assets/logo.png">
     <img v-show="fade[2]" key="2" class="logo" src="../assets/logo.png">
</transition-group>
  


먼저 template 부분인데, 아주아주 간단하다.


  1. 그냥 "transition-group" 이라는 태그를 사용해주면 되고, 

  2. 효과 이름을 정해서 name="내가 정한 트랜지션 이름" 의 형태로 속성을 넣어주고,

  3. 자식 엘리먼트들에다가 'key'를 부여해주자.

  4. 그리고, 이 효과가 나타나게 해주는 switch 역할을 하는 v-show="내가 정한 switch 데이터 변수명" 을 추가해준다(요놈 스위치 변수는 아래 script 코드 부분에서 추가할거심).

  5. 마지막으로, 우리는 세 개의 로고가 뿅뿅뿅 차례차례 뜨도록 하는게 목적이니까, 첫번째 로고가 다 뜨면 다음 로고가 뜨도록 하는 메소드를 호출해주어야 한다. 
    요거슨 뷰에서 트랜지션 훅(hook)을 제공해주니, 그거슬 이용하면 된다. 
    사용하는 방법은 다양한데, 나는 'v-on' 디렉티브를 이용하는 방법을 선택했다.
    v-on:after-enter="내가 정한 메소드명" 형태로 집어넣어 주면 된다(훅 메소드도 아래 script 코드 부분에서 가할 것이니 인내하자).



<참고>


트랜지션 훅(hook) 메소드는 총 8가지가 있다.


v-on:before-enter
v-on:enter
v-on:after-enter
v-on:enter-cancelled

v-on:before-leave
v-on:leave
v-on:after-leave
v-on:leave-cancelled


각각이 어떤 것인지는 뷰 가이드에 두말하면 입아프게 너무나도 자세히 나와 있으니, 


조오기 위에 링크 눌러서 참고하시길.





아직까지는 아무런 효과를 적용해주지 않았으므로 걍 아래와 같은 정적인 상태가 보일 거심.









뷰 스타일(style) 코드 작성

얘가 뿅뿅 나타나도록 CSS 코드로 Fade-in 효과를 만들어보자.


  
.fade-enter {
  opacity: 0;
}
.fade-enter-active {
  transition: opacity .5s;
};
 


이 코드는, "fade" 라는 이름의 뷰 트랜지션을 정의해준다.


트랜지션 진입 시 처음 상태"fade-enter"로 정해주고,

트랜지션 효과"fade-enter-active"로 정해준다.


위 코드를 통해 "fade"라는 뷰 트랜지션은

=> 처음에는  'opacity: 0' 에서 시작해서 0.5초동안 opacity가 1로 변하는 트랜지션

이라고 이해하면 된다.




뷰 스크립트(script) 코드 작성

이제 중요한 것 2가지가 남았다.


1. 처음에 트랜지션 효과가 일어나도록 해주는 스위치를 만들어주는 것과,


2. 첫번째 엘리먼트에 트랜지션 효과가 끝났으면, 그 다음 엘리먼트에 효과가 일어나도록 하는 훅 메소드를 만들어주는 것.


 
let fadeIdx = 0

export default {
  ...
  data() {
    return {
      fade: [false, false, false]
    }
  },
  methods: {
    fadeNext: function() {
       this.fade.splice(fadeIdx++, 1, true)
    }
  },
  mounted() {
    setTimeout(this.fadeNext, 1000)
  }
}
...
 


fade 배열은 각각의 로고 이미지들의 트랜지션을 시작시켜주는 스위치들의 배열이다.


default 값이 모두 false 이므로, 어디선가 누군가가 값을 true로 바꿔주지 않으면, 트랜지션 효과는 일어나지 않는다.



fadeNext 함수다음 순서의 트랜지션 스위치를 켜는 역할을 한다.


즉, fade 배열에서, 다음 인덱스의 스위치를 켜는 역할을 한다.


인덱스 값의 default가 0이므로, 맨 처음 트랜지션도 해당 함수로 켜면 된다.


(위 코드에서는 mount가 되면, 1초 뒤에 맨 처음 로고의 트랜지션이 시작되도록 해놔씀)






결과물, 그리고...







읭...?


뭔가 이상하지 않음?


원하던 아름다운 그림이 아닐 거시다.


'by WANZARGEN' 이 위에있다가, 로고 트랜지션이 시작되어야 제자리로 내려간다...


by WANZARGEN 요거슬 괜히, 그냥, 한 번 짱인 척 할라고 넣은 것이 결코 아녀. 



그 뿐만이 아님.


얘네들이 제자리에서 뿅뿅뿅 하고 뜨는게 아니라, 하나가 생길때마다 왼쪽으로 밀려난다는...





문제점


이러한 현상이 나타나는 이유는 아주 심플하다.


뷰의 트랜지션은 기본적으로 v-if, v-show 를 사용하여 트랜지션을 시작시키도록 되어있다.



뭔말이냐,


아까 위에서 '트랜지션 스위치' 역할을 하는 변수가 있다고 했잖음?


그 친구가 true면  v-if = true  혹은  v-show = true  가 되고, 해당 엘리먼트가 화면에 보여지기 시작한다는 말이 된다.



이렇게 엘리먼트가 화면에 출력되는 때가 트랜지션이 시작되는 때라고 이해하면 된다.




요까지만 말해도 이해하는 당신, 아래 '참고2'는 제끼십쇼.





<참고2>


    • v-if

      이 친구는 그 대응 값이 false이면 DOM 트리에 아예 렌더링 자체가 안된다.


    • v-show

      이 친구는 그 대응 값이 false이면 렌더링은 되지만, 출력이 안된다.

      그 출력이 안되는 방식이 중요한데, 해당 엘리먼트의 스타일을 "display: none" 처리 해버린다.

      "display: none" 처리를 하게 되면, 해당 엘리먼트가 차지하는 영역 또한 사라지게 된다.

      해당 영역을 살리고 싶으면,  "visibility" 속성을 이용하면 된다.

      "visibility" 속성을 'hidden' 으로 설정하면 엘리먼트가 차지하는 영역은 살리되, 안보이게 할 수 있다.

      또 다른 방법으로는, "opacity" 속성 값을 0으로 두어 투명하게 하는 방법이 있다.

 



자신의 영역이 없다가, 트랜지션 스위치가 켜지는 때에 영역이 생겨버리니...


그에 따른 부작용으로 '안예쁜', '의도한 바 없는' 애니메이션이 되버린 것이다.




비교해서 보자.


원하지 않은 결과물

 이것이 내가 원한 그 것

 

 



 

로고의 영역이 없다가 생기니까 'by WANZARGEN' 문구가 위에 있다가 내려오는 것이고,


로고 이미지를 감싸고 있는 엘리먼트가 '가운데 정렬'인 상황에서,


엘리먼트들이 하나씩 늘어나니, 그에 따라 왼쪽으로 밀리는 것이다.



ㅠ_ㅠ


왜 이런걸 고려하지 않았을까.. 




솔루션


시간이 좀 걸려도, 트랜지션을 하루 이틀 쓸게 아니라서 

나는 뷰의 '커스텀 디렉티브'를 만들어 해결했다.


참고로 vuetifyjs 등 디자인 프레임웍을 이용해 해결하면 더 쉽고 빠르게 원하는 트랜지션을 만들 수 있을 것이지만..

나는 커스텀 디렉티브를 좀 더 심도있게 다뤄볼 겸 + 아주 라이트하고 특정한 상황에도 동작하는게 필요해서 직접 만들어 해결했다.


















  1. vue맨 2018.12.24 11:38

    정말 내용이 간지러운데를 제대로 긁어주시네요 효자손이세요? 다음번에는 믹스인에 대해서좀 알아보고 적어주세요 제발요 부탁입니다.

  2. 허허 2019.06.20 23:12

    열심히 하시네요 ㅎㅎ

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


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


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



스코프 체인(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




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


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

라고 설명할 수 있겠다.



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


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










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


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


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



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


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




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



  1. 소문듣고왔습니다 2018.05.24 09:50

    클로저 만드신분이세요? 감사합니다

  2. 양탕구리2 2018.05.30 14:05

    대단합니다

+ Recent posts