자바스크립트 고급 #2 this 바인딩과 호출 패턴

자바스크립트의 this호출 방식에 따라 달라집니다. 다른 언어들에서는 클래스 인스턴스가 자연스럽게 this가 되는 상황에서, 자바스크립트는 호출하는 모양에 따라 결정됩니다. 이번 글은 그 규칙을 한 번에 정리합니다.

this가 결정되는 네 가지 규칙 #

호출 방식에 따라 우선순위가 있으며, 위에서부터 차례로 적용됩니다.

규칙호출 모양this가 됨
1new Func()새로 만들어진 인스턴스
2obj.method()obj
3func.call(x) / apply / bind명시한 x
4func()글로벌 객체(또는 strict에서는 undefined)

위 네 규칙의 예외 — 화살표 함수는 이 모두를 무시하고 자기가 정의된 위치의 this를 그대로 사용합니다(잠시 뒤).

규칙 4 — 그냥 호출 #

기본 호출
function show() {
  console.log(this);
}

show();   // strict 모드: undefined / 비-strict: globalThis (window)

가장 단순한 경우입니다. strict 모드 (기초 #2let/const/모듈은 자동으로 strict)에서는 undefined, 비-strict에서는 글로벌 객체입니다.

ES Modules와 클래스 본문은 자동 strict 라서 모던 코드에서는 거의 undefined 라고 보면 됩니다.

규칙 2 — 메서드 호출 #

메서드 호출 — 점 앞이 this
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 — 즉시 호출 #

call
function greet(greeting) {
  console.log(`${greeting}, ${this.name}`);
}

greet.call({ name: '커티스' }, '안녕');
// 안녕, 커티스

첫 번째 인자가 this, 그 뒤가 함수 인자. 함수를 즉시 호출합니다.

apply — 인자를 배열로 #

apply
greet.apply({ name: '커티스' }, ['안녕']);
// 안녕, 커티스

call과 같지만 인자를 배열로 받습니다. spread가 등장한 뒤로는 apply를 직접 쓸 일이 거의 없습니다 — fn(...args)가 더 짧습니다.

bind — this가 묶인 새 함수 반환 #

bind
const boundGreet = greet.bind({ name: '커티스' });
boundGreet('안녕');   // 안녕, 커티스

boundGreet.call({ name: '다른' }, '안녕');
// 안녕, 커티스  ← bind로 묶인 this가 우선

bind는 호출하지 않고 this가 묶인 새 함수를 돌려줍니다. 일단 묶이면 그 뒤로는 어떻게 호출하든 this가 안 바뀝니다. 콜백 등록에서 자주 사용합니다.

bind로 인자도 미리 묶기 #

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 호출 #

new 호출
function User(name) {
  this.name = name;
}

const u = new User('커티스');
u.name;   // '커티스'

new와 함께 호출하면 — 새 객체를 만들고, 그 객체가 this가 되고, 함수 끝에서 자동으로 그 객체를 반환합니다. 클래스 호출이 결국 이 동작입니다.

new 호출이 다른 모든 규칙보다 우선합니다.

화살표 함수 — 위 규칙을 깨는 예외 #

화살표 함수는 자기만의 this를 가지지 않습니다. 정의된 위치의 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는 무시됨 #

bind도 화살표에는 안 통함
const arrow = () => this;
const bound = arrow.bind({ name: '커티스' });
bound();   // 여전히 바깥 this

화살표 함수는 자기 this를 갖지 않으니까 — 바인딩할 게 없습니다. bind가 조용히 무시됩니다.

자주 만나는 헷갈리는 경우 #

1) forEach 콜백 #

forEach 안의 this
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 값)를 줄 수도 있습니다.

forEach의 thisArg
this.items.forEach(function(x) {
  console.log(this.prefix + x);
}, this);   // 두 번째 인자가 thisArg

화살표 함수가 등장한 뒤로는 thisArg 패턴은 거의 안 씁니다.

2) DOM 이벤트 핸들러 #

이벤트 핸들러 — this는 currentTarget
button.addEventListener('click', function(e) {
  console.log(this);   // 이벤트가 걸린 엘리먼트 (currentTarget)
});

button.addEventListener('click', (e) => {
  console.log(this);   // 바깥 (보통 undefined 또는 window)
});

옛 코드의 this는 이벤트가 걸린 엘리먼트를 가리키게 의도되어 있었습니다. 화살표 함수를 쓰면 그 의도가 깨집니다. **DOM 핸들러에서 엘리먼트가 필요하면 e.currentTarget**을 쓰는 게 어느 함수 형태든 안전합니다.

this와 strict 모드 #

옛 자바스크립트의 함정을 한 가지 짚어봅니다.

비-strict — this가 글로벌
function f() {
  console.log(this);   // window 또는 global
}
f();
strict — undefined
'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 프로토타입 체인)에서는 클래스의 진짜 정체 — 프로토타입과 그 체인이 어떻게 동작하는지를 다룹니다.

X