자바스크립트 고급 #2 this 바인딩과 호출 패턴
자바스크립트의 this는 호출 방식에 따라 달라집니다. 다른 언어들에서는 클래스 인스턴스가 자연스럽게 this가 되는 상황에서, 자바스크립트는 호출하는 모양에 따라 결정됩니다. 이번 글은 그 규칙을 한 번에 정리합니다.
this가 결정되는 네 가지 규칙
#
호출 방식에 따라 우선순위가 있으며, 위에서부터 차례로 적용됩니다.
| 규칙 | 호출 모양 | this가 됨 |
|---|---|---|
| 1 | new Func() | 새로 만들어진 인스턴스 |
| 2 | obj.method() | obj |
| 3 | func.call(x) / apply / bind | 명시한 x |
| 4 | func() | 글로벌 객체(또는 strict에서는 undefined) |
위 네 규칙의 예외 — 화살표 함수는 이 모두를 무시하고 자기가 정의된 위치의 this를 그대로 사용합니다(잠시 뒤).
규칙 4 — 그냥 호출 #
function show() {
console.log(this);
}
show(); // strict 모드: undefined / 비-strict: globalThis (window)
가장 단순한 경우입니다. strict 모드 (기초 #2 의 let/const/모듈은 자동으로 strict)에서는 undefined, 비-strict에서는 글로벌 객체입니다.
ES Modules와 클래스 본문은 자동 strict 라서 모던 코드에서는 거의 undefined 라고 보면 됩니다.
규칙 2 — 메서드 호출 #
const obj = {
name: '커티스',
greet() {
console.log(this.name);
},
};
obj.greet(); // '커티스'
점 왼쪽의 객체가 this가 됩니다. 가장 흔한 경우입니다.
메서드를 떼면 잃음 #
이게 자바스크립트의 가장 흔한 함정입니다.
const greet = obj.greet;
greet(); // undefined (또는 글로벌)
greet 변수에는 함수 자체만 들어있습니다. 점 호출이 아니니 규칙 4가 적용되어 this가 사라집니다.
이게 콜백으로 메서드를 넘기거나 이벤트 핸들러로 등록할 때 자주 사고를 만듭니다.
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
console.log(this.count);
}
}
const c = new Counter();
button.addEventListener('click', c.increment);
// ✗ click 시 this가 button (또는 undefined)
해결법은 두 가지입니다.
// 1. 화살표 함수로 감싸기
button.addEventListener('click', () => c.increment());
// 2. bind로 묶기
button.addEventListener('click', c.increment.bind(c));규칙 3 — call / apply / bind
#
this를 명시적으로 지정하는 세 가지 메서드입니다.
call — 즉시 호출
#
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
greet.call({ name: '커티스' }, '안녕');
// 안녕, 커티스
첫 번째 인자가 this, 그 뒤가 함수 인자. 함수를 즉시 호출합니다.
apply — 인자를 배열로
#
greet.apply({ name: '커티스' }, ['안녕']);
// 안녕, 커티스
call과 같지만 인자를 배열로 받습니다. spread가 등장한 뒤로는 apply를 직접 쓸 일이 거의 없습니다 — fn(...args)가 더 짧습니다.
bind — this가 묶인 새 함수 반환
#
const boundGreet = greet.bind({ name: '커티스' });
boundGreet('안녕'); // 안녕, 커티스
boundGreet.call({ name: '다른' }, '안녕');
// 안녕, 커티스 ← bind로 묶인 this가 우선
bind는 호출하지 않고 this가 묶인 새 함수를 돌려줍니다. 일단 묶이면 그 뒤로는 어떻게 호출하든 this가 안 바뀝니다. 콜백 등록에서 자주 사용합니다.
bind로 인자도 미리 묶기
#
function add(a, b) {
return a + b;
}
const add5 = add.bind(null, 5); // this는 null, 첫 인자는 5로 묶음
add5(3); // 8
add5(10); // 15
#1 클로저 에서 본 부분 적용 패턴이 bind 로도 가능합니다.
규칙 1 — new 호출
#
function User(name) {
this.name = name;
}
const u = new User('커티스');
u.name; // '커티스'
new와 함께 호출하면 — 새 객체를 만들고, 그 객체가 this가 되고, 함수 끝에서 자동으로 그 객체를 반환합니다. 클래스 호출이 결국 이 동작입니다.
new 호출이 다른 모든 규칙보다 우선합니다.
화살표 함수 — 위 규칙을 깨는 예외 #
화살표 함수는 자기만의 this를 가지지 않습니다. 정의된 위치의 this를 그대로 가져옵니다.
const obj = {
name: '커티스',
greetRegular: function() {
console.log(this.name);
},
greetArrow: () => {
console.log(this.name); // 바깥 this — 보통 undefined
},
};
obj.greetRegular(); // '커티스'
obj.greetArrow(); // undefined (객체 밖의 this를 봄)
greetArrow는 객체 안에서 정의됐어도, 화살표 함수의 this는 객체 바깥의 환경(보통 모듈 최상위 = undefined)을 그대로 캡처합니다.
그래서 — 메서드는 일반 함수, 콜백은 화살표 #
class Counter {
constructor() {
this.count = 0;
}
increment() { // 메서드 → 일반 함수
setTimeout(() => { // 콜백 → 화살표
this.count++; // 바깥 this (Counter 인스턴스) 유지
}, 100);
}
}리액트의 hook 콜백, setTimeout/setInterval, fetch의 .then 모두 같은 패턴입니다.
화살표 함수에 bind는 무시됨
#
const arrow = () => this;
const bound = arrow.bind({ name: '커티스' });
bound(); // 여전히 바깥 this
화살표 함수는 자기 this를 갖지 않으니까 — 바인딩할 게 없습니다. bind가 조용히 무시됩니다.
자주 만나는 헷갈리는 경우 #
1) forEach 콜백
#
const obj = {
prefix: '> ',
items: ['a', 'b', 'c'],
log() {
this.items.forEach(function(x) {
console.log(this.prefix + x); // ✗ this가 obj가 아님
});
},
};forEach의 콜백이 일반 함수라 — 점 호출이 아닌 그냥 호출 이라 규칙 4 (undefined). 화살표 함수로 바꾸면 해결.
log() {
this.items.forEach((x) => {
console.log(this.prefix + x); // 바깥 this 유지
});
}또는 forEach가 받는 두 번째 인자(this 값)를 줄 수도 있습니다.
this.items.forEach(function(x) {
console.log(this.prefix + x);
}, this); // 두 번째 인자가 thisArg
화살표 함수가 등장한 뒤로는 thisArg 패턴은 거의 안 씁니다.
2) DOM 이벤트 핸들러 #
button.addEventListener('click', function(e) {
console.log(this); // 이벤트가 걸린 엘리먼트 (currentTarget)
});
button.addEventListener('click', (e) => {
console.log(this); // 바깥 (보통 undefined 또는 window)
});옛 코드의 this는 이벤트가 걸린 엘리먼트를 가리키게 의도되어 있었습니다. 화살표 함수를 쓰면 그 의도가 깨집니다. **DOM 핸들러에서 엘리먼트가 필요하면 e.currentTarget**을 쓰는 게 어느 함수 형태든 안전합니다.
this와 strict 모드
#
옛 자바스크립트의 함정을 한 가지 짚어봅니다.
function f() {
console.log(this); // window 또는 global
}
f();'use strict';
function f() {
console.log(this); // undefined
}
f();ES Modules, 클래스 본문, ES2015+ 코드는 자동으로 strict. 모던 코드에서는 this가 그냥 호출됐을 때 undefined 라고 보면 됩니다.
마무리 #
이번 글에서 정리한 내용:
this는 호출 방식에 따라 결정 — 4가지 규칙- 그냥 호출 → undefined / 메서드 호출 → 점 앞 / call,apply,bind → 명시 / new → 새 인스턴스
- 메서드를 떼서 콜백으로 넘기면
this사라짐 call/apply즉시 호출,bind는 묶인 새 함수- 화살표 함수는 자기
this없음 — 바깥 캡처 - 메서드는 일반 함수, 콜백은 화살표 (가장 흔한 가이드)
- DOM 엘리먼트 필요하면
e.currentTarget이 가장 안전
다음 글(#3 프로토타입 체인)에서는 클래스의 진짜 정체 — 프로토타입과 그 체인이 어떻게 동작하는지를 다룹니다.