본문 바로가기

Backend/Javascript

JavaScript - this 에 대해

자바스크립트에서 혼란스러운 개념엔 몇 가지가 있지만, 그 중 1순위를 꼽으라면 this 일 것입니다.

깊숙히는 아니지만 제가 경험해봤던 객체지향 언어에서 this 는 클래스로 생성한 인스턴스 객체를 의미합니다.

클래스에서만 사용할 수 있기 때문에 혼란스러울 부분이 없었지만, 자바스크립트는 어디에서나 this 를 사용할 수 있습니다.

이 점 때문에 JS 에서 this 가 참 혼란스러운 개념이라 생각하며, 제대로 한 번 짚고 넘어가보겠습니다.

 

- 상황에 따른 this

 

자바스크립트에서 this 는 기본적으로 실행 컨텍스트에 따라 바뀝니다.

실행 컨텍스트는 함수를 호출할 때 생성되므로, this 는 함수를 호출할 때 결정된다고 할 수 있습니다.

 

• 전역 공간

 

전역 공간에서 this 는 전역 객체를 가리킵니다.

브라우저 환경에서 전역 객체는 window, Node.js 환경에서는 global 입니다.

 

브라우저 환경
Node.js 환경

 

this 와 별 상관은 없지만 전역 컨텍스트에서의 특이점을 하나 보고 넘어가겠습니다.

 

이 부분은 사실 var 키워드에만 해당 되는 부분이지만, 전역 공간에서 전역 변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당합니다.

 

var a = 1;
const b = 1;

console.info(a); // 1
console.info(b); // 1

console.info(this.a); // 1
console.info(this.b); // undefined

console.info(global.a); // 1
console.info(global.b); // undefined

 

이 배경은 자바스크립트의 모든 변수는 특정 객체의 프로퍼티로서 동작하기 때문에, var 연산자를 통해 변수를 선언하더라도 자바스크립트 엔진은 이를 특정 객체의 프로퍼티로 인식하는 것입니다. (특정 객체 = 실행 컨텍스트의 LexicalEnvironment)

다만 ES6 에서 나온 const, let 은 전역 공간에서 전역 변수로 선언해도 전역 객체의 프로퍼티에 추가되지 않습니다. (class 도 마찬가지)

이러한 특성때문에 var 가 메모리 릭을 유발할 수 있어서 const, let 과 같은 키워드가 나오게 된 것 같습니다.

 

근래의 자바스크립트 프로그래밍에선 var 를 쓸 일이 없으니 이런게 있구나 정도로만 알고 넘어가면 되겠습니다.

 

• 메서드에서의 this

 

함수를 실행하는 가장 일반적인 방법 두 가지는 함수로서 호출하는 경우와 메서드로서 호출하는 경우입니다.

함수는 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상 객체에 대한 동작을 수행하는 독립성의 차이가 있습니다.

함수를 객체의 프로퍼티에 할당한다고 메서드가 되는 것은 아니고, 객체의 메서드로서 호출한 경우에만 메서드로 동작합니다.

 

const tmpFunc = function (x) {
  console.info(this, x);
};

tmpFunc(1); // Object[global]... 1 (함수)

const tmpObj = {
  method: tmpFunc,
};

tmpObj.method(2); // { method: [Function: tmpFunc] } 2 (메서드)

 

호출된 함수는 동일하지만 함수로서 호출되었을 때는 this 에 전역 객체가, 메서드로서 호출되었을 때는 그 객체가 되었습니다.

 

this 에는 호출한 주체에 대한 정보가 담깁니다. 어떤 함수를 메서드로서 호출하는 경우 호출 주체는 함수명 앞의 객체입니다.

위 예시에서 method 메서드를 호출한 주체는 tmpObj 가 되는 것이죠.

함수로서 호출한 경우엔 this 가 지정되지 않으므로, 전역 객체를 가리키게 되는 것입니다.

 

const obj1 = {
  outer : function () {
    console.info(this);
    const inner = function () {
      console.info(this);
    }
    
    inner();
    
    const obj2 = {
      innerMethod: inner
    };
    
    obj2.innerMethod();
  }
};

obj1.outer();

/*
obj1
global
obj2
*/

 

물론 console.info 의 출력은 조금 다르겠지만, 위에서 찍는 this 가 어떤 객체를 가리키는지만 중점으로 보면 되겠습니다.

outer 는 obj1 의 메서드로 호출되고 있기 때문에 3번째 라인에서 this 는 obj1 를 가리키게 됩니다.

이후 8번째 라인에서 inner 를 단순 함수로 호출하고 있으니 이 때 5번째 라인의 this 는 전역 객체 (global) 를 가리킵니다.

같은 inner 함수지만 14번째 라인에선 obj2 의 메서드로 호출하고 있기에 5번째 라인의 this 는 obj2 를 가리키게 됩니다.

 

즉 this 바인딩에 관해선 함수를 실행하는 주변 환경 (함수 내부인지 메서드 내부인지) 은 중요하지 않습니다.

