본문으로 바로가기
반응형

다른 언어를 사용하다가 자바스크립트로 넘어온 경우, 'this'에 대해서 매우 혼란스러운 경험을 하게 됩니다. 대부분의 언어에서 'this'는 클래스가 인스턴스화 한 현재 객체에 대한 참조입니다. 반면, 자바스크립트에서 'this'는 일반적으로 함수를 호출하는 객체에 대한 참조입니다.

이번 글에서는 자바스크립트의 this에 대해서 많이 사용되는 케이스의 예제 위주로 다루어 보겠습니다.

 

#1. 혼돈의 카오스 - 'this'

자바스크립트의 this가 무엇을 나타내는지 결정하는 방식에 대해서 일반적으로 다음과 같이 설명하고 있습니다.

 

this는 어떻게 정의되었느냐가 아니라 
어떻게(how) 호출되었느냐에 따라 결정된다.

 

뭔 소린지 여전히 헛갈리긴 하지만 아무튼 그렇다고 합니다. 정확히 알기 위해서는 실행 컨텍스트(execution context)에 대한 이해가 필요한 것 같지만 이는 추후에 다시 다루도록 하고 여기서는 호출 방식에 따라 달라지는 'this'에 대해서 case by case로 살펴보도록 하겠습니다.

 

#2. this 파헤치기 - Case Study

1. 전역에서의 this

✅ 전역에서의 this는 기본적으로 window 객체입니다.

console.log(this); // window {...}

 

2. 일반 함수의 this

✅ 일반 함수에서의 this는 window입니다.

function whatIsThis() {
  console.log(this);
}

whatIsThis(); // window

 

함수가 호출될 때 특별히 binding 된 사항 없이 독립적으로 실행되었습니다. 이와같이 전역에서 실행된 함수 내부의 this는 window가 됩니다.

 

단, 예외 상황이 하나 있습니다.

 

✅ strict mode에서는 undefined입니다.

function whatIsThis() {
  'use strict'
  console.log(this);
}

whatIsThis(); // undefined

 

3. 객체 메서드(method)의 this

✅ 객체의 method로 호출될 때 this는 해당 객체를 가리킵니다.

 

객체 method로의 호출하는 경우와 일반 함수로 호출하는 경우의 차이를 예제를 통해서 비교해보겠습니다.

 

 

백설공주 사과 먹은 이야기

 

옛날 옛적, 사과라면 정신을 못차리는 백설공주가 살고있었습니다.

그녀에게는 두개의 사과가 있었습니다.

하나는 집에 있던 사과,

다른 하나는 

왕비님이 손수 제작한 수제 독사과 입니다.

 

백설공주는 어느 사과를 먹게 될까요?

 

 

백설공주가 사과 먹는 함수(eatAppleFn)를 어떻게 호출했느냐에 대한 차이를 살펴볼까요?

var apple = '독이 든 사과';
var home = {
  apple: '맛있는 사과',
  eatApple: eatAppleFn
}

function eatAppleFn() {	
  console.log(`백설공주가 ${this.apple}를 먹습니다.`);    
}

// (1) 객체 method 호출
home.eatApple(); // 백설공주가 맛있는 사과를 먹습니다.

// (2) 함수 직접 호출
eatAppleFn(); // 백설공주가 독이 든 사과를 먹습니다.

 

먼저 ① 객체 메서드로 실행되는 경우를 살펴보겠습니다. eatAppleFn() 함수는 어떻게 (how) 호출되었나요? 

home.eatApple()로 home의 메서드로 호출되었습니다. 객체의 메서드로 호출된 경우 this는 호출한 객체를 가리킵니다. 따라서 여기서의 this는 home이 되고 백설공주는 객체 내부의 apple, 맛있는 사과(home.apple)를 먹습니다.

 

마시쪙!

 

반면, ②와 같이 함수를 직접 호출하는 경우를 보면, eatAppleFn()은 어디에도 bind 되어있지 않고 전역에서 독립적으로 실행되었습니다. 이 경우의 this는 window를 나타냅니다. 안타깝게도 백설공주는 독이 든 사과(var apple)를 먹습니다.

 

껍질은 씻고 먹어야해요.

 

모르는 사람이 준 음식은 함부로 먹으면 안 됩니다.

 

 

위의 결과를 보면 같은 함수 eatAppleFn()를 실행하더라도 함수를 어떻게 호출했느냐에 따라 this가 참조하는 객체가 다른 것을 알수 있습니다.

 

만약 다음과 같은 방식으로 호출되었다면 결과는 어떻게 될까요?

var homeApple = home.eatApple;
homeApple(); // ???

 

