본문 바로가기
javascript

[javascript] this

by 박연호의 개발 블로그 2020. 5. 4.

 

 

자바스크립트에서 가장 혼란스러운 개념을 고르라고 하면 많은 사람들이 망설임 없이 this를 꼽을 것입니다. 다른 대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미합니다. 자바스크립트에서의 this는 어디서든 사용할 수 있고 상황에 따라 this가 바라보는 대상이 달라지는데 어떤 이유로 그렇게 되는지를 파악하기 힘든 경우도 있고 예상과 다르게 엉뚱한 대상을 바라보는 경우도 있습니다.

 

함수와 객체(메서드)의 구분이 느슨한 자바스크립트에서 this는 실질적으로 이 둘을 구분하는 거의 유일한 기능입니다. 이번 장에서는 상황별로 this가 어떻게 달라지는지, 왜 그렇게 되는지, 예상과 다른 대상을 바라보고 있을 경우 그 원인을 효과적으로 추적하는 방법 등을 살펴보겠습니다.


전역 공간에서의 this

자바스크립트에서 this는 기본적으로 실행 컨텐스트가 생성될 때 함께 결정됩니다. 실행 컨텍스트는 함수를 호출할 때 생성되므로, 바꿔 말하면 this는 함수를 호출할 때 결정된다고 할 수 있겠습니다. 함수를 어떤 방식으로 호출하느냐에 따라 값이 달라지는 것입니다.

 

전역 공간에서 this는 전역 객체를 가리킵ㄴ디ㅏ. 개념상 전역 컨텍스트를 생성한ㄴ 주체가 바로 전역객체 이기 때문입ㄴ디ㅏ. 전역 객체는 자바스크립트 런타임 환경에 따라 다른 이름과 정보를 가지고 있습니다. 브라우저 환경에서 전역 객체는 window, node.js 환경에서는 global입니다. 


메서드로서 호출할 때 그 메소드 내부서의 this

함수 vs 메서드

어떤 함수를 실행하는 방법은 여러가지가 있는데, 가장 일반적인 방법 두 가지는 함수로서 호출하는 경우와 메서드로서 호출하는 경우입니다. 프로그래밍 언어에서 함수와 메서드는 미리 저의한 동작을 수행하는 코드 뭉치고, 이 둘을 구분하는 유일한 차이는 독립성에 있습니다.

 

함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행합니다. 자바스크립트는 상황별로 this 키워드에 다른 값을 부여하게 함으로써 이를 구현했습니다.

 

자바스크립트를 처음 접하는 분들은 흔히 메서드를 '객체의 프로퍼티에 할당된 함수'로 이해하는데 반은 맞고 반은 틀립니다. 어떤 함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로서 무조건 메서드가 되는 것이 아니라 객체의 메서드로서 호출할 경우에만 메서드로 동작하고 그렇지 않으면 함수로 동작합니다.

 

1 var func = function(x){
2        console.log(this,x);
3 };
4 fun(1);           // window { ... }
5
6 var obj = {
7   method : func
8 }
9  obj.method(2);    // { method: [Function: func]}
10 obj['mehtod'](2)  // { method: [Function: func]}

여기서 '함수로서 호출'과 '메서드로서 호출'을 어떻게 구분할까요 ?

 

함수 앞에(.)이 있는지 여부만으로 간단하게 구분할 수 있습니다. 위의 코드에서 4번째 줄 앞에 점이 없으니 함수로서 호출한 것이고, 이때 함수를 호출한 대상은 전역객체 입니다. 9번째 줄은 method앞에 점이 있으리 메서드로 호출한 것이며 이때 method를 호출한 놈(?)은 obj 객체 입니다. 10번째 줄 처럼 메서드를 대괄호 표기법으로 호출할 수도 있습니다.

 

메서드 내부에서의 this

this에는 호출한 주제에 대한 정보가 담깁니다. 어떤 함수를 메서드로서 호출하는 경우 호출 주체는 바로 함수명(프로퍼티명) 앞의 객체입니다. 점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this가 되는 것입니다.


