[javascript] this 에 대하여 - 1편

in kr •  7 years ago 

자바로 처음 프로그래밍을 입문한 나에게 자바스크립트의 모호함은 괴로움의 존재였다.

그러던 와중에 프로젝트로 Angular2를 알아보았고, 그와 더불어 타입스크립트의 존재도 알게 되었다.

타입스크립트의 명확한 타입설정과 객체스러움은 자바스크립트에 대한 친숙함을 전해주었다.

(자바스크립트 자체도 ES6, 7 스펙으로 진화함에 따라 점차 객체지향적으로 변하고 있으니, 앞으로 기대하는 바이다. )

Angular2 프로젝트를 하면서 자바스크립트로 구성된 라이브러리를 가져다 쓰는일이 잦아졌다. 그때마다 갈증이 생긴건 자바스크립트에 대한 보다 깊은 이해였다. 라이브러리를 쓰려면 결국 그 라이브러리에 대한 이해도가 있어야 하는데, 자바스크립트에 대한 정확한 이해가 없으니, 눈만 껌뻑이며 컴퓨터화면을 멍하니 쳐다보기 일쑤였다.

특히 this에 대한 부분은 쉽사리 머릿속에 들어오지 않았다. 요 this를 잡지 않으면 앞으로도 고생할 것이란 생각에 책을사고 구글링을 시작했다...

참고문헌의 블로그 포스트들이 잘 설명이 되어있으나 좀 더 나만의 방식으로 정리해보았다.