일반 함수의 형태로 호출되므로 this는 window입니다.

따라서 정답은 '독사과를 먹는다' 입니다. 뭔가 오묘하네요.

 

 

◑ 번외 1. 프로토타입의 this도 호출한 객체를 가리킵니다.

var apple = '독이 든 사과';
function Home() {
  this.apple = '맛있는 사과';
}
Home.prototype.eatApple = function() {
  console.log(`백설공주가 ${this.apple}를 먹습니다.`);    
}

var home = new Home();
home.eatApple(); // 백설공주가 맛있는 사과를 먹습니다.

 

 

◑ 번외 2. 내부 함수의 this는 전역 객체 (window)를 바인딩합니다.

var apple = '독이 든 사과';
var home = {
  apple: '맛있는 사과',
  eatApple: function() {
    eatAppleFn();       
  }
}

function eatAppleFn() {	
  console.log(`백설공주가 ${this.apple}를 먹습니다.`);    
}

home.eatApple(); // 백설공주가 독이 든 사과를 먹습니다.

 

home.eatApple()의 형태로 호출하긴 했지만, 호출된 함수는 eatAppleFn()이 아닌 외부 함수입니다. 'this'를 사용하는 eatAppleFn() 함수는 바인딩 없이 일반 함수의 형태로 호출되었습니다. 따라서 this는 window...!

 

음... 

왜 그렇냐고 원리를 묻는다면 할 말은 없습니다.

JavaScript가 원래 그렇게 생겨먹은 걸로 생각할래요. 😅

(자세한건 다음 기회에...)

 

어쨌거나 백설공주는 독이 든 사과를 먹게 됩니다.

 

내부자들: 범인은 이 안에

 

무슨 공식 외우는 것 같습니다.

 

4. call, apply, bind를 이용한 호출

✅ call, apply, bind를 이용하면 'this'를 원하는 객체에 연결할 수 있습니다.

 

앞에서 내부 함수의 경우에는 this가 window를 나타내는 것을 확인했습니다. 어떻게 하면 내부 함수를 사용하면서도 this가 객체를 가리키도록 연결해 줄 수 있을까요?

 

Call (Apply), bind를 이용한 예제를 살펴보겠습니다.

var apple = '독이 든 사과';
var home = {
  apple: '맛있는 사과',
  eatApple: function() {
    eatAppleFn();       
  },
  eatAppleCall: function() {
    // 여기서의 this는 home
    eatAppleFn.call(this); 
  },  
  eatAppleBind: function() {
    // 여기서의 this는 home
    (eatAppleFn.bind(this))(); 
  }
}

function eatAppleFn() {	
  console.log(`백설공주가 ${this.apple}를 먹습니다.`);    
}

home.eatApple();     // 백설공주가 독이 든 사과를 먹습니다.
home.eatAppleCall(); // 백설공주가 맛있는 사과를 먹습니다.
home.eatAppleBind(); // 백설공주가 맛있는 사과를 먹습니다.

 

위에서 eatAppleCalleatAppleBind 메서드의 this는 home입니다. (이는 3번에서 살펴본 내용이죠?) 이 'this'를 call 또는 bind의 인자로 제공하여 eatAppleFn 함수 내에서의 this로 사용할 수 있습니다. 이제 백설공주는 home 안의 '맛있는 사과'를 먹을 수 있습니다. 

 

이와같이 call, apply, bind 등을 사용하면 내부 함수의 this를 원하는 객체로 바인딩 할 수 있습니다. 

 

◑ 번외 1. var that = this

var apple = '독이 든 사과';
var home = {
  apple: '맛있는 사과',
  eatApple: function() {       
    var that = this; // this == home        
    (function() {
      console.log(`백설공주가 ${that.apple}를 먹습니다.`); 
    })();
  }
}

home.eatApple(); // 백설공주가 맛있는 사과를 먹습니다.

 

자바스크립트 코드에서 자주 보이는 that = this (또는 self, _this...)는 결국 외부의 'this'를 내부 함수로 전달하기 위해서 사용하는 변수 선언인 것을 알 수 있습니다. 물론 bind, call 등으로 대체 할 수 있습니다.

 

5. 생성자 함수

✅ 생성자 함수로 객체를 생성할 때, 생성자 함수의 this는 새로 생성된 객체를 가리킵니다.

 

누가누가 예쁜가: 왕비가 더 나은거 같은데 -.-;;;

 

거울은 계속 백설공주가 예쁘다고 합니다.

계속되는 거울의 반항에 지친 왕비...

아부 잘하는 거울을 만들기로 결심합니다.

 