함수로서 호출할 때 그 함수 내부에서의 this

1. 함수 내부에서의 this

어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않습니다. this에는 호출한 주체에 대한 정보가 담긴다고 했씁니다. 그런데 함수로서 호출하는 것은 호출 주체(객체지향 언어에서의 객체)를 몇시하지 않고 개발자가 코드에 직접 관여해서 실핸한 것이기 때문에 호출 주체의 정보를 알 수 없는 것입니다. 실행컨텍스트를 활설화할 당시에 this가 지정되지 않는 경우 this는 전역객체를 바라본다고 했습니다. 따라서 함수에서의 this는 전역 객체를 가리킵니다. 더클라스 그락포드는 이를 명백한 설계상의 오류라고 지적합니다. 그 이유는 바로 이어서 설명하겠습니다.

 

2. 메서드의 내부함수에서의 this

메서드 내부에서 정의하고 실행한 함수에서의 this는 자바스크립트 초심자들이 this에 관해 가장 자주 혼란을 느끼는 지점 중 하나입니다. 앞서 소개한 '설계상의 오류'로 인해 실제 동작과 다르게 예측하곤 합니다. this라는 단어 자체가 주는 느낌적 느낌 그대로 코드를 바라보면 예상과 다른 결과가 나옵니다. 그러나 우리는 이미 어떤 함수를 메서드로서 호출할 때와 함수로서 호출할 때 this가 무엇을 가리키는지를 알고 있습니다. 내부함수 역시 이를 함수로서 호출했는지 메서드로서 호출했는지만 파악하면 this의 값을 정확히 맞출 수 있습니다. 다음 예제를 한번 보겠습니다.

1 var obj1 = {
2      outer: function(){
3           console.log(this);                 // obj1
4           var innerFunc = function(){
5               console.log(this);            // 전역객체(window), obj2.innerMethod
6           }
7           innerFunc();
8
9           var obj2 = {
10              innerMethod: innerFunc
11          };
12          obje2.innerMethod()
13       }
14 };
15 obj1.outer();

 

  1. 1번째 줄 : 객체를 생성하는데, 이때 객체 내부에는 outer라는 프로퍼티가 있으며 여기에는 익명함수가 연결됩니다. 이렇게 생성한 객체를 변수 obj1에 할당합니다.
  2. 15번째 줄 : obj1.outer를 호출합ㄴ디ㅏ.
  3. 2번째 줄 : obj1.outer 함수의 실행 컨텍스트가 생성되면서 호이스팅하고, 스코프 체인 정보를 수집하고 this를 바인딩 합니다.이 함수는 호출할 때 함수명인 outer 앞에 점(.)이 있었으므로 메서드로서 호출한 것입니다. 따라서 this에는 마지막 점 앞의 객체인 obj1이 바인딩 됩니다.
  4. 3번째 줄 : obj1 객체 정보가 출력됩니다.
  5. 4번째 줄 : 호이스팅된 변수 innerFunc는 outer 스코프 내에서만 접근할 수 있는 지역변수 입니다. 이 지역변수에 익명함수를 할당합니다.
  6. 7번째 줄 : innerFunc를 호출합니다.
  7. 4번째 줄 : innerFunc 함수의 실행 컨텍스트가 생성되면서 호이스팅, 스코프체인 수집, this 바인딩 등을 수행합니다. 이 함수를 호출할 때 함수명 앞에는 점(.)이 없습니다. 즉 함수로서 호출한 것이므로 this가 지정되지 않았고 따라서 스코프 체인상의 최상위 객체인 전역객체(window)가 바인딩 됩니다.
  8. 5번째 줄 : window 객체 정보가 출력됩니다.
  9. 9번째 줄 : 호이스팅된 변수 obj2 역시 outer 스코프 내에서만 접근하 수 있는 지역변수입니다. 여기에는 다시 객체를 할당하는데 그 객체는 innerMethod라는 프로퍼티가 있으며, 여기에는 앞서 정의한 변수 innerFunc와 연결된 익명 함수가 연결됩니다.
  10. 12번째 줄 : obj2.innerMethod를 호출합니다.
  11. 9번째 줄 : obj2.innerMethod 함수의 실행 컨텍스트가 실행됩니다. 이 함수는 호출할 때 함수명인 innerMethod 앞에 점().이 있었으므로 메서드로서 호출한 것입니다. 따라서 this에는 마지막 점 앞의 객체인 obj2가 바인딩 됩니다.
  12. 10번째 줄 : obj2 객체 정보가 출력됩니다. 

