🗽
JavaScript 개발자라면 꼭 알아야 할 this

JavaScript에서 흔히 혼동되는 this에 대한 개념을 정리해봅니다.

Posted by 재그지그 on March 04, 2019

JavaScript


혹시라도 여러분들은 다른 프로그래밍 언어를 사용해 본 경험이 있나요? 저는 처음 배운 프로그래밍 언어가 C++이었는데, 제가 기억하고 있던 this는 클래스로부터 생성되는 인스턴스 중 현재 객체를 가리키는 것으로 알고 있었습니다.

하지만 JavaScript에서는 this의 의미가 기존에 알고 있던 것과는 좀 다르게 사용됩니다.

따라서 이번 포스트에서는 JavaScript에서 this는 무엇이며, 어떻게 사용하는지에 대해 알아보도록 하겠습니다.

그 때 그 때 달라요

dice JavaScript에서 this의 값은 결정되어 있지 않습니다. 문맥에 따라 그 값이 바뀌죠.

MDN 문서를 보시면 아래와 같은 부분을 확인할 수 있습니다.

대부분의 경우 this의 값은 함수를 호출한 방법이 결정합니다. 실행하는 중 할당으로 설정할 수 없고 함수를 호출할 때 마다 다를 수 있습니다.

이 말은, 일반적으로 JavaScript에서는 this가 무식하게 하나로 정해져 있는 게 아니라는 것입니다. 즉 this의 값이 내가 어디에서 어떻게 호출하느냐에 따라 변한다는 것!

따라서 this의 값이 달라질 수 있는 경우들을 하나씩 살펴보도록 하겠습니다.

전역 범위

전역 범위(Global context), 그러니까 JavaScript에서 가장 평범하게 일반적으로 this를 호출한다면, thiswindow라는 전역 객체를 가리킵니다(Node.js에서는 Global).

참고로 Window 객체라는 것은, 현재 실행되고 있는 JavaScript의 모든 변수, 함수, 객체, DOM 등을 포함하고 있는 객체로, 만물의 근원이 되는 객체입니다.

// 1. 전역 범위에서 호출
console.log(this); // Window {...}

함수 범위

headache 함수 내에서 this를 사용하는 것은 머리를 지끈거리게 만듭니다. 문맥을 알아야 하거든요!

하지만 this를 함수 내에서 호출한다면 현재 실행되고 있는 코드의 문맥(Context)에 따라 this가 달라집니다. 이것이 JavaScript에서 this가 직관적으로 이해하기 어려운 문법이 된 계기죠.

단순 함수 호출

// 1. 일반 함수 범위에서 호출
function outside() {
    console.log(this); // this는 window

    // 2. 함수 안에 함수가 선언된 내부 함수 호출
    function inside() {
        console.log(this); // this는 window
    }
    inside();
}
outside();

우선 가장 일반적인 방법으로 함수를 선언한 후 호출하는 경우, thisWindow 객체입니다.

함수 안에서 또 함수를 선언하더라도, this는 여전히 Window입니다.

객체의 메소드(Method)

// 1. 객체 또는 클래스 안에서 메소드를 실행한다면 this는 Object 자기 자신
var man = {
    name: 'john',
    hello: function() {
        // 2. 객체이므로 this는 man을 가리킴
        console.log('hello ' + this.name);
    }
}
man.hello(); // 3. hello john

이제부터 this가 바뀌기 시작합니다. 그 첫 번째 시작은 바로 객체(Object)나 클래스 내부에 선언된 메소드 함수입니다. MDN에서는 이렇게 정의하고 있네요.

함수를 어떤 객체의 메소드로 호출하면 this의 값은 그 객체를 사용합니다.

함수를 객체 외부에서 선언하고, 객체 안에서 호출하는 경우에도 this는 해당 객체의 this를 참조합니다. 위의 예시에 이어 아래 예시를 보세요.

// 3. 일반 함수 welcome을 선언
function welcome() {
    // 4. 여기서 this는 전역 객체 Window이므로, 만약 실행시키면 undefined가 뜸
    console.log('welcome ' + this.name);
}

// 5. man 객체의 welcome 속성에 일반 함수 welcome을 추가
man.welcome = welcome;