우리 왕비님은 못 만드는게 없습니다. ♡

 

 

많은 연구 끝에 드디어 내가 제일 예쁘다고 세뇌시킨 거울 (newMirror)을 생성합니다.

var sejelye = '백설공주';

function mirrorReply() {
  console.log(`세상에서 제일 예쁜 사람은 ${this.sejelye}입니다.`); 
}

// 생성자 함수
function MagicMirror(name) {  
  this.sejelye = name;
  this.reply = mirrorReply;
}

// 새로운 거울 생성
var newMirror = new MagicMirror('왕비님');

newMirror.reply(); // 세상에서 제일 예쁜 사람은 왕비님입니다.

 

new keyword를 사용하는 경우 생성자 함수 내의 this를 통해서 프로퍼티와 메서드를 추가할 수 있습니다. 생성자 함수 내의 this는 새로 생성된 객체를 가리키며 생성자 함수는 암묵적으로 this를 return합니다. 따라서 newMirror는 다음과 같은 객체가 됩니다. 

 

이제 거울이 제대로 대답하는지 테스트만 하면 되겠네요!

mirrorReply() 함수는 newMirror의 메서드로 호출되므로 3번에서 살펴본 것처럼 함수 내부의 this는 newMirror를 가리키겠죠? 새로 만든 거울에게 묻는다면 newMirror안의 'sejelye'를 찾아서 대답할 것입니다. 고로...

 

왕비님 최고! ❤

 

 

◑ 번외: ES6 클래스에서 생성자 내의 this는 생성한 인스턴스를 가리킵니다.

var sejelye = '백설공주';

class MagicMirror {  
  constructor(name) {
    this.sejelye = name;    
  }
  
  mirrorReply() {
    console.log(`세상에서 제일 예쁜 사람은 ${this.sejelye}입니다.`); 
  }
}

var newMirror = new MagicMirror('왕비님'); 

newMirror.mirrorReply(); // 세상에서 제일 예쁜 사람은 왕비님입니다.

 

6. DOM event handler에서의 this

✅ 함수를 event handler로 사용하는 경우, this는 이벤트를 발생시킨 요소로 설정됩니다. (구형 IE의 경우 일부 예외가 있는듯 하나 일단 무시하겠습니다.)

 

EventHandler에서의 this는 handler 함수를 등록한 element를 가리킵니다. 만약 외부의 this를 참조하려 한다면 앞에서 살펴본 bind 등을 이용해야 합니다. (그 외에 몇가지 방법이 있습니다만, 여기서는 다루지 않겠습니다. 여기를 참조하세요!)

 

아래 예제에는 두개의 버튼이 있습니다.

  • EventListener를 등록하면서 bind를 하지 않은 btn1
  • Event handler에 bind(this)를 사용한 btn2

 

<button id="btn1">eventHandler</button>
<button id="btn2">eventHandler_bind</button>
<script>
  function alertThis() {
    alert('this: ' + this);
  }
  document.getElementById("btn1").addEventListener("click", alertThis); // this: HTMLButtonElement
  document.getElementById("btn2").addEventListener("click", alertThis.bind(this)); // this: window
</script>

 

직접 아래 버튼을 눌러서 차이를 확인해보세요!

 

 

 

 

#3. Summary

  1. 기본적으로 this는 전역 객체(브라우저에서 window)를 참조한다. (예외: 'use strict')
  2. 일반 함수 또는 내부함수의 this는 전역 객체를 참조한다.
  3. 객체 메서드 또는 프로토타입 메서드에서 this는 해당 메서드를 호출한 객체를 참조한다.
  4. 생성자 함수 내부의 this는 생성된 객체를 참조한다.
  5. DOM event handler로 사용되는 함수 내부의 this는 이벤트가 발생한 element로 설정된다.

 

맺음말

이번 글에서는 JavaScript에서 this의 의미를 다양한 case에 대해서 살펴보았습니다. 여기서 다루지 않은 화살표 함수(Arrow Function)에서의 this는 화살표 함수 소개 글에서 다루도록 하겠습니다.

그나저나 알아서 나쁠 건 없지만, 참 복잡하긴 합니다. 뭔가 불멸의 진리라기보다는 단지 JavaScript가 그렇게 되어있기 때문에 알아야 하는 것 같은 느낌?

 

자바스크립트도 발전하고, 타입스크립트도 점점 퍼지고 있고... 클래스도 등장하고 화살표 함수도 있고...

점차 'this'가 괴롭힐 상황 자체가 줄어들지 않을까 생각하면서 글을 마무리합니다.

 

반응형