여기서 중요한 점은 this 바인딩 관해서는 함수를 실행하는 당시 주변 환경(메서드 내부인지, 함수 내부인지)은 중요하지 않고, 오직 함수를 호출하는 구문 앞에 점 또는 대괄표 표기가 있는지 없는지가 관건입니다.

 

3. 메서드의 내부 함수에서의 this를 우회하는 방법

호출 주체가 없을 때는 자동으로 전역객체를 바인딩하지 않고 호출 당시 주변 환경의 this를 그대로 상속받아 사용할 수 있다면 좋겠습니다. 그게 훨씬 자연스러울뿐더러 자바스크립트 설계상 이렇게 동작하는 편이 스코프 체인과의 일관성을 지키는 설득력 있는 방식입니다. 변수를 검색하면 우선 가장 가까운 스코프의 L.E를 찾고 없으면 상위 스코프를 탐색하듯이, this 역시 현재 컨텍스트에 바인딩된 대상이 없으면 직전 컨텍스트의 this를 바라보도록 말이죠.

 

사용자 입장에서는 어색하거나 설득력 없는 기능들이라 하더라도 그 자체를 언어가 가지는 고유한 특성으로 받아들이고 주어진 환경에 적응할 수 밖에 별다른 도리가 없습니다. 아쉽게도 ES5까지는 자체적으로 내부함수에 this를 상속할 방법이 없지만 다행히 이를 우회할 방법이 없지는 않습니다. 그 중 대표적인 방법은 바로 변수를 활용하는 것입니다.

1 var obj = {
2    outer: function(){
3        console.log(this);
4        var innerFunc1 = function(){
5              console.log(this);
6         };
7         innerFunc1();
8
9        var self = this;
10       var innerFunc2 = function(){
11               console.log(self);
12       }
13       innerFunc2();
14    }
15 };
16 obj.outer();

outer 스코프에서 self라는 변수에 this를 저장한 상태에서 호출한 innerFunc2의 경우 self에는 객체 obj가 출력됩니다. 우회라고 할 수 없을 만큼 간단한 방법이지만 잘 동작합니다. 그저 상위 스코프의 this를 저장해서 내부 함수에서 활용하려는 수단일뿐이므로 의미만 통한다면 변수명으로 무엇으로 정해도 상관없습니다.

 

4. this를 바인딩 하지 않는 함수

ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자 this를 바인딩하지 않는 화살표 함수(arrow function)를 새로 도입했습니다. 화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있습니다.

1 var obj = {
2      outer: function(){
3             conosle.log(this);
4             var innerFunc = () =>{
5                 console.log(this);
6             };
7             innerFunc();
8       }
9 }
10 obj.outer();

콜백 함수 호출 시 그 함수 내부에서의 this

함수 A의 제어권을 다른 함수(또는 메서드)B에게 넘겨주는 경우 함수 A를 콜백 함수라 합니다. 이때 함수  A는 함수 B의 내부 로직에 따라 실행되며, this 역시 함수 B 내부로직에서 정한 규칙에 따라 값이 결정됩니다. 콜백 함수도 함수이기 때문에 기본적으로 this가 전여 객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지쟁한 경우에는 그 대상을 참조하게 됩니다.

 

