본문으로 건너뛰기
기술스택 · stack/javascript/bind와 this: 콜백에 메서드를 넘길 때

JavaScript · 6장 함수 심화

bind와 this: 콜백에 메서드를 넘길 때

4장에서 this는 함수를 부르는 순간 정해지고, 메서드를 콜백으로 넘기면 this를 잃는다고 봤어요(this를 잃는 순간). 이번에는 그 해결책인 bind를 제대로 보고, 화살표 함수와 무엇이 다른지, bind가 만드는 새 버그까지 정리해요.

#6.7

01. bind: this를 묶어 둔 새 함수

함수.bind(객체)는 this가 그 객체로 고정된 새 함수를 돌려줘요. 원래 함수는 그대로예요. 새 함수는 어디로 넘겨져 어떻게 불리든 this가 바뀌지 않아요. call로 다른 객체를 줘도 소용없어요.

'use strict';

const user = {
  name: '하늘',
  greet() { return `안녕, ${this?.name}`; },
};

const loose = user.greet;
console.log(loose());                        // 안녕, undefined — this를 잃었어요

const bound = user.greet.bind(user);
console.log(bound());                        // 안녕, 하늘
console.log(bound === user.greet);           // false — 새 함수예요
console.log(bound.call({ name: '가람' }));   // 안녕, 하늘 — 한 번 묶으면 안 바뀌어요

setTimeout(bound, 50);                       // 콜백으로 넘겨도 안전해요
setTimeout(() => console.log('타이머:', bound()), 60);
①user.greet를 그대로 setTimeout에 넘기면 타이머가 점 앞의 객체 없이 불러서 this가 user가 아니게 돼요(타이머는 전역 객체를 this로 줘요). bind(user)로 만든 새 함수는 this가 user로 묶여 있어서, 누가 어떻게 부르든 '안녕, 하늘'을 돌려줘요.

같은 문제를 화살표 함수로 감싸서 풀 수도 있어요. setTimeout(() => user.greet(), 50)는 부르는 순간 점 앞에 user가 있으니까요. 차이는 언제 객체를 읽느냐예요. 화살표 함수는 부를 때마다 그때의 user를 읽고, bind는 묶는 순간의 객체를 기억해요. 대부분은 짧고 읽기 쉬운 화살표 함수를 써요.

02. 인자 미리 채우기 (부분 적용)

bind의 두 번째 인자부터는 앞쪽 인자를 미리 채워 둘 수 있어요. this가 필요 없는 함수면 첫 인자로 null을 줘요. 이렇게 일부 인자를 고정해 새 함수를 만드는 걸 부분 적용이라고 해요. 화살표 함수로도 같은 일을 할 수 있어요(설정을 기억하는 함수).

function formatPrice(currency, amount) {
  return new Intl.NumberFormat('ko-KR', { style: 'currency', currency }).format(amount);
}

const won = formatPrice.bind(null, 'KRW');      // 첫 인자를 'KRW'로 채워 둬요
const dollar = (amount) => formatPrice('USD', amount); // 화살표 함수로 같은 일

console.log(won(12900));    // ₩12,900
console.log(dollar(12.5));  // US$12.50

03. bind는 부를 때마다 새 함수를 만들어요

bind는 부를 때마다 새 함수를 만들어요. 이벤트 리스너를 붙일 때와 뗄 때 각각 .bind(this)를 쓰면, 붙인 함수와 떼려는 함수가 서로 다른 함수라서 떼어지지 않아요. 리스너가 쌓이는 누수의 흔한 원인이에요(누수). 아래 실행 블록은 브라우저의 이벤트 대상(EventTarget)으로 직접 확인해요.

const counter = {
  count: 0,
  inc() { this.count++; },
};
const button = new EventTarget();           // 버튼이라고 생각해요

button.addEventListener('click', counter.inc.bind(counter));
button.removeEventListener('click', counter.inc.bind(counter)); // 다른 함수를 떼려 해요

button.dispatchEvent(new Event('click'));   // 클릭
console.log(counter.count);                 // 1 — 뗐다고 생각했는데 실행됐어요

해결은 묶은 함수를 한 번만 만들어 변수에 담아 두고, 붙일 때와 뗄 때 같은 변수를 쓰는 거예요. 클래스에서는 handleClick = () => { … }처럼 화살표 함수 필드로 만들어 두는 방식을 많이 써요(9장). 떼는 일 자체를 AbortController에 맡기는 방법도 있어요(정리하는 습관).

04. 화살표 함수 다시 보기, 그리고 함수도 객체

화살표 함수는 그냥 짧게 쓰는 function이 아니에요. 자기 this와 arguments가 없어서 감싼 곳의 것을 그대로 쓰고(화살표 함수의 this), new로 부를 수 없어요. 그래서 콜백에는 잘 맞고, 객체의 메서드 · 생성자 · 래퍼(this를 넘기는 래퍼)에는 맞지 않아요. 화살표 함수에 bind를 써도 this는 바뀌지 않아요.