함수로 호출되었는지, 메서드로 호출되었는지, 메서드라면 어떤 객체가 호출했는지가 중요한 것입니다.

 

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

 

별로 좋은 방법은 아니지만, 변수를 사용해서 this 를 우회할 수 있습니다.

 

const obj = {
  outer: function () {
    const inner = function () {
      console.info(this);
    };
    
    inner();
    
    const self = this;

    const inner2 = function () {
      console.info(self);
    };
    
    inner2();
  }
};

obj.outer();

/*
global
obj
*/

 

7번 라인에서 호출한 inner 함수의 this 는 전역 객체 (global) 를 가리킵니다.

outer 를 obj 의 메서드로서 호출하고 있기 때문에 9번 라인의 this 는 obj 를 가리키게 되고 이걸 self 라는 변수에 저장해 넘긴게 전부입니다. self 변수명은 정해진건 아니고 _this, that 을 쓰는 개발자도 종종 보았지만 self 가 이런 케이스에선 가장 널리 쓰이는 것 같습니다.

물론 우회라고 하기에도 뭐하지만.. 다음 소개할 방법을 알기 전까지 궁여지책으로 종종 사용하던 방법입니다.

 

• arrow function (화살표 함수)

 

ES6 에서는 함수 내부에서 this 가 전역 객체를 바라보는 문제를 보완하기 위해 this 를 바인딩하지 않는 화살표 함수를 도입했습니다.

이걸 처음 봤을 땐 이런 기능이 있는지 모르고 그저 function 표현에 비해 구문이 짧아진 장점만 있는 줄 알았던..

 

화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정을 거치지 않아 상위 스코프의 this 를 그대로 활용할 수 있습니다.

즉 화살표 함수를 사용하면 this 는 무조건 상위 스코프의 this 를 가리키게 됩니다.

 

const obj = {
  outer: function () {
    console.info(this);
    
    const inner = () => {
      console.info(this);
    };
    const inner2 = function () {
      console.info(this);
    };
    
    inner();
    inner2();
  },
  outer2: () => {
    console.info(this);
    
    const inner3 = () => {
      console.info(this);
    };
    
    inner3();
  }
};

obj.outer();
obj.outer2();

/*
obj
obj
global
global
global
*/

 

outer 는 기존 function 표현을, outer2 는 화살표 함수를 사용했고 각 내부의 inner,  inner2, inner3 모두 화살표 함수로 표현했습니다.

outer 를 obj 의 메서드로 호출하고 있기 때문에 3번 라인의 this 는 obj 를 가리키게 되고, inner 와 inner2 는 각각 함수로서 호출되고 있지만 inner 는 화살표 함수를 사용하고 있기에 this 가 상위 스코프의 this 인 obj 를 가리키게 되고 inner2 는 전역 객체 (global) 를 가리키게 됩니다.

마찬가지로 outer2 는 원래대로면 메서드로서 호출되기 때문에 this 가 obj 를 가리키지만, 화살표 함수를 사용하고 있기 때문에 상위 스코프의 this 인 전역 객체 (global) 를 가리키게 되고 inner3 또한 같은 이유로 전역 객체 (global) 를 가리키게 되는 것입니다.

 

이러한 점 때문에 몇몇 회사에선 JS 작성 style 로 기존의 function 표현 대신 무조건 화살표 함수를 쓰도록 권장하고 있습니다.

저도 개인적으로 코드에서 가독성을 가장 1순위로 선호하는 개발자이다보니 (물론 여러 팀원과 협력하는 환경에서) 코드를 읽을때마다 혼란을 유발할 수 있는 function 표현의 this 보다는, this 를 쓸거면 화살표 함수를 쓰도록 권하고 있습니다.

(아이러니하게도 JS 개발자라면 어떤 표현식이던 this 를 헷갈리지 않아야 한다고는 스스로 생각하지만 현실은 꽤 다르기에..)

 

이 외에도 call, apply 등의 메서드를 활용해 함수 호출 시 명시적으로 this 를 지정하는 방법도 있습니다.

 

• 콜백 함수의 this

 

콜백 함수도 함수이므로 기본적으로는 this 가 전역 객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this 가 될 대상을 지정한 경우 그 대상을 참조하게 됩니다.

 

setTimeout(function () {
  console.info(this); // Window
}, 1000);

[1, 2, 3].forEach(function (x) {
  console.info(this, x); // Window
});

document.body.innerHTML = '<button id="a">click</button>';
document.body.querySelector('#a').addEventListener('click', function (e) {
  console.info(this, e); // <button id="a">click</button>
});

 

setTimeout, forEach 에서의 콜백 함수는 지정된 this 가 없으므로 전역 객체 (브라우저 환경이므로 Window) 를 가리킵니다.

마지막의 경우 addEventListener 를 document.body.querySelector('#a') 에서 실행하므로 여기서의 this 는 저 DOM element 를 가리키게 됩니다.

 