// 6. welcome이 man 객체에서 호출되었으므로 welcome john이 출력됨
man.welcome();

이와는 반대로, 객체의 함수를 외부에서 호출할 때 thisWindow가 됩니다.

// 7. man 객체의 thanks 속성에 함수를 선언
man.thanks = function () {
    // 8. man.thanks()를 호출하면 thanks john이 출력
    console.log('thanks ' + this.name);
}

// 8. 이 함수를 객체 외부에서 참조
var thankYou = man.thanks;

// 9. 객체 외부이므로 this가 Window 객체가 되어서 thanks (undefined)가 출력
thankYou();

// 10. 그럼 또 다른 객체에서 이 함수를 호출하면 어떻게 될까?
women = {
    name: 'barbie',
    thanks: man.thanks // 11. man.thanks 함수를 women.thanks에 참조
}

// 12. this가 포함된 함수가 호출된 객체가 women이므로 thanks barbie가 출력
women.thanks();

잠깐, 여기서 조심해야 할 점이 있습니다. 바로 메소드에서 내부 함수를 선언하는 경우는 this가 어떻게 될까요?

var man = {
    name: 'john',
    // 1. 이것은 객체의 메소드
    hello: function() {
        // 2. 객체의 메소드 안에서 함수를 선언하는 것이니까 내부 함수
        function getName() {
            // 3. 여기서 this가 무엇을 가리키고 있을까?
            return this.name;
        }
        console.log('hello ' + getName()); // 4. 내부 함수를 출력시키고
    }
}
man.hello(); // 메소드를 실행시키면 undefined가 뜬다! this는 Window였던 것

객체의 메소드에서 this가 객체를 가리키고 있던 것과는 다르게, 내부 함수에서 thisWindow 객체를 가리키고 있습니다. 내부 함수는 엄밀히 말해 메소드가 아니기 때문에, 단순 함수 호출 규칙에 따라 Window를 가리키고 있다는 점에 유의해야 합니다.

call(), apply(), bind()

그럼 위의 코드를 우리가 의도한 결과가 나올 수 있도록 할 수는 없을까요? JavaScript에서는 각가 다른 문맥의 this를 필요에 따라 변경할 수 있도록 함수를 제공합니다. call(), apply(), bind() 등이 있는데, 여기서는 call()을 한 번 사용한 예시를 보도록 하겠습니다.

// 1. 이것은 객체의 메소드
var man = {
    name: 'john',
    // 2. 객체의 메소드 안에서 함수를 선언하는 것이니까 내부 함수
    hello: function() {
        function getName() {
            // 3. 여기서 this가 무엇을 가리키고 있을까?
            return this.name;
        }
        // 4. 이번에는 call()을 통해 현재 문맥에서의 this(man 객체)를 바인딩해주었다
        console.log('hello ' + getName.call(this));
    }
}

// 이번에는 메소드를 실행시키면 john가 뜬다!
// this가 man 객체로 바인딩 된 것을 확인할 수 있다
man.hello();

콜백 함수

// 1. 콜백함수
var object = {
    callback: function() {
        setTimeout(function() {
            console.log(this); // 2. this는 window
        }, 1000);
    }
}

콜백 함수에서는 thisWindow를 가리킵니다. 객체 안에 메소드로 선언되어 있어도요.

생성자

함수 앞에 new 키워드가 붙이고 선언할 때, this를 해당 객체에 바인딩합니다.

// 1. 클래스 역할을 할 함수 선언
function Man () {
    this.name = 'John';
}

// 2. 생성자로 객체 선언
var john = new Man();

// 3. this가 Man 객체를 가리키고 있어 이름이 정상적으로 출력된다
john.name; // => 'John'

ECMAScript 6 문법인 class를 이용해 작성할 수도 있습니다.

// 1. Class Man 선언
class Man {
    constructor(name) {
        this.name = name;
    }
    hello() {
        console.log('hello ' + this.name)
    }
}

// 2. 생성자 실행
var john = new Man('John');
john.hello(); // 3. hello John 출력

여기서 주의할 점은 new 키워드를 붙이지 않을 경우 this가 해당 객체로 바인딩 되지 않기 때문에 Window 객체를 건드리는 일이 발생할 수 있습니다. 따라서 new 키워드를 꼭 써주도록 합시다.