const Arrow = () => {};
console.log(typeof Arrow.bind({ a: 1 }));   // function — 만들어지긴 하지만 this는 안 바뀌어요
new Arrow();                                // 생성자로 부르면?

함수도 객체예요. 그래서 이름(name)과 매개변수 개수(length, 기본값 · 나머지 앞까지) 같은 속성이 있고, 직접 속성을 붙일 수도 있어요. 함수 표현식에 이름을 붙이면(기명 함수 표현식) 그 이름은 함수 안에서만 보여서, 변수 이름이 바뀌어도 자기 자신을 안전하게 다시 부를 수 있어요.

function search(query, page, ...rest) {}
console.log(search.name, search.length);   // search 2

function hit() { hit.count = (hit.count ?? 0) + 1; }
hit(); hit();
console.log(hit.count);                    // 2 — 함수에 붙인 속성

const factorial = function inner(n) {      // inner는 함수 안에서만 보여요
  return n <= 1 ? 1 : n * inner(n - 1);
};
console.log(factorial(5), typeof inner);   // 120 undefined

위젯이 열릴 때 리스너를 붙이고 닫힐 때 떼는 코드예요. 닫은 뒤에 클릭해도 숫자가 올라가요. 고쳐서 1 1이 나오게 해 보세요(열려 있을 때 한 번, 닫은 뒤에는 그대로).

const button = new EventTarget();

const widget = {
  clicks: 0,
  onClick() { this.clicks++; },
  open() { button.addEventListener('click', this.onClick.bind(this)); },
  close() { button.removeEventListener('click', this.onClick.bind(this)); },
};

widget.open();
button.dispatchEvent(new Event('click'));
const whileOpen = widget.clicks;

widget.close();
button.dispatchEvent(new Event('click'));
console.log(whileOpen, widget.clicks);
해답 보기

open과 close에서 각각 bind를 불러 서로 다른 함수가 생겼어요. 묶은 함수를 한 번 만들어 객체에 담아 두고 둘 다 그 함수를 써요.

const widget = {
  clicks: 0,
  onClick() { this.clicks++; },
  open() {
    this.handler = this.onClick.bind(this);      // 한 번 만들어 보관
    button.addEventListener('click', this.handler);
  },
  close() { button.removeEventListener('click', this.handler); },
};
// 1 1

05. 실전 포인트

  • 언제 쓰나요? 메서드를 콜백으로 넘길 때 this를 지키려면 () => obj.method()나 obj.method.bind(obj)를 써요. 떼야 하는 리스너는 묶은 함수를 변수에 보관해요. 인자를 미리 채운 함수가 필요하면 화살표 함수나 bind를 써요.
  • 대신 쓰는 것: 함수와 클로저로만 상태를 다루면 this 자체가 없어서 bind가 필요 없어요(React 함수 컴포넌트처럼). 클래스라면 화살표 함수 필드로 메서드를 한 번만 묶어 둬요.
  • 흔한 실수: 붙일 때와 뗄 때 각각 bind를 써서 리스너가 안 떨어지는 것, 화살표 함수에 bind로 this를 바꾸려는 것, 객체 메서드를 화살표 함수로 만드는 것, 이미 묶은 함수를 다시 bind하면 바뀔 거라 생각하는 것이에요.

정리

  • fn.bind(obj)는 this가 obj로 고정된 새 함수를 돌려줘요. 한 번 묶으면 바뀌지 않아요.
  • 콜백에 메서드를 넘길 땐 화살표 함수로 감싸거나 bind해요.
  • bind는 부를 때마다 새 함수예요. 떼야 하는 리스너는 묶은 함수를 보관해요.
  • 화살표 함수는 this · arguments가 없고 new로 부를 수 없어요. 함수는 name · length를 가진 객체예요.
const f = obj.method.bind(obj); f.call(other)에서 this는 무엇일까요?

obj예요. bind로 묶은 함수는 call · apply · 다시 bind로도 this가 바뀌지 않아요.

removeEventListener('click', this.save.bind(this))가 리스너를 떼지 못하는 이유는?

bind는 부를 때마다 새 함수를 만들어서, 붙일 때 넘긴 함수와 다른 함수를 떼려고 하기 때문이에요. 묶은 함수를 변수에 담아 두고 붙이기 · 떼기에 같은 변수를 써요.

콜백으로 넘길 때 () => user.greet()와 user.greet.bind(user)의 차이는?

둘 다 this를 지켜요. 화살표 함수는 부를 때마다 그때의 user 변수를 읽고, bind는 묶는 순간의 객체를 기억해요. 나중에 user가 다른 객체로 바뀌면 결과가 달라져요.

SIMPLE · 기술스택 / JavaScript — 6장 함수 심화