1 setTimeout(function() {console.log(this); },300);                   // (1)
2 
3 [1,2,3,4,5].forEach(function(x) {                                   // (2)
4          console.log(this,x);
5 });
6
7 document.body.innerHTML += '<Button id="a">클릭</Button>';
8 document.body.querySelector('#a')
9       .addEventListener('click', function(e) {                     // (3)
10            console.log(this,e);
11     });

(1)의 setTimeout 함수와 (2)의 forEach 메서드는 그 내부에서 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않습니다. 따라서 콜백 함수 내부에서의 this는 전역객체를 참조합니다. (3)의 addEveneListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의돼 있습니다. 그러니깐 메서드명의 점(.) 앞부분이 곧 this가 됩니다. 


생성자 함수 내부에서의 this

생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는데 함수입니다. 객체지향 언어에서는 생성자를 클래스, 클래스를 통해 만든 객체를 인스턴스라고 합니다. 현실세계에서 '인간'의 공통 특성 몇 가지만 생각해보자면 직립 보행, 언어 구사, 도수 사용 등을 들 수 있겠죠. 이런 공통 송석들을 모아 인간 집합을 정의한 것이 바료 클래스이며, 각 사람들은 인간 클래스에 속한 인스턴스 입니다. 각 인스턴스들은 위에 예로든 공통점들도 있지만 저마다의 개성도 존재할 수 있습니다.

 

프로그래밍 적으로 '생성자'는 구체적인 인스턴스를 만들기 위한 일종의 틀입니다. 이 틀에는 해당 클래스의 공통 속성들이 미리 준비돼 있고, 여기에 구체적인 인스턴스의 개성을 더해 개별 인스턴스를 만들 수 있죠. 

 

자바스크립트는 함수에 생성자로서의 역할을 함께 부여했습니다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로 동작하게 됩니다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 됩니다.

 

생성자 함수를 호출(new 명령어와 함께 함수를 호출)하면 우선 생성자의 prototype 프로퍼티를 참조하는 __proto__라는 프로퍼티가 있는 객체(인스턴스)를 만들고 미리 준비된 공통 속성 및 개성을 해당 객체(this)에 부여합니다. 이렇게 해서 구체적인 인스턴스가 만들어 집니다.

 

1 var Cat = function(name,age){
2     this.bark = '야옹';
3     this.name = name;
4     this.age = age;
5 }
6
7 var choco = new Cat('초코',7);
8 var nabi = new Cat('나비',5);
9 console.log(choco, nabi);
10
11 / * 결과
12 Cat { bark: '야옹', name: '초코', age:7 }
13 Cat { bark: '야옹', name: '나비', age:5 }
14 * /

위에서 7번째 줄에서 실행한 생성자 함수 내부에서의 this는 choco 인스턴스를, 8번째 줄에서 실행한 생성자 함수 내부에서의 this는 nabi 인스턴스를 가리킴을 알 수 있습니다.


명시적으로 this를 바인딩 하는 방법

앞 절에서 상황별로 this에 어떤 값이 바인딩되는지 살펴봤지만 이러한 규칙을 깨고 this에 별도의 대상을 바인딩하는 방법도 있습니다.

 

1. call 메서드

call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령입니다. 이때 call 메서드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 합니다. 함수를 그냥 실행하면 this는 전역객체를 참조하지만 call 메서드를 이용하면 임의의 객체를 this로 지정할 수 있습니다.

1 var func = function(a,b,c){
2           console.log(this,a,b,c);
3 };
4 
5 func(1,2,3);               // window 1 2 3
6 func.call({x: 1},4,5,6);   // {x: 1} 4 5 6
7 
8 var obj = {
9     a: 1,
10    method: function(x,y) {
11            console.log(this.a,x,y);
12    }
13 }
14
15 obj.method(2,3);                  // 1 2 3
16 obj.method.call({a: 4}, 5, 6);    // 4 5 6

 

2. apply 메서드