• 생성자 함수의 this

 

생성자 함수는 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수입니다.

자바스크립트는 함수에 생성자로서의 역할을 같이 부여했고, new 명령어와 함께 호출하면 생성자로 동작하게 됩니다.

어떤 함수가 생성자 함수로서 호출된 경우, 내부의 this 는 인스턴스 자신이 됩니다.

 

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

 

• call 메서드

 

Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

 

call 메서드는 주어진 첫 번째 인자값을 this 로 바인딩하고 전달된 인자와 함께 함수를 호출합니다.

함수를 그냥 실행하면 this 는 전역 객체를 참조하지만 call 메서드를 이용하여 원하는 객체를 this 로 지정할 수 있습니다.

 

const tmpFunc = function (a, b, c) {
  console.info(this, a, b, c);
};

tmpFunc(1, 2, 3); // global... 1 2 3
tmpFunc.call({ id: 1 }, 1, 2, 3); // { id: 1 } 1 2 3

 

• apply 메서드

 

Function.prototype.apply(thisArg[, argsArray])

 

apply 메서드는 call 메서드와 기능적으로 완전히 동일하나 매개 변수를 직접 받느냐 배열로 받느냐의 차이만 있습니다.

 

const tmpFunc = function (a, b, c) {
  console.info(this, a, b, c);
};

tmpFunc.apply({ id: 1 }, [1, 2, 3]); // { id: 1 } 1 2 3

 

이미 화살표 함수를 봤기에 this 를 명시적으로 바인딩 하는 call, apply 메서드를 왜 알아야 하는가 싶을 수 있습니다.

이 메서드들은 단순 this 바인딩 뿐만 아니라 여러 용도로 사용하여 자바스크립트를 더욱 다채롭게 만들 수 있습니다. 물론 저도 이 메서드들을 거의 사용하지 않지만 간혹 외부 모듈의 코드를 직접 까봐야 할 때 call, apply 함수가 꽤 여러군데 사용된 것을 자주 접할 수 있습니다.

이러한 call, apply 메서드의 활용은 다른 포스팅으로 올려보도록 하겠습니다.

 

• bind 메서드

 

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])

 

bind 는 ES5 에서 추가되었는데, call 과 비슷하지만 함수를 호출하진 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하는 메서드입니다.

bind 메서드는 함수에 this 를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 갖고 있습니다.

 

const tmpFunc = function (a, b, c) {
  console.info(this, a, b, c);
};

tmpFunc(1, 2, 3); // gloabl... 1 2 3

const bindFunc = tmpFunc.bind({ id: 1 });
bindFunc(5, 6, 7); // { id: 1 } 5 6 7

const bindFunc2 = tmpFunc.bind({ id: 1 }, 5, 6);
bindFunc2(8); // { id: 1 } 5 6 8
bindFunc2(10); // { id: 1 } 5 6 10

 

7번 라인에서 bind 를 통해 bindFunc 에는 this 를 { id: 1 } 로 지정한 함수가 담기게 됩니다.

10번 라인에선 bindFunc2 에 this 를 지정하고 두 개의 인수를 지정한 새로운 함수를 담았습니다.

this 를 미리 적용함과 동시에 부분 적용 함수를 구현한 것입니다.

 

부분 적용 함수는 논외로 두고 React 에서 hook 이 나오기 전 버전에선 (v15) class 형 컴퍼넌트를 사용하면서 함수 바인딩 시에 bind 메서드를 많이 사용했습니다.

 

부분 적용 함수를 보니 자바스크립트에서의 커링 함수도 생각나는데, 이 부분 또한 다른 포스팅에서 따로 다뤄보도록 하겠습니다.

 

• name 프로퍼티

 

위에서 bind 메서드를 적용해 만든 함수는 name 프로퍼티에 'bound' 라는 bind 의 수동태가 prefix 로 붙게 됩니다.

 

const tmpFunc = function (a, b) {
  console.info(this, a, b);
};

const bindFunc = tmpFunc.bind({ id: 1 });

console.info(tmpFunc.name); // tmpFunc
console.info(bindFunc.name); // bound tmpFunc

 

디버깅 시 어떤 함수의 name 이 'bound xxx' 라면 원본 함수에 bind 메서드를 적용한 함수라는 뜻이므로 코드를 추적하기 용이할 수 있습니다.

 

- Summary

 

• 전역 공간에서의 this 는 전역 객체를 참조한다.

• 어떤 함수를 메서드로서 호출한 경우 this 는 메서드 호출 주체를 참조하고, 함수로서 호출한 경우 this 는 전역 객체를 참조한다.

• 콜백 함수 내부에서 this 는 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 그렇지 않은 경우 전역 객체를 참조한다.

• 생성자 함수의 this 는 생성될 인스턴스를 참조한다.

• call, apply, bind 메서드를 통해 this 를 명시적으로 지정할 수 있다.