탱구탱구 개발자 일기

자바스크립트를 다루면서 this에 대해서 난감했던 경험이 많다. 백엔드 Java에서 this를 사용할 때는 생성자를 만들 때나 사용했고 보통 IDE를 사용해 코드 작성의 도움을 받거나 요즘은 Spring 프레임워크나 Lombok 플러그인에서 제공하는 어노테이션을 붙여서 아예 생성자 코드를 구경하기도 어렵다. 근데 프론트엔드의 경우 개발하다 보니 this 요녀석으로 코드가 흘러가는 것을 보고 우울했던 적이 많다.

 

this로 불러오고 this.xxx로 된 코드를 대입하는 등..

 

결론부터 말하자면 개발할 때 잘 모르겠으면 그때 그때마다 this를 console.log()로 찍어보는 게 맘 편하다.

그래도 아래와 같이 요약했다.

 

  1. 생성자 함수함수 리터럴의 메서드에서 선언된 this : 해당 함수를 호출한 객체
  2. 1번을 제외한 모든 함수(내부함수 or 중첩 함수, 콜백 함수 포함)에서 선언된 this : 전역 객체 window
  3. apply, call, bind 메서드로 호출한 함수 : 해당 메서드 첫 번째 인수에 바인딩된 객체
  4. 화살표 함수(Arrow Function) 안의 this : 화살표 함수 바깥의 this (상위 스코프의 this)
  5. 이벤트 리스너 안의 this : 함수를 호출할 때 그 함수가 속해 있던 객체

이 포스팅에서는 4번과 5번은 다루지 않고 분리해서 설명하겠다. 4번에 대해서는 여기에서, 5번에 대해서는 여기로 들어가면 확인할 수 있다.

 

[this]

실행 컨텍스트(global, function or eval)의 프로퍼티는 비 엄격 모드에서 항상 객체를 참조, 엄격 모드(strict)에선 어떠한 값이든 될 수 있다고 말한다.

 

이론이라 무슨말인지 잘 와 닿지 않아서 코드로 살펴보았다.

// 브라우저 콘솔창에 this;
this;
Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}

 

아래와 같이 window 객체를 반환한다. 이때 window 객체라는 건 웹 브라우저의 창을 나타내는 것을 말한다.

이때 웹 브라우저 창 안에는 다양한 것들이 포함되는 데 다음과 같은 요소들을 말한다.

  • 브라우저의 요소 (주소창, 즐겨찾기, 스크롤 등..)
  • JavaScript 엔진
  • 모든 변수

이렇듯 window 객체는 모든 것을 브라우저와 관련된 모든 것을 포함하고 있고 window를 통해 다양한 요소에 접근할 수 있다. 이러한 객체를 전역(global) 객체라고 말한다.

 

순수 자바스크립트를 이용하다 보면 document 객체를 직접 조작할 때가 생기는 데 이때 document 객체라 함은 말 그대로 문서이고 dom 객체 요소로 이루어진 웹사이트를 말한다. 이 document 객체 또한 window 객체에 포함되어 있다.

 

그렇다면 함수 내부에서 this를 호출하면 어떻게 될까?? 다음과 같이 호출하였다.

 

[직접 호출한 함수 안의 this]

// 직접 호출한 함수 내부에서 this
function test1() {
    return this;
}

test1(); // // Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}

동일하게 전역객체인 window 객체를 반환한다.

 

이 밖에도 아래와 같이 다양한 상황에서 this가 어떤 객체를 가리키는지 알아보도록 하자.

 

[객체 리터럴 안의 this]

객체 리터럴 안에서 아래와 같이 프로퍼티에 함수를 정의하는 것을 메서드라고 부른다. 이 때 b 메서드가 호출되는 실행 문맥(콘텍스트)의 디스 바인딩 컴포넌트가 가리키는 객체는 t1이다.

// 객체 리터럴 안의 this
let t1 = {
    a : "name",
    b : function () {
        return this;
    }
}
console.log(t1.b()); // {a: "name", b: ƒ} => t1 객체

 

[객체 리터럴 안의 중첩 함수의 this]

위와 비슷한 객체 리터럴 안에서 중첩 함수의 참조가 변수 y에 할당되었고 그것을 호출시켰다. 이때 중첩된 함수 y에서 this는 전역 객체인 window를 가리킨다.

// 중첩 함수 this

let x = {
    a : function () {
        console.log('x:', this); // x: {a: ƒ}
        let y = function () {
            console.log('y:', this); // y: Window{parent: Window, …}
        }
        y();
    }
}
x.a();

 

[생성자 함수 안의 this]