apply메서드는 call 메서서와 기능적으로 완전히 동일합니다. call 메서드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정하는 반면, apply 메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서만 차이가 있습니다.

 

1 var func = function(a,b,c){
2            console.log(this,a,b,c);
3 }
4 func.apply({x: 1},[4,5,6]);
5
6 var obj = {
7        a:1,
8        method: function(x,y){
9                console.log(this.a,x,y); // {x: 1} 4 5 6
10       }
11 };
12 obj.method.apply({a: 4},[5,6]);        // 4 5 6

 

3. bind 메서드

bind 메서드는 ES5에서 추가된 기능으로, call과 비슷하지만 즉시 호출하지는 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드입니다. 다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록됩니다. 즉 bind 메서드는 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닙니다.

1 var func = function(a,b,c,d){
2            console.log(this,a,b,c,d);
3 };
4 func(1,2,3,4);                        // window 1 2 3 4
5 
6 var bindFunc1 = func.bind({x: 1});
7 bindFunc(5,6,7,8);                   // {x: 1} 5 6 7 8
8
9 var bindFunc2 = func.bind({x: 1},4, 5);
10 bindFunc2(6,7);                     // {x: 1} 4 5 6 7
11 bindFunc2(8,9);                     // {x: 1} 4 5 8 9

또한 bind 메서드를 적용해서 새로 만듬 함수는 한가지 독특한 성질이 있는데, bind 메서드를 사용해서 새로 생성한 함수에는 name이라는 프로퍼티가 추가됩니다. 어떤 함수의 name 프로퍼티가 'bound xxx'라면 이는 곧 함수명이 xxx인 원본 함수에 bind 메서드를 적용한 새로운 함수라는 의미가 됩니다. 

 

1 var func = function(a,b,c,d){
2            console.log(this,a,b,c,d);
3 }
4 var bindFunc = func.bind({x: 1}, 4, 5);
5 console.log(func.name);         // func
6 console.log(bindFunc.name);     // bound func

화살표 함수의 예외사항

ES6에 새롭게 도입된 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐습니다. 즉 이 함수 내부에는 this가 아예 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 됩니다.

1 var obj = {
2       outer : function() {
3             console.log(this);
4             var innerFunc = () =>{
5                 console.log(this);
6             };
7             innerFunc();
8         }
9 };

상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기

앞에서 내부 함수에서 메서드의 this를 그대로 바라보게 하기 위한 방법으로 self 등의 변수를 활용한 우회법을 소개했는데, call, apply 또는 bind를 이용하면 더 깔끔하게 처리할 수 있습니다.

1 var obj1 = {
2       outer : function(){
3               console.log(this);
4               var innerFunc = function(){
5                               console.log(this);
6               };
7               innerFunc.call(this);
8           }
9 };
10 
11 var obj2 = {
12       outer : function(){
13               console.log(this);
14               var innerFunc = function(){
15                               console.log(this);
16               }.bind(this);
17               innerFunc();
18           }
19 };

정리

1. 명시적 this 바인딩이 없는 경우

  1. 전역 공간에서늬 this는 전역객체(브라우저는 window, node.js는 global)를 참조합니다.
  2. 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체(메서드명 앞의 객체)를 참조합니다.
  3. 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조합니다. 메서드의 내부 함수에서도 같습니다.
  4. 콜백 함수 내부에서늬 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조합니다.
  5. 생성자 함수에서의 this는 생성될 인스턴스를 참조합니다.

2. 다음은 명시적 this 바인딩이 있는 경우

  1. call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출합니다.
  2. bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듭니다.
  3. 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받기도 합니다.

참고 : 코어 자바스크립트(Core JavaScript), 정재남

'javascript' 카테고리의 다른 글

[javascript] 클래스  (1) 2020.05.11
[javascript] 클로저  (0) 2020.05.08
[javascript] 실행 컨텍스트  (0) 2020.05.02
[javascript] undefined와 null  (0) 2020.05.02
[javascript] 데이터 타입  (0) 2020.05.01