본문 바로가기

JavaScript

JavaScript 함수 바인딩

* 아래의 글은 JavaScript 프롤로그 및 목차를 먼저 읽으신 후 읽으시기를 권장합니다.


본 글에서 다룰 내용

  • 함수 바인딩이 필요한 이유 및 예시
  • 함수를 바인딩 할 수 있는 방법
  • 요약

사라진 'this'

 

객체 메서드가 객체 내부가 아닌 다른 곳에 전달되어 호출되면 this가 사라집니다.

 

setTimeout을 사용한 아래 예시에서 this가 어떻게 사라지는지 살펴봅시다.

 

let user = {
  firstName: "John",
  sayHi() {
    console.log(`Hello, ${this.firstName}!`);
  }
};

setTimeout(user.sayHi, 1000); // Hello, undefined!

this.firstName이 "John"이 되어야 하는데, 콘솔창에는 undefined가 출력됩니다.

 

이렇게 된 이유는 setTimeout에 객체에서 분리된 함수인 user.sayHi가 전달되기 때문입니다.

위 예시의 마지막 줄은 다음 코드와 같습니다.

let f = user.sayHi;
setTimeout(f, 1000); // user 컨텍스트를 잃어버림

 

위 코드를 보니 더욱더 쉽게 이해되죠? 더욱더 자세하게 설명하자면

브라우저 환경에서 setTimeout 메서드는 조금 특별한 방식으로 동작합니다.
인수로 전달받은 함수를 호출할 때, this에 window를 할당합니다. 따라서 위 예시의 this.firstName은 window.firstName이 되는데, window 객체엔 firstName이 없으므로 undefined가 출력됩니다. 다른 유사한 사례에서도 대부분 this는 undefined가 됩니다.

객체 메서드를 실제 메서드가 호출되는 곳으로 전달하는 것은 아주 흔하게 발생합니다.

이렇게 메서드를 전달할 때, 컨텍스트를 제대로 유지하기 위해 함수 바인딩이 필요한 것 입니다.


방법 1: 래퍼

 

가장 간단한 해결책은 래퍼 함수를 사용하는 것입니다.

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(function() {
  user.sayHi(); // Hello, John!
}, 1000);

위 예시가 의도한 대로 동작하는 이유는 외부 렉시컬 환경에서 user를 받아서 보통 때처럼 메서드를 호출했기 때문입니다.

 

setTimeout 부분은 아래와 같이 변경할 수도 있습니다.

setTimeout(() => user.sayHi(), 1000); // Hello, John!

이렇게 코드를 작성하면 간결해져서 보기는 좋지만, 약간의 취약성이 생깁니다.

 

setTimeout이 트리거 되기 전체(1초가 지나기 전에) user가 변경되면, 변경된 객체의 메서드를 호출하게 됩니다.

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(() => user.sayHi(), 1000);

// 1초가 지나기 전에 user의 값이 바뀜
user = { sayHi() { alert("또 다른 사용자!"); } };

// setTimeout에 또 다른 사용자!

이러한 문제점은 아래의 두 번째 방법을 사용하면 발생하지 않습니다.


방법 2: bind

 

모든 함수는 this를 수정하게 해주는 내장 메서드 bind를 제공합니다.

 

기본 문법은 다음과 같습니다.

// 더 복잡한 문법은 뒤에 나옵니다.
let boundFunc = func.bind(context);

func.bind(context)는 함수처럼 호출 가능한 '특수 객체'를 반환합니다. 이 객체를 호출하면 this가 context로 고정된 함수 func가 반환됩니다.

 

아래 예시 코드를 보면 더욱더 이해가 쉬울 것 같습니다.

let user = {
  firstName: "John"
};

function func() {
  alert(this.firstName);
}

let funcUser = func.bind(user);
funcUser(); // John

funcUser에는 this가 user로 고정된 func가 할당됩니다.

여기서 func.bind(user)는 func의 this를 user로 '바인딩한 변형'이라고 생각하시면 됩니다.

 

그렇다면 인수를 가지는 함수는 어떻게 될까요?

let user = {
  firstName: "John"
};

function func(phrase) {
  alert(phrase + ', ' + this.firstName);
}

// this를 user로 바인딩합니다.
let funcUser = func.bind(user);

funcUser("Hello"); // Hello, John (인수 "Hello"가 넘겨지고 this는 user로 고정됩니다.)

인수는 원본 함수 func에 '그대로' 전달됩니다.

 

bind는 래퍼 방법을 사용했을 때 발생하는 문제점이 발생하지 않는다고 했습니다.

아래의 예시 코드를 보면서 왜 그런지 알아봅시다.

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

let sayHi = user.sayHi.bind(user); // (*)

// 이제 객체 없이도 객체 메서드를 호출할 수 있습니다.
sayHi(); // Hello, John!

setTimeout(sayHi, 1000); // Hello, John!

// 1초 이내에 user 값이 변화해도
// sayHi는 기존 값을 사용합니다.
user = {
  sayHi() { alert("또 다른 사용자!"); }
};

(*)로 표시한 줄에서 메서드 user.sayHi를 가져오고, 메서드에 user를 바인딩합니다.

sayHi는 이제 '묶인(bound)' 함수가 되어 단독으로 호출할 수 있고 setTimeout에 전달하여 호출할 수도 있습니다.

어떤 방식이든 컨택스트는 원하는 대로 고정됩니다.


요약

  • func.bind(context, ...args)는 this가 context로 고정되고 인수도 고정된 함수 func을 반환합니다.
  • bind는 보통 객체 메서드의 this를 고정해 어딘가에 넘기고자 할 때 사용합니다.
  • 기존 함수의 인수 몇 개를 고정한 함수를 부분 적용 함수 또는 부분 함수라고 합니다.

참고 문헌 및 사이트

 

모던 JavaScript 튜토리얼 - https://ko.javascript.info/bind

 

긴 글 읽어주셔서 감사합니다😀