화살표 함수(Arrow function)

실행 환경에 따라 의미가 달라지니까 머리 아프다! 도와줘요 화살표 함수!

화살표 함수는 ECMAScript 6에서 새로 추가된, 함수를 축약해서 사용할 수 있는 문법입니다.

하지만 단순히 함수를 축약해서 사용하는 것 뿐만이 아니라 this외부 스코프에서 정적으로 바인딩된 문맥(정적 컨텍스트, Lexical context)을 가진다는 특징을 갖고 있습니다. 이게 무슨 소리일까요.

우선 이번 포스트의 주제는 this이기 때문에, 정적 컨텍스트의 필요 없는 불필요한 정의를 제외하고 짧게 요약하자면 다음과 같습니다.

The name resolution depends on the location in the source code and the lexical context, which is defined by where the named variable or function is defined.

정적 컨텍스트(Lexical context)는 소스코드가 작성된 그 문맥의 실행 컨텍스트나 호출 컨텍스트에 의해 결정된다.

컨텍스트는 개념이 복잡하기 때문에, 우선은 JavaScript 코드를 실행하기 위한 변수, 함수 등의 정보를 담고 있는 환경이라고만 이해하셔도 됩니다.

즉, 정적 컨텍스트는 함수가 실행된 위치가 아닌, 정의(defined)된 위치에서의 컨텍스트를 참조한다는 이야기입니다. 이 코드가 어디서 실행되고 그런 것 따질 필요 없이, 그냥 정의된 부분에서 가까운 외부 함수의 this만 보면 됩니다.

예제 코드를 통해 이를 살펴보도록 합시다.

// 1. 화살표 함수
var obj = {
    a: this, // 2. 일반적인 경우 this는 window,
    b: function() {
      console.log(this) // 3. 메소드의 경우 this는 객체
    },
    c: () => {
        console.log(this)
        // 4. 화살표 함수의 경우 정적 컨텍스트를 가짐, 함수를 호출하는 것과 this는 연관이 없음
        // 5. 따라서 화살표 함수가 정의된 obj 객체의 this를 바인딩하므로 this는 window
    }

}

obj.b() // 6. obj
obj.c() // 7. window

일반적인 방법으로 함수를 선언(function () { ... })하면, 일반적인 함수는 함수가 실행될 때 자체적으로 this를 할당하게 되는데, 이 함수는 메소드 함수이므로 this가 메소드를 포함하는 객체로 바인딩됩니다.

하지만 화살표 함수는 this가 없기 때문에, 부모 스코프의 this를 바인딩하는데 위의 예시에서 이는 곧 Window객체를 의미합니다. 따라서 메소드로 화살표 함수를 쓰면, this를 이용한 부모 객체에 접근할 수 없습니다.

마무리

음… 그렇군… 그래서… 뭐 어쨌다고?

여기까지가 this에 대한 간단한(?) 정리였습니다. 함수 내부에서 this를 사용하는 경우에 따라 값이 바뀔 수 있다 정도로만 이해하셔도 좋다고 생각합니다. 크게 와닿지 않을 수도 있습니다. 저 역시도 JavaScript에서 this를 쓸 일이 그렇게 많지는 않았었기 때문입니다.

사실 저는 ECMAScript 6 문법인 화살표 함수에 대해서 알아보다가 this에 대해 궁금한 점이 생겨 이렇게 정리하는 글을 작성하게 되었습니다. ES6 문법과 화살표 함수는 다들 많이 쓰잖아요? 그래서 화살표 함수에서 가장 중요하다는 this바인딩을 살펴볼 겸 해서 아예 this에 대해 알아보는 시간을 가졌습니다.

물론 실행 컨텍스트, 스코프 체인 등 보다 더욱 깊은 개념을 접하고 멘붕에 빠지긴 했지만요…

마지막으로 혹시라도 이 글을 참고하시는 여러분께 더 도움이 되시라고 제가 참고한 글들을 올리며 마무리하겠습니다.

MDN 『this

자바스크립트에서 사용되는 this에 대한 설명

자바스크립트의 this는 무엇인가?