다른 언어를 하다 온 사람이 자바스크립트에서 제일 크게 데는 게 this다. 대부분의 언어에서 this는 그냥 "이 객체 자신"이라 헷갈릴 게 없다. 그런데 자바스크립트의 this는 함수가 어디 적혀 있느냐가 아니라, 어떻게 불렸느냐에 따라 값이 달라진다. 나는 이걸 모르고 "분명 이 객체를 가리켜야 하는데 왜 딴 걸 가리키냐"며 며칠을 싸웠다. 이번 편은 this가 상황마다 어떻게 정해지는지를 코드를 돌려가며 짚는다. 결과는 전부 실제로 실행해 찍은 것이다.


this는 부른 방식이 정한다. 이 한 문장이 전부다. 함수를 정의할 때 this가 정해지는 게 아니라, 부르는 순간에 정해진다. 같은 함수라도 점을 찍어 부르면 그 앞의 객체가 this가 되고, 그냥 떼어내 부르면 그게 사라진다. 이 규칙을 붙잡고 예제를 보자.


메서드로 부르면 앞의 객체가 this다. 객체의 메서드를 객체.메서드() 꼴로 부르면, 점 앞의 객체가 this가 된다.


const user = {
name: "가나",
hello() { return "안녕 " + this.name; }
};
console.log(user.hello());


실행 결과다.


안녕 가나


user.hello()로 불렀으니 점 앞의 userthis가 되고, this.name은 "가나"다. 여기까진 다른 언어와 똑같아 보인다. 문제는 이 함수를 객체에서 떼어내는 순간 벌어진다.


떼어내 부르면 this를 잃는다. 같은 함수를 변수에 담아 그냥 부르면, 점 앞에 아무 객체도 없어서 thisuser를 못 가리킨다.


const fn = user.hello;
console.log(fn());


실행 결과다.


안녕 undefined


함수 내용은 똑같은데 this.nameundefined가 됐다. fn()은 점 앞에 객체가 없는 맨 호출이라 thisuser가 아니게 됐고, 그래서 name을 못 찾는다. 함수를 콜백으로 넘길 때 이 일이 자주 터진다. 버튼 핸들러에 user.hello를 그냥 넘기면, 나중에 브라우저가 그걸 맨 호출로 불러서 this가 엉뚱해진다. 나는 이 함정을 몰라서 멀쩡한 메서드가 콜백으로 넘기면 고장 나는 걸 두고 한참을 헤맸다.


일반 함수 콜백 안에서 this가 또 끊긴다. 메서드 안에서 반복 함수를 돌리는데, 그 안쪽 일반 함수의 this는 바깥 메서드의 this와 이어지지 않는다. 옛날엔 이걸 self 같은 변수에 this를 미리 담아 우회했다.


const timer = {
seconds: 0,
start() {
const self = this;
[1, 2, 3].forEach(function() {
self.seconds += 1;
});
return this.seconds;
}
};
console.log(timer.start());


실행 결과다.


3


바깥 thisself에 담아뒀기에 안쪽 일반 함수에서 self.seconds로 올려 3이 나왔다. 만약 self 없이 안쪽에서 그냥 this.seconds를 썼다면, 그 thistimer가 아니라서 값이 안 올랐을 것이다. 이 self 우회는 예전 코드에 지겹도록 나오는데, 지금은 화살표 함수로 훨씬 깔끔하게 푼다.


화살표 함수는 this를 새로 만들지 않는다. 화살표 함수는 자기만의 this를 갖지 않고, 자기가 적힌 바깥의 this를 그대로 물려받는다. 그래서 self 트릭이 필요 없다.


const timer = {
seconds: 0,
start() {
[1, 2, 3].forEach(() => {
this.seconds += 1;
});
return this.seconds;
}
};
console.log(timer.start());


실행 결과다.


3


안쪽 화살표 함수의 this가 바깥 startthis, 즉 timer를 그대로 이어받아 seconds가 3까지 올랐다. 콜백 안에서 바깥 this를 그대로 쓰고 싶을 때 화살표 함수를 쓰는 이유가 이거다. 물려받은 this를 쓴다는 이 성질이 화살표 함수의 전부라 해도 된다.


객체 메서드를 화살표로 쓰면 오히려 망한다. 방금 성질을 거꾸로 뒤집으면 함정이 된다. 객체의 메서드 자체를 화살표 함수로 적으면, 그 this가 객체를 가리키지 않는다. 화살표는 자기를 부른 객체가 아니라, 자기가 적힌 바깥의 this를 따르기 때문이다.


const bad = {
name: "바보",
hello: () => "안녕 " + this.name
};
console.log(bad.hello());


실행 결과다.


안녕 undefined


bad.hello()로 불렀으니 thisbad일 것 같지만 아니다. 화살표라 바깥 this를 따르는데 그 바깥엔 name이 없어 undefined가 나온다. 그래서 규칙은 간단하다. 객체 메서드는 일반 함수로 적고, 그 안의 콜백은 화살표로 적는다. 나는 이걸 반대로 해서 애먼 시간을 버렸다.


call과 apply로 this를 직접 지정한다. this를 원하는 객체로 못 박아 함수를 부르는 방법도 있다. call은 인자를 하나씩 나열해 넘기고, apply는 인자를 배열로 넘긴다. 나머지는 똑같다.


function introduce(greeting, mark) {
return greeting + " " + this.name + mark;
}
const p1 = { name: "철수" };
console.log(introduce.call(p1, "안녕", "!"));
console.log(introduce.apply(p1, ["반가워", "?"]));


실행 결과다.


안녕 철수!
반가워 철수?


introduce는 어느 객체의 메서드도 아니지만, callapply로 첫 인자에 p1을 넘겨 thisp1으로 못 박았다. 그래서 this.name이 "철수"가 된다. 인자를 낱개로 주느냐 배열로 주느냐만 다르지 하는 일은 같다.


bind는 this를 고정한 새 함수를 만든다. callapply가 그 자리에서 바로 부른다면, bindthis를 못 박은 새 함수를 하나 만들어 돌려준다. 나중에 아무리 떼어내 불러도 this가 안 흔들린다. 앞에서 콜백으로 넘기면 this를 잃던 문제를 이걸로 푼다.


const bound = user.hello.bind(user);
console.log(bound());
const detached = bound;
console.log(detached());


실행 결과다.


안녕 가나
안녕 가나


bindthisuser에 고정했으니, bounddetached에 옮겨 담아 맨 호출로 불러도 this가 여전히 user다. 앞에서 fn()undefined를 뱉던 것과 대조된다. 그때 잃었던 thisbind가 붙잡아 준 셈이다.


정리하면 자바스크립트의 this는 함수가 어떻게 불렸느냐로 정해진다. 점을 찍어 메서드로 부르면 앞의 객체, 떼어내 맨 호출하면 잃는다. 일반 함수 콜백은 this가 끊기고, 화살표 함수는 바깥 this를 물려받는다. 못 박고 싶으면 callapply로 그 자리에서, bind로는 고정된 새 함수를 만들어 쓴다. "어떻게 불렀나"만 되물으면 this는 더 이상 안 무섭다.