본문 바로가기

javascript/Basic

TIL no.46 - this 는 무엇인가요 : JavaScript

이번 글에서는 JavaScript 를 공부하면서 어려웠고 잘 이해하고 싶었던 this에 대해 포스팅해보도록 하겠습니다.

 

 

내가 궁금한 거 

 

1. 왜 this를 써야 하나요?

2. this 가 무엇인가요?

3. 왜 어려울까요?

4. 어떻게 써야하나요 ?

 

왜 this를 써야 하나요?

 

Java는 클래스 기반 언어입니다. Java에서 함수는 메서드이고, 메서드는 클래스에 묶여 있습니다. 그리고 인스턴스는 클래스에서 생성됩니다. 그러므로 모든 메서드는 this 키워드가 가리키는 객체가 자신이 속한 클래스의 인스턴스라고 확신할 수 있습니다.

 

하지만 JavaScript에는 클래스가 없습니다. 그래도 함수는 구동되고 인스턴스는 원하는 대로 막 만들 수 있습니다.

클래스가 없으니 기본적으로 상속기능도 없습니다. 그래서 프로토타입 메커니즘을 통한 상속을 하며 속성과 메서드는 어떤 객체든 동적으로 추가될 수 있습니다.

프로토타입에 기반한 프로그래밍은 객체 지향 프로그래밍의 스타일이며, 상속으로 알려진 동작 재사용은 프로토타입으로 제공되는 이미 존재하는 객체를 복제하는 과정을 수반합니다.

 

this는 바로 JavaScript에서 객체지향을 구현하기 위한 키워드입니다.

함수 내부에서 this 키워드는 호출 시점에서 이 함수를 property로 가지는 객체를 의미합니다.

 

this 는 무엇인가요 ?

 

this 현재 실행 문맥입니다. 실행문맥이란 말은 호출자가 누구냐는 것과 같습니다. 호출을 주체하는 '나'가 누구인지 의미하는 것과 같습니다.  호출할 때, 호출하는 함수가 객체의 메서드인지 그냥 함수인지가 중요합니다. 

메서드 뒤의 this는 메서드를 실행한 this 앞의 객체를 가리키며 함수로서 this를 호출한 경우에는 전역 객체인 window를 가리킵니다.

this의 값은 this를 사용하는 함수를 호출하는 방법에 따라 값(의미)이 바뀐다. 

그래서 this를 이해할 때는 ' 어떻게 ' 가 가장 중요한 것 같습니다. 어떻게 실행했느냐,

 

 

왜 this가 어려울까요 ?

this가 어려운 이유는 위에 정의했듯이 함수를 호출하는 방법에 따라 값이 매번 다르다는 점 때문인 것 같습니다.

정확하게 매번 같은 결과를 내는 것이 아니기때문에 헷갈리는 개념입니다.

그래서 이 this 의 사용을 쉽게 하기 위해 ES5에서는 함수를 어떻게 호출했는지에 상관없이 this 값을 설정할 수 있는 bind 메서드를 도입했고, ES6에서는 스스로의 this 바인딩을 제공하지 않는 화살표 함수를 추가했습니다.

 

 

어떻게 사용해야하나요 ?

this 는 누가 호출했느냐에 따라 결정된다는 것입니다. ES6 문법을 사용하면 this 를 사용할때 문제점을 완화할 수 있습니다. 예를들어, 서브루틴 내에서 바깥의 this 를 사용하려고 할때는 arrow function 을 이용하면 간단하게 해결할 수 있습니다.

 

어떤 함수를 메서드로서 호출하는 경우 호출 주체는 바로 함수명(프로퍼티명) 앞의 객체입니다.

점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this가 됩니다.

 

어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않습니다. this에는 호출한 주체에 대한 정보가 담긴다고 했습니다.

그런데 함수로서 호출하는 것은 호출 주체를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이기 때문에 호출 주체의 정보를 알 수 없는 것입니다. 명확한 호출 주체가 지정되어 있지 않을 때에는 전역 객체를 바라본다고 했기 때문에 함수에서의 this는 전역 객체를 가리킵니다. 그래서 this를 함수로서 호출하면 window를 가리키고 만일 strict 모드일 경우 undefined 를 나타냅니다.

 

this 라는 단어 자체가 주는 느낌적 느낌 그대로 코드를 바라보면 예상과 다른 결과가 나옵니다.

그렇지만 우리는 앞서 this가 무얼 가리키는지 배웠습니다 그쵸? 

내부 함수 역시 함수로서 호출했는지 메서드로서 호출했는지만 파악하면 this의 값을 정확히 맞출 수 있습니다.

 

그런데 함수 내부에서 this가 전역 객체를 가리키는 것은 전혀 저희가 생각하고 원하는 this의 기능이 아닌 것 같습니다.

물론 우리는 이러한 구조이지만 이 구조를 사용해서 저희의 의도에 맞게 코드를 반영해야 하겠죠?

 

 

콜백함수에서의 this 또한 무조건 이거다 라고 정의할 수 없습니다. 콜백 함수의 제어권을 가지는 함수(메서드)가 콜백 함수에서의 this를 무엇으로 할 지를 결정하며, 특별히 정의하지 않은 경우에는 기본적으로 함수와 마찬가지인 전역 객체를 바라봅니다.

 

 

 생성자 함수 내부에서의 this

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

객체 지향 언어에서는 생성자를 클래스, 클래스를 통해 만든 객체를 인스턴스 라고 합니다.

프로그래밍적으로 '생성자' 는 구체적인 인스턴스를 만들기 위한 일종의 틀입니다. 

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

 

 

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

규칙에 부합하지 않는 경우라면 이 방법들을 사용할 수 있습니다.

 

 

