4 분 소요

bound this

자바스크립트에서 this는 크게 객체 내 메서드 내에서 this를 사용하는 경우와, 함수 내부에 사용하는 경우로 나눠볼 수 있겠다. 이 중, 객체 내 메서드 내에서 사용되는 this를 bound this라 한다.

다음은 bound this를 사용한 두 예제이다.

class User {
    name = "no name";
    shoppingBasket = [];

    constructor(name, basket) {
        this.name = name;
        this.shoppingBasket = basket;
    }

    showProfile() {
        console.log("====== 사용자 정보 ====== ");
        console.log(`이름 : ${this.name}`);
        console.log(`장바구니 : ${this.shoppingBasket}`);
        console.log("==========================");
    }
}

let userObj = new User("자바스", ["사과", "바나나"]);
userObj.showProfile();
let user = {
    name: "자바스",
    shoppingBasket: ["사과", "바나나"],
    showProfile() {
        console.log("====== 사용자 정보 ====== ");
        console.log(`이름 : ${this.name}`);
        console.log(`장바구니 : ${this.shoppingBasket}`);
        console.log("==========================");
    }
}

user.showProfile();

위 두 예제 모두 실행결과는 다음과 같다.

====== 사용자 정보 ====== 
이름 : 자바스
장바구니 : 사과,바나나
==========================

객체 내 메서드 내부에서 사용된 this는 해당 객체 자체를 가리킨다. 위 예제에서의 showProfile() 내부에 든 this들은 모두 각각 userObj, user을 가리킨다.

런타임에 결정되는 this

반면, 자바스크립트에서는 bound this 말고도 다른 방법으로도 사용할 수 있다. 함수 자체에도 내부에 this를 사용할 수 있다.

let me = {name : "자바스"};
let you = {name : "파이썬"};

function showInfo() {
    console.log(this.name);
}

me.showInfo = showInfo;
you.showInfo = showInfo;
me.showInfo();
you.showInfo();

showInfo();
자바스
파이썬
undefined

위 예제의 showInfo() 함수 내부에 this가 사용되었다. 앞서 살펴본 bound this는 객체 내 메서드 내부에서 사용되며, 이 경우 this는 해당 객체를 가리키는 것임을 확인하였다. 그렇다면 위 예제에서의 this는 결국 무엇을 가리키는 것일까?

이 경우, this는 란타임, 즉 실행 도중에 결정된다. 위 예제의 경우, 각각 다른 객체 리터럴을 할당한 두 변수 me, you에 대해, showInfo라는 동일한 프로퍼티에 그 값으로 showInfo() 함수의 참조값을 대입하였다. 그러면 me.showInfo()의 경우 this는 me를, you.showInfo()의 경우 this는 you를 가리키게 된다.

this는 이 this를 품은 함수 또는 메서드의 마침표(.) 앞의 무언가를 가리킨다.

만약, this를 사용한 함수를 그대로 호출하는 경우에는 어떻게 될까? 위 예제의 맨 마지막 줄에 showInfo() 함수를 그대로 호출한 것처럼 말이다. 이 경우에도 컨텍스트에 따라 달라진다. 위 코드를 node.js로 터미널 창에서 실행시키면 undefined가 나온다. 반면, 웹 브라우저 환경에서 실행시키면 window라는 전역 객체를 참조하게 된다.

화살표 함수와 this

화살표 함수 내부에 사용된 this는 외부 환경에 존재하는 this값을 그대로 가져온다.


let user = {
    name: "자바스",
    showInfo() {
        console.log(this);
        let arrow = () => console.log(this.name);
        arrow();
    }
};

let arrowFunc = () => {
    console.log(this.name);
    console.log(this);
    console.log(typeof this);
};
arrowFunc();

console.log("===============");
user.showInfo();
undefined
{}
object
===============
{ name: '자바스', showInfo: [Function: showInfo] }
자바스

화살표 함수는 자신만의 this를 가지지 않는다. 따라서 화살표 함수는 new 키워드와 함께 쓰여 생성자 함수로 사용될 수 없다. (”생성자 함수”는 [JS] 객체 (2) 페이지 참고) 반대로, 일반 함수는 각자 자신만의 this를 가지므로, 생성자 함수로서도 사용될 수 있다.

함수 바인딩

this의 문제점

this를 포함하는 메서드 또는 함수가 콜백으로써 사용될 때 문제점이 있다. 바로 this가 가리키는 대상이 개발자 의도와 다르게 사라진다는 점이다. setTimeout 등의 호출 스케줄러 메서드에 인자로 전달할 때 이러한 현상이 발생한다.

다음은 1초마다 숫자가 증가하는 스탑워치 예제이다. 먼저 html 코드이다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="stopwatch">
        <div id="time-data"><p></p></div>
        <input type="button" value="중단" id="timer-pause">
    </div>
    <script src="stopwatch-fail.js"></script>
</body>
</html>

