메서드와 this에서 c.up()처럼 점(.)으로 메서드를 부르면 새 호출 칸의 this가 점 앞의 객체 c를 가리킨다고 배웠습니다. 그런데 메서드를 변수에 담아 fn()처럼 점 없이 부르거나 다른 함수의 콜백으로 넘기면, 부르는 자리에 점 앞 객체가 사라져 this가 undefined가 됩니다. 이 레슨에서는 왜 this를 잃어버리는지, 그리고 화살표 함수 () => c.up()이 어떻게 이 문제를 막아 주는지 확인합니다.
1메서드를 꺼내 점 없이 부르면 this는 undefined가 된다
메서드가 객체 안에 적혀 있으니, 변수에 꺼내 담아도 원래 객체를 기억할 것이라고 생각하기 쉽습니다. 다음 코드를 실행하면 무엇이 나올지 먼저 맞혀 보세요.
class Counter {
constructor(start) {
this.count = start;
}
up() {
this.count += 1;
return this.count;
}
}
이어서 c.up을 fn에 붙인 뒤 fn()을 부릅니다.
const c = new Counter(0);
const fn = c.up;
console.log(fn());
1이 찍힐까요, 아니면 오류가 날까요?
실행 결과 보기
main.js:6 this.count += 1; ^ TypeError: Cannot read properties of undefined (reading 'count') at up (main.js:6:5) at main.js:12:131이 찍히지 않고 6번 줄에서 TypeError가 납니다. 두 줄을 나누어 보면 까닭이 분명합니다.
const fn = c.up;은up함수를 부른 것이 아니라,up이라는 함수 값 자체에fn이름표를 하나 더 붙인 것입니다(화살표 함수와 함수 값의 규칙 3). 이때c객체와의 연결은 따라가지 않습니다.fn()을 부르는 순간에는 이름 앞에 점(.)이 없습니다. 점 앞의 객체가 없으므로 새로 열린up호출 칸의this는undefined를 가리킵니다.- 6번 줄의
this.count += 1은undefined.count를 읽으려 하므로TypeError가 나며 멈춥니다.
2화살표 함수로 감싸면 점 호출과 this를 지킨다
함수를 넘기기에서 배웠듯 함수를 다른 함수의 인자로 넘길 때도 똑같은 일이 벌어집니다. 받은 함수가 action()처럼 점 없이 부르기 때문입니다.
이때 () => c.up()처럼 화살표 함수로 한 겹 감싸면 this를 잃지 않습니다.
class Counter {
constructor(start) {
this.count = start;
}
up() {
this.count += 1;
return this.count;
}
}
이어서 화살표 함수로 감싼 safe를 만들어 부릅니다.
const c = new Counter(0);
const safe = () => c.up();
console.log(safe());
console.log(safe());
1
2왜 safe()는 잘 돌아갈까요?
safe화살표 함수는 자신이 만들어진 전역 칸의c이름표를 기억합니다(클로저: 만들어진 칸을 기억하는 함수의 규칙 7).safe()가 불리면 그 안쪽에서c.up()을 점(.)으로 부릅니다.up()을 부르는 자리에 점 앞의c가 살아 있으므로,up호출 칸의this는 언제나c객체를 가리킵니다.
또한 화살표 함수는 일반 함수와 달리 자기만의 this 이름표를 새로 만들지 않습니다. 화살표 함수 안에서 this를 쓰면 일반 변수 이름처럼 **화살표 함수가 만들어진 바깥 칸의 this**를 찾아 그대로 씁니다.
3일반 이름과 this를 찾는 규칙 비교
지금까지 배운 이름 찾기 규칙과 this 규칙을 나란히 놓고 비교해 보겠습니다.
| 찾는 대상 | 무엇이 결정하나 | 규칙 |
|---|---|---|
일반 변수 이름 (count, c) | 함수가 만들어진 칸(바깥 칸 기억) | 규칙 7 (클로저) |
일반 함수·메서드의 this | 함수를 부른 모양(점 앞 객체, 없으면 undefined) | 규칙 5 (this) |
화살표 함수 안의 this | 자기 this 없이 만들어진 바깥 칸의 this | 규칙 7 |
- 일반 변수 이름은 함수를 어디서 만들었는지만 보면 됩니다.
- 반대로 메서드의
this는 함수를 어떻게 불렀는지(c.up()인지fn()인지)를 봐야 합니다.
나중에 할 일 앱에서 화면 단추의 클릭 리스너나 타이머(setTimeout)에 객체의 메서드를 넘길 때도, card.add를 그대로 넘기지 않고 () => card.add()로 감싸 넘기는 까닭이 바로 여기에 있습니다.
예제
전달받은 함수를 두 번 실행하는 runTwice(action) 함수에 스탬프 카드의 add() 메서드를 화살표 함수로 감싸 넘겨 보겠습니다.
class StampCard {
constructor(owner) {
this.owner = owner;
this.stamps = 0;
}
add() {
this.stamps += 1;
}
}
이어서 runTwice 함수를 만들고 화살표 함수로 감싸 넘깁니다.
function runTwice(action) {
action();
action();
}
const c = new StampCard('민지');
runTwice(() => c.add());
console.log(c.stamps);
2만약 runTwice(c.add)처럼 메서드만 떼어 넘겼다면 runTwice 안의 action()에서 점 앞 객체가 없어 TypeError가 났겠지만, () => c.add()로 감싸 넘겼기 때문에 매번 점 앞의 c로 add()가 불려 도장이 2개가 됩니다.
자주 틀리는 곳
1. this가 메서드를 정의한 객체에 늘 붙어 있다고 생각함
const add = card.add;처럼 메서드를 꺼내는 순간 객체와의 연결은 남지 않습니다. 나중에 add()를 점 없이 부르면 this는 card가 아니라 undefined가 됩니다.
2. 콜백 자리에 card.add를 그대로 넘김
runTwice(card.add)나 나중에 배울 setTimeout(card.add, 100), 화면 단추의 클릭 리스너 자리에 card.add를 그대로 넘기면 this가 원래 인스턴스를 가리키지 못합니다. 콜백으로 넘길 때는 () => card.add()처럼 화살표 함수로 감싸 넘깁니다.
3. 객체 리터럴의 메서드를 화살표 함수로 적음
화살표 함수는 자기 this를 만들지 않고 바깥 칸의 this를 찾습니다. 따라서 아래처럼 일반 객체의 메서드를 화살표 함수로 적으면 item.show()를 불러도 this가 item을 가리키지 않아 undefined가 찍힙니다.
const item = {
name: '커피',
show: () => {
console.log(this?.name);
},
};
item.show();
undefined객체의 메서드는 화살표 함수가 아니라 show() { ... } 꼴로 적어야 점 앞의 객체를 this로 받습니다.
확인 문제
문제 1
다음 두 코드 (가)와 (나)를 실행했을 때 각각 무엇이 출력되는지(오류가 나면 오류 종류) 쓰세요.
// (가)
class Box {
constructor(n) {
this.n = n;
}
inc() {
this.n += 1;
return this.n;
}
}
const b = new Box(5);
const f = b.inc;
console.log(f());
// (나)
class Box {
constructor(n) {
this.n = n;
}
inc() {
this.n += 1;
return this.n;
}
}
const b = new Box(5);
const g = () => b.inc();
console.log(g());
힌트
(가)의 f()는 점 없이 불려 inc 칸의 this가 undefined가 되고, (나)의 g()는 안쪽에서 b.inc()를 점으로 부릅니다.
정답
- (가):
TypeError오류 - (나):
6
(가)는 b.inc를 꺼내 점 없이 f()로 불렀으므로 this가 undefined이고, (나)는 화살표 함수 안에서 b.inc()를 점으로 불렀으므로 this가 b를 가리켜 6을 돌려줍니다.
문제 2
callOnce에 wallet.pay()를 넘겨 잔액에서 1000원이 정상으로 빠져나가도록 빈칸 ____을 채우세요.
class Wallet {
constructor(money) {
this.money = money;
}
pay() {
this.money -= 1000;
}
}
이어서 callOnce에 콜백을 넘깁니다.
function callOnce(fn) {
fn();
}
const w = new Wallet(5000);
callOnce(() => ____);
console.log(w.money);
힌트
화살표 함수 몸에서 점(.)을 찍어 w의 pay() 메서드를 부르면 됩니다.
정답
- 빈칸:
w.pay()
class Wallet {
constructor(money) {
this.money = money;
}
pay() {
this.money -= 1000;
}
}function callOnce(fn) {
fn();
}
const w = new Wallet(5000);
callOnce(() => w.pay());
console.log(w.money);4000문제 3
다음 코드는 TypeError가 납니다. 마지막 두 줄 가운데 const step = c.up; 한 줄만 화살표 함수로 고쳐 1이 출력되게 만드세요.
class Counter {
constructor(start) {
this.count = start;
}
up() {
this.count += 1;
return this.count;
}
}
const c = new Counter(0);
const step = c.up;
console.log(step());
힌트
const step = c.up;을 const step = () => c.up();으로 바꾸면 step()을 부를 때 안쪽에서 c.up()이 점과 함께 불립니다.
정답
class Counter {
constructor(start) {
this.count = start;
}
up() {
this.count += 1;
return this.count;
}
}
const c = new Counter(0);
const step = () => c.up();
console.log(step());1컴퓨터에서 해 보기(선택) — 4절 예제의 runTwice(() => c.add());를 runTwice(c.add);로 바꿔 돌려 보세요. 몇 번 줄에서 TypeError가 나는지 확인한 뒤 다시 화살표 함수로 감싸 고쳐 보세요.