생성자를 통해 객체를 생성할 때도 디스 바인딩 컴포넌트가 가리키는 this는 그 생성자로 생성한 객체 즉 인스턴스다.

// 생성자 함수 안의 this
function Member(name, age) {
    console.log('this:', this); // this: Member {}
    this.name = name;
    this.age = age;
}

let m1 = new Member("준영", 30);
console.log(m1); // Member {name: "준영", age: 30}

 

[new 생성자 없이 객체 생성]

만약 new 생성자없이 그냥 객체를 생성한다면 기본적으로 this는 해당 코드 앞에 객체가 없으므로 전역 객체인 window를 가리키고 인자들은 window의 프로퍼티가 된다.

// new 생성자 없이 객체 생성 시 디스 바인딩 컴포넌트 위치
function Member(name, age) {
    console.log('this:', this); //this: Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
    this.name = name;
    this.age = age;
}

Member("준영", 30);

new 생성자 없이 객체 생성

 

이러한 문제를 방지하고자 ECMAScript 6부터 객체를 생성하는 또 다른 방법인 클래스(class)가 추가되었고 클래스로 생성된 객체는 반드시 new 생성자를 통해 만들어야 오류가 발생하지 않는다! 이 부분은 객체 기초편 클래스 부분에 정리해뒀다.

 

 

[DOM 객체에 걸린 이벤트 리스너]

dom 객체에 걸린 이벤트 리스너 메서드에서 this는 해당 리스너를 호출(등록)한 dom을 가리킨다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<script>
    window.onload = function () {
        let e1 = document.getElementById('event');
        e1.onclick = function () {
            console.log(this);
        }
    }
</script>
<body>
<h1 id="event">여기 클릭</h1>
</body>
</html>

 

[apply와 call 메서드로 호출한 함수 안에 있는 this]

아래 코드에서 apply, call 메서드의 첫 번째 인수는 introduce 함수의 this 값이다. 따라서 introduce 함수의 this는 h1 객체를 가리킨다. 이렇게 apply와 call 메서드의 첫 번째 인수는 함수 내부의 this에 바인딩할 객체를 뜻한다.

// apply와 call 메서드
function introduce(greetings, closing) {
    console.log(`${greetings}! 내 이름은 ${this.name}이고 나이는 ${this.age}살이야 ${closing}!`);
}

let h1 = {
    name: "준영",
    age: 30
};

//apply => 인수가 배열
introduce.apply(h1, ["Hello!", "Nice to Meet you"]); // Hello!! 내 이름은 준영이고 나이는 30살이야 Nice to Meet you!
introduce.apply(h1, ["Hi", "Good Luck"]); // Hi! 내 이름은 준영이고 나이는 30살이야 Good Luck!

// 유사 배열 객체도 가능
let text = {
    greetings : "hi",
    closing : "잘 부탁해"
}

introduce.apply(h1, [text.greetings, text.closing]); //hi! 내 이름은 준영이고 나이는 30살이야 잘 부탁해!

// call => 인수가 쉼표로 구분한 값의 목록
introduce.call(h1, "안녕", "잘 부탁해"); // 안녕! 내 이름은 준영이고 나이는 30살이야 잘 부탁해!
introduce.call(h1, "반가워", "잘 지내자"); // 반가워! 내 이름은 준영이고 나이는 30살이야 잘 지내자!

 

[bind 메서드로 호출한 this 객체]

아래 코드에서 introduceH1 함수를 호출하면 항상 this가 h1 객체를 가리킨다. 이처럼 bind 메서드에 넘긴 인수로 넘긴 객체는 함수 introduce의 this로 설정되고 새로운 함수를 만들어 반환한다.

apply, call 메서드와 다른 점은 bind 메서드로 this인 객체를 설정해서 새로운 함수를 정의하기 때문에 항상 this가 넘긴 객체로 정해져 있다는 점이다.

// bind 메서드
function introduce(greetings, closing) {
    console.log(`${greetings}! 내 이름은 ${this.name}이고 나이는 ${this.age}살이야 ${closing}!`);
}

let h1 = {
    name: "준영",
    age: 30
};

let introduceH1 = introduce.bind(h1);
introduceH1("잘 들어~", "한 번 잘 지내보자"); // 잘 들어~! 내 이름은 준영이고 나이는 30살이야 한 번 잘 지내보자!

 

[참고]

 

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/this

 

this

JavaScript에서 함수의 this 키워드는 다른 언어와 조금 다르게 동작합니다. 또한 엄격 모드와 비엄격 모드에서도 일부 차이가 있습니다.

developer.mozilla.org

 

이 글을 공유합시다

facebook twitter kakaoTalk kakaostory naver band
loading