다음은 위 html 문서에 연결된 자바스크립트 코드이다.

class StopWatchElement {
    passedSeconds = 0;
    stopwatchId = undefined;

    constructor() {
        this.timeDisplay = document.querySelector('#time-data > p');
        this.pauseButton = document.querySelector('#timer-pause');
    }

    tick() {
        this.timeDisplay.innerHTML = `${this.passedSeconds}`;
        this.passedSeconds += 1;
        // 아래 줄의 setTimeout을 호출할 때마다 그에 대한 타이머 식별자도 
        // 그 값이 바뀌므로, 이 값을 담는 변수에도 계속 갱신해줘야 제 기능을 한다.
        this.stopwatchId = setTimeout(this.tick, 1000);
    }
    
    executeTimer() {
        this.stopwatchId = setTimeout(this.tick, 1000);
        this.pauseButton.addEventListener('click', () => {
            clearTimeout(this.stopwatchId);
        });
    }
}

let stopwatchEle = new StopWatchElement();
stopwatchEle.executeTimer();

위 html을 실행해보면 다음의 결과를 얻는다.

예제 4-1~2 실행결과

예제 4-1~2 실행결과

위 사진을 보면, 먼저 “중단” 버튼 위에 원래 숫자가 나타나야 하는데 아무것도 없다. 또한, 개발자 도구를 보면, 에러가 발생했음을 알 수 있다. 이러한 에러가 발생한 원인은, executeTimer() 메서드 내부에서 setTimeout() 메서드를 실행할 때, 해당 메서드의 인자로 전달된 tick() 메서드 내부에 작성된 this를 개발자가 의도한 StopWatchElement 객체로 인식하지 않기 때문에 벌어진 일이다.

setTimeout 내부에 객체의 메서드가 전달될 때, 객체의 정보는 생략된다. 그로 인해 원래 객체의 맥락(컨텍스트, context)을 잃어버려 this를 제대로 인식하지 못하는 것이다.

웹 브라우저 환경의 경우, setTimeout 메서드에 전달된 함수 내부의 this는 window 전역 객체로 인식된다. 그래서 위 예제의 tick() 메서드는 setTimeout() 메서드의 인자로 전달되어 실행될 때에는 tick() 메서드 내부의 this가 StopWatchElement 객체가 아닌 전역 객체 window로 인식된다. 그리고 window에는 timeDisplay이라는 것이 정의되어 있지 않으니 window.timeDisplay는 undefined로 인식되고, 결국 this.timeDisplay.innerHTML은 undefined.innerHTML이 되기에 에러가 발생한 것이다.

bind()

이처럼 this의 정보가 사라지는 문제를 해결하는 방법은 bind() 메서드를 사용하는 것이다. bind() 메서드는 모든 함수가 기본적으로 가지고 있다. bind() 메서드에 this가 가리키고자 하는 객체를 넘기면, 해당 함수 내부에서 사용되는 this는 무조건 bind() 메서드의 인자로 넘겨진 객체를 가리키게 된다.

위 스탑워치 예제를 bind() 메서드를 이용하여 다음과 같이 수정해보았다.

class StopWatchElement {
    passedSeconds = 0;
    stopwatchId = undefined;

    constructor() {
        this.timeDisplay = document.querySelector('#time-data > p');
        this.pauseButton = document.querySelector('#timer-pause');
    }

    tick() {
        this.timeDisplay.innerHTML = `${this.passedSeconds}`;
        this.passedSeconds += 1;
        // 아래 줄의 setTimeout을 호출할 때마다 그에 대한 타이머 식별자도 
        // 그 값이 바뀌므로, 이 값을 담는 변수에도 계속 갱신해줘야 제 기능을 한다.
        this.stopwatchId = setTimeout(this.tick.bind(this), 1000); // bind
    }
    
    executeTimer() {
        this.stopwatchId = setTimeout(this.tick.bind(this), 1000); // bind
        this.pauseButton.addEventListener('click', () => {
            clearTimeout(this.stopwatchId);
        });
    }
}

let stopwatchEle = new StopWatchElement();
stopwatchEle.executeTimer();

위 예제로 코드를 수정한 후에 앞선 html 문서를 실행하면 정상적으로 숫자가 등장하며 1초마다 1씩 증가된 값으로 갱신된다.

예제 5-1 실행결과

예제 5-1 실행결과

위 예제에서는 tick() 메서드의 bind() 메서드에 this를 전달하였다. 이 때, bind() 메서드에 전달된 this는 StopWatchElement 클래스의 객체를 의미한다. 따라서, setTimeout의 인자로 전달된 tick() 메서드 내부의 this는 더 이상 window 전역 객체가 아닌 StopWatchElement 클래스의 객체를 가리키게 되어 정상적으로 작동하게 되는 것이다.


References

[1] 메서드와 this

[2] 함수 바인딩

[3] 화살표 함수 다시 살펴보기

This content is licensed under CC BY-NC 4.0

댓글남기기