reference
https://muckycode.blogspot.kr/2015/04/javascript-this.html
http://huns.me/development/1407
자바스크립트 핵심가이드 - 더글라스 크락포드| 김명신 역 (한빛미디어, O'RELLY)

this를 이해하려면 Lexical Environment를 알아야 한다고 하고, 또 Lexical Environment를 이해하려면 Execution Context를 이해를 해야 한다고 한다.

공부의 묘미는 이런점이 아닌가 싶다. 하나하나 세밀한 것을 찾아나서며, 발견해나가는 재미말이다.

목차는 다음과 같다.

  • Execution Context
  • Lexical Evironmetn 타입
  • 예제분석

그럼 지금부터 알아보도록 하자.

1. Execution Context

먼저, Execution Context에 대해 조사했다.
Execution Context 은 말그대로 실행환경이다. 우리는 이제부터 자바스크립트 엔진이 어떻게 자바스크립트를 실행하는지를 알아볼 것이다.
자바스크립트 엔진은 실행가능한 코드를 만나면 Execution Context를 생성한다고 한다.
실행가능한 코드란 다음의 3가지를 말한다. Global Code, Eval Code, Function Code 이다.
(이번 포스트에서 eval code는 사용하지 않았다. with문과 더불어 별로 안좋은 방법이라고 크락포드행님께서 말씀하셨기 때문이다)
먼저, 브라우저 엔진이 스크립트 태그를 만나는 상황을 가정한다. script 태그 안에는 이런 코드가 있다고 해보자.

testObject란 객체를 만들었다. 이 객체는 thisMember 를 하나의 속성으로 가지고 있다.

그다음에 그 객체에 foo라는 함수를 지정해주었다. 이 함수는 매개변수로 2개의 숫자를 받아서 저장을 하고, 내부함수를 통해서 그 2개를 더하는 것이다.

testObject.foo(1,2) 를 실행하고,콘솔에 찍힐 값으로 다음과 같이 예상했다

3

testObject

하지만 나오는 값은 다음과 같다

NaN

Window

객체의 메소드를 사용하면 그 객체에 this가 바인딩 된다고 알고 있었는데, 이게 무슨소리?

그럼 지금부터 내부적으로 this가 어떻게 바인딩 되었길래 저렇게 값이 나왔는지 알아보자.

Execution Context가 어떻게 생성되는지 그림을 통해 확인해본다.

엔진이 그림 1과 같은 스크립트를 만나면 Global EC를 실행한다.배경이 되는 메모리가 생기는 것이다. 그 다음 실행 가능한 코드인 foo 함수를 만나므로 foo함수에 대한 EC를 생성한다.

스택구조로 쌓이는 EC

이런식으로 실행가능한 코드가 선언될 때 EC가 스택형식으로 차곡차곡 쌓이며, 실행가능한 코드가 호출되어 실행이 전부 완료되면 EC는 사라진다. (Stack 구조)

EC를 유사코드로 나타내면 위와 같이 나타낼 수 있다고 한다. 3개의 속성 모두 Object 형식으로 저장되긴 하지만 LexicalEnvironment와 VariableEnvironment는 우리가 알고있는 자바스크립트의 Object 자료형이 아니라고 한다. ThisBinding 을 제외하고 그 둘은 사용자가 접근할 수 없다고 하는데, 참고하시길.

그림 3 의 LexicalEnvironment , VariableEnvironment 속성 모두 Lexical_Environment 를 타입으로 저장하고 있다. 즉, 두 속성 모두 같은 값을 지닌다는 것인데, 차이점이 무엇일까?

값이 변할 수 있냐 없냐의 차이이다. LexicalEnvironment 속성은 값이 변할 수 있는 반면, VariableEnvironment 속성은 값이 변하지 않는다.

VE는 값이 변하지 않기때문에 실행환경이 끝나기전까지는 계속 유지될 수 있다. LE는 실행환경에따라 변할 수 있는 값이다. 만약 실행환경이 끝나고 다시 원래의 값으로 돌아가야할 필요가 생길때 무엇을 보고 판단하느냐? 바로 VE이다.

세번째 속성인 ThisBinding은 어휘 그대로 this에 어떤 것이 바인딩 되느냐이다. 이 값은 함수 선언부가 아니라 함수가 호출되는 시점에 결정된다. 내가 궁극적으로 알고자 하는 것은 바로 이부분이다. 타입은 object 형식의 타입이다.

EC의 정리 = EC는 실행환경으로써, 실행가능한 코드가 선언되어질때, 예를 들어 function(){} 과같이 문장이 나타나면 생성된다. 스택구조이며, 가장 나중에 생성된 EC가 가장 먼저 실행되고 파괴되는 것이 일반적이다.

EC는 3가지 속성이 있다. Lexical Environment, VariableEnvironment,ThisBinding 이다.

LE와 VE는 똑같이 Lexical Environment를 타입으로 가지고 있다. (LexicalEnvironment의 작명때문에 혼동스러울 수 있는데, LE와 VE는 말그대로 속성이며, 그 속성의 타입으로 주어진 Lexical Environment 는 형태일 뿐이다. ) 이 둘은 변경할 수 있느냐 없느냐의 차이점을 가지고 있다고 했다.

우리에게 중요한 ThisBinding은 this에 바인딩 되는 값을 결정한다. 그 타입은 object이다.

호출되는 시점에 결정이 된다. 다시한번 강조하지만 호출되는 시점이다. (사실상 이게 핵심)

2. Lexical Environment 타입

실행환경의 전체적 맥락만 분석하고나서는 위 예제가 저렇게 값이 나오는지 알수가 없다.

LE와 VE 값의 타입으로 쓰이는 Lexical Environment 를 분석해보자!

직역을 하면 언어적 환경 구성? 이라고 할 수 있는데, LexicalEnvironment 타입을 유사코드로 표현해보면 다음과 같다.

environment_record : 식별자(identifier)로 구분된 함수나, 변수들을 저장한다. 식별자란 예를 들어 var abc ; 가 있다고 하면, abc에 해당한다.

이 evironment_recode도 내부에 속성을 지니고 있다. DeclarativeEnvironmentRecord , ObjectEnvironmentRecord 두가지가 있다.

DeclarativeEnvironmentRecord는 실행코드 자신 내부에서 선언된 변수나 함수들을 저장한다.

ObjectEnvironmentRecord는 바인딩된 객체의 변수등을 저장한다고 한다. 즉, thisbinding 에서 바인딩된 객체값들을 가진다는 것으로, 어디에 바인딩되느냐에 따라 값이 바뀔수도 있는 것이다!!

조금씩 this 의 비밀이 풀리고 있다...

outer_environment_reference는 말그대로 외부 환경 참조로써, 현재 스코프보다 상위 스코프를 의미한다.(바로위 스코프 , 가장 근접한 부모스코프라고도 할 수 있다) - 이 속성은 this와는 크게 관계가 없고 지역변수를 찾는데 도움이 된다.

Lexical Environment 타입 정리 = Lexical Environment 타입은 실행환경을 나타내는 값의 일종의 형태(타입)이다. 크게 2가지로 나타나는데, 먼저 environment_record는 식별자로 구분된 함수나 변수들을 저장한다고 한다. 자신의 내부 식별자를 저장하는지 자신의 외부에 지정되는 변수들을 저장하는지 구분한다. this 개념에서 특별히 중요한것은 environment_record 안의 ObjectEnvironmentRecord 이다. thisBinding에서 결정된 객체를 이녀석이 담게 된다. (with문과 같이 강제로 this를 묶을때 이용된다)

결국 중요한건 thisBinding이다.

outer_environment_referance는 가장 근접한 스코프를 가리킨다.

3.예제 분석

어느정도 기반지식을 쌓았으니 예제로 돌아가보자.

첫번째 문단의 testObject 관련 코드를 실행시킨다고 했을때, 실행환경은 다음과 같이 쌓인다.

가장 먼저 생기고 가장 기반이 되는 Global EC와 그 LexicalEvironment를 분석해보자.

Global EC 가 위에 정의되어 있고, 그 아래에는 Global EC의 LE를 나타낸 것이다.

Global EC 의 LE는 environment_record 로 window가 바인딩되어 있다. 착각하면 안될 것이 웹환경에서는 global 객체가 자동적으로 window로 묶이는 것일뿐 그 둘은 별개이다.

ES5 스펙에서 'use strict'를 쓸경우 따로 명시되어 있지 않다면 environment_record이 null로 바인딩이 된다는 규칙이 있다. 'use strict'이 없다면 window 객체에 바인딩이 된다. (window 객체가 global 객체가 아님을 다시한번 강조한다.)

LE 의 두번째 속성 outer_environment_referance를 보면 null 값으로 되어있는데, Global EC가 더 이상 상위 환경이 존재하지 않는 최상위이기 때문이다.

EC의 ThisBinding은 전역객체로 바인딩되어진 window 객체이다.
다음은 Global EC 보다 위에 있는 foo 함수이다. 조금 헷갈릴 수 있으니 주의깊게 보는게 좋다.

2가지로 나눠놨는데, foo 함수가 호출되는 전 시점과 호출 된 후 시점으로 나눠놨다.

먼저, fooLexicalEnvironment를 보자.




environment_record: {
        DeclarativeEnvironmentRecord: {
            //호출 전
            value1: undefined,
            value2: undefined,
            user: undefined,
            bar: 'Function reference',

            //호출 후
            value1: 1,
            value2: 2,
            user: "kim",
            bar: 'Function reference'
        },
        ObjectEnvironmentRecord : {
            //호출 전

            //호출 후
            thisMember : "test"
        }
    }
outer_environment_referance: GlobalExecutionContext

호출 전에는 매개변수들이 정해지지 않고, ThisBinding 또한 null 값이기 때문에, 내부 식별자 값이 저장되는 곳에는 undefined 되어있다.

호이스팅을 잠깐 설명하고 가자면,호이스팅(Hoisting)이란 함수같은 것들을 정의하기도 전에 가져다 써도 된다는 그런 의미이다. 우리 예제에서도 bar 함수가 정의되기도 전에 호출되고 있다. 이는 엔진이 선언되는 식별자들을 먼저 저장하고 그 후에 그 식별자에 할당된 표현식들을 해석하기 때문이다. user 같은 경우는 호출전에는 undefined값이었다가 호출 된 후에는 'kim' 값을 가지고 있다. 사실은 호출 후가 아니라 호출전에 값이 바뀌는데, 그거까지 다 표현하면 헷갈릴거같아서 그랬다. 처음 선언될때는 식별자만 저장되서 undefined 였다가, 식별자 저장이 끝나면 그 후에 , 표현식을 통해 값을 저장시키는 것이다. bar 함수의 경우는 다르다. var bar = funciton() 형식이 아닌 함수 그대로 저장시키고 있기때문에 식별자가 바로 값을 가지고 있다. 그래서 undefined 가 아닌 함수 내용을 선언시점부터 가지고 있기때문에, 호이스팅이 가능한 것이다. 함수가 만약 표현식으로 저장되어 있다면 , 그러니까 var bar = function() 형태라면 bar가 undefined로 먼저 등록되기때문에 호이스팅이 되지 않고 에러가 난다.

outer_environment_reference는 Global EC를 참조하고 있다.
자 다시 상기시켜보자면 EC는 실행가능한 코드로 인해 생성된다고 했다. foo 함수가 testObject의 메소드이긴 하지만 그 부모 EC로 testObject가 생기지 않는 것은 객체는 실행가능한 코드가 아니기때문이다. EC와 ThisBinding은 별개다. 이것을 이해하면 this가 무엇인지 이해할 수 있을 것이다.

ThisBinding 속성은 호출된 시점에서 결정된다고 했다. foo(1,2) 와 같이 호출되어 실행되면 값이 정해진다. ThisBinding은 testObject로 바인딩이 된다. testObject가 바인딩 되면서

ObjectEnvironmentRecord 에도 testObject의 멤버인 thisMember가 생겨난다.

즉 이때부터 this를 쓸 수 있는 것이다.

마지막으로 bar 함수를 뜯어보면 왜 우리가 기대한 값이 나오지 않았는지 최종적으로 알 수 있을것이다.

barExecutionContext = {
    LexicalEnvironment: [barLexicalEnvironment],
    VariableEnvironment: [barLexicalEnvironment],
    // 호출 전
    ThisBinding: [null],

    // 호출 후
    ThisBinding: [window]

}
barLexicalEnvironment = {
    environment_record: {
        DeclarativeEnvironmentRecord: {
            //호출 전
            result : undefined,

            //호출 후
            result : NaN // === (undefined + undefined)
        },
        ObjectEnvironmentRecord : {
            //호출 전
            

            //호출 후
            // this가 window 이므로 전역변수가 있었다면 전역변수가 생겼을 것이다. 
        }
    },
    outer_environment_referance: fooExecutionContext
}

먼저 ThisBinding을 보자. 호출 전에는 당연히 null 이겠고, 어라? 호출 후에 window 객체가 바인딩 되어있다. 거의 다 왔다. 왜 window로 되었는지 알기만 하면 된다.

이유는 간단하다!!!! 바로 언어 설계상의 문제라고 한다. ^^

함수가 호출될 당시에 명시를 해주지 않으면 다짜고짜 global 객체에 바인딩 시키도록 설계되어있다고 한다. 앞서 이야기 했듯이 global 객체는 window 객체와 연결된다고 했으니까 window 객체와 연결되어 있는 것이다. 더 나아가서 만약 'use strict'를 했으면 값이 어떨까? window 객체가 아닌 null 로 묶인다고 했다. 즉, 실행은 커녕 에러 밖에 날 수가 없다.

function bar() {
        var result = this.value1 + this.value2;
        console.log(result);
        console.log(this);
    }

bar 함수를 보면 this가 window로 묶여있으면 this.value1 = undefined 이다.
this가 'use strict'로 인해 null 이 되면 null 에서 value1을 찾으려 했으므로

Uncaught TypeError: Cannot read property 'value1' of undefined
    at bar (<anonymous>:12:26)
    at Object.testObject.foo (<anonymous>:10:5)
    at <anonymous>:1:12

와 같은 에러를 만나볼수 있다.
barLexicalEnvironment 를 마저 살펴보자.
'use strict' 를 사용하지 않았다면, 호출 후 bar 함수의 result는 undefined 2개를 더했으므로
NaN 값이 되는 것이다.

outer_environment_reference값도 헷갈리지 말자! 그림 5에서 순차적으로 3개의 EC를 표시했다. this 바인딩은 전역객체로 되었지만 bar 함수는 설계상 foo함수의 내부에 있다. 그렇기때문에 가장 근접한 EC를 참조한다고 했던 outer_environment_reference 는 fooExecutionEnvironment 인 것이다.

정리

우리는 예제를 통해 EC와 LexicalEnvironment를 통해 this가 언제 무엇으로 바인딩되는 지를 알아보았다. 요지는 this가 무엇으로 바인딩(즉 호출시점) 되느냐에 따라 이 함수가 어떤 변수를 쓸 수 있는지 정해진다는 것이다. 함수 호출시점을 고려하면서 코딩을 하면 훨씬 이해하기 쉽고 유려한 프로그램을 만들 수 있을 것이다.

하지만 복잡한 감이 없잖아 있다. 이런 복잡함을 없애는 방법은 간단하다.

전역변수를 쓰지 않고, 지역변수를 위주로 쓰는 것이다.

전역변수는 나쁘다! 라는 생각을 가지면 된다.

다음 포스트에서는 -Scope Chain 을 알아본다. 스코프체인을 알면 지역변수를 어떻게 써야하는지 예제와 같은 상황에서 this를 올바르게 참조할 수 있는 방법을 제시해준다!

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

완벽하게 이해는 못했지만 2번 정독했네요. 시간 날때 더 보려고 담아갑니다. ㅎㅎ

부족한 글인데 칭찬해주셔서 감사합니다! ^^

진입장벽은 상대적으로 낮은 편이지만 언어 특유의 모호함 덕분에 늘 헤매게 되는거 같습니다. 특히 this는 진짜 보면 볼수록 햇갈리고 쓸때마다 자신이 없더라구요 ㅜㅜ 이렇게 세부사항 하나에 집착하는 진성 개발자 느낌 넘넘 좋습니다. 짱이에요! 앞으로 많이 배우러오겠습니다. 좋은 글 감사합니다. 저도 리스팀으로 담아갑니다 :)

좋게봐주셔서 감사합니다!

low단부터 파헤치는 개발자분들 좋아합니다. 나중에는 메모리 구조까지 파가면서 자바스크립팅 하시는 분도 계시더라구요 ㅎㅎ this는 늘 저를 힘들게 만드는데, 좋은 글 덕분에 많이 배워갑니다.

도움이 될수 있어 기쁘네요!

어렵네요 다시 정독해봐야 겠네요