이 외에도 Function.prototype.call() 이나 Function.prototype.apply(),  Function.prototype.bind() 등의 수단으로 어디에서 호출되느냐에 상관없이 this를 고정시켜 버릴 수도 있다.

 

call 메서드 / apply 메서드

function add(c, d) {
  return this.a + this.b + c + d;
}

var o = {a: 1, b: 3};

// 첫 번째 인자는 'this'로 사용할 객체이고,
// 이어지는 인자들은 함수 호출에서 인수로 전달된다.
add.call(o, 5, 7); // 16

// 첫 번째 인자는 'this'로 사용할 객체이고,
// 두 번째 인자는 함수 호출에서 인수로 사용될 멤버들이 위치한 배열이다.
add.apply(o, [10, 20]); // 34

 

 

bind 메서드

func.bind(thisArg[, arg1[, arg2[, ...]]])

func.bind(someObject)를 호출하면 f와 같은 본문(코드)과 범위를 가졌지만 this는 원본 함수를 가진 새로운 함수를 생성합니다. 새 함수의 this는 호출 방식과 상관없이 영구적으로bind()의 첫 번째 매개변수로 고정됩니다.

function f() {
  return this.a;
}

var g = f.bind({a: 'azerty'});
console.log(g()); // azerty

var h = g.bind({a: 'yoo'}); // bind는 한 번만 동작함!
console.log(h()); // azerty

var o = {a: 37, f: f, g: g, h: h};
console.log(o.a, o.f(), o.g(), o.h()); // 37, 37, azerty, azerty

 

 

 

그래서 ES6에서는 이 문제를 보완하고자 this를 바인딩하지 않는 화살표 함수 (arrow function)을 도입했습니다.

화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있습니다.

하지만 ES5 환경에서는 arrow function을 사용할 수 없습니다.

 

var obj2 = { c: 'd' };
function b() {
  console.log(this);
}
b(); // Window
b.bind(obj2).call(); // obj2
b.call(obj2); // obj2 
b.apply(obj2); // obj2

 

명시적으로 this를 바꾸는 함수 메서드 삼총사 bind, call, apply를 사용하면 this가 객체를 가리킵니다.

 

다시 한 번, 정리하자면, this는 기본적으로 window이지만, 객체 메서드, bind call apply, new일 때 this가 바뀝니다. 그리고 이벤트리스너나 기타 라이브러리처럼 this를 내부적으로 바꿀 수도 있으니 항상 this를 확인해보셔야 하고요. 여러분이 선언한 function의 this는 항상 window라는 것 알아두세요. 

 

 

자, 이제 this에 대해 어느정도 알 것 같나요?

 

import React, { Component } from 'react';
import PropTypes from 'prop-types';

class Basic extends Component {
  constructor(props) {
    super(props);
    this.state = {
      hidden: false,
    };
    this.onClickButton = this.onClickButton.bind(this);
  }

  onClickButton() {
    this.setState(() => ({ hidden: true }));
  }

  render() {
    return (
      <div>
        <span>저는 {this.props.lang} 전문 {this.props.name}입니다!</span>
        {!this.state.hidden && <span>{this.props.birth}년에 태어났습니다.</span>}
        <button onClick={this.onClickButton}>숨기기</button>
      </div>
    );
  }
}

Basic.propTypes = {
  name: PropTypes.string.isRequired,
  birth: PropTypes.number.isRequired,
  lang: PropTypes.string,
};

Basic.defaultProps = {
  lang: 'Javascript',
};

export default Basic;

 

위의 코드에서 this.onClickButton  = onClickButton.bind(this); 를 보고 왜 this 를 바인딩해주는지 아시겠나요?

저도 this에 대해 공부하고 이해하기 전에는 '왜 this를 넣어주는거야' 하고 의아했을 것입니다.

하지만 ! 이제는 설명할 수 있습니다.

만약에 이 코드에서 this를 바인딩해주지 않는다면 render 메서드에서 this.onClickButton 함수를 호출했을 때 함수의 this가 window나 undefine가 되어버립니다. 따라서 에러가 발생하겠죠 ?!

 

왜 this가 window나 undefined가 되는지는 함수로서 실행되는 함수는 this가 명확히 지정하는 것이 없기 때문에 최상위에 있는 window를 반환한다고 위에 설명했습니다.

 

그리고 ES6부터 this를 바인딩하지 않고도 이와 같이 this를 지정해주는 것을 하기 위해서 arrow function이 등장했다고 했습니다.

 

onClickButton = () => {
    this.setState(() => ({ hidden: true }));
  }

 

그래서 코드를 이렇게 해주면 this를 바인딩하지 않고도 원하는 것을 가리킬 수 있겠죠 !

 

 

 

 

this를 설명하기 위해 필요한 개념들

*실행 컨텍스트

*호이스팅: 호이스팅은 함수 선언식인 경우에만 적용되며 함수 안에 변수 선언을 함수 상단으로 끌어올려서 선언하는 것을 말합니다. 이렇게 하게 된다면 변수 선언 위에서 변수를 사용한 경우 에러가 발생하지 않고 값만 정의되지 않았다는 의미로 undefined를 반환합니다.

*스코프 체인: 자기 자신의 스코프에 해당 변수가 없을 때 자신과 가장 가까운 변수 객체의 스코프 순으로 접근하는 것.

*this를 바인딩

*콜백함수: 함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라 합니다.

*유사배열객체

*arguments 객체: arguments 객체는 함수에 전달된 인수에 해당하는 Array 형태의 객체

Array 형태 란 arguments가 length 속성과 더불어 0부터 인덱스 된 다른 속성을 가지고 있지만, Array의 foreach, map 과 같은 내장 메서드를 가지고 있지 않다는 뜻입니다.