-
JavaScript - 함수 바인딩개발언어/JavaScript 2022. 12. 23. 14:39
함수 바인딩
setTimeout에 메서드를 전달할 때 처럼, 객체 메서드를 콜백으로 전달할 때 'this 정보가 사라지는' 문제가 생긴다.
사라진 'this'
객체 메서드가 객체 내부가 아닌 다른 곳에 전달되어 호출되면 this가 사라진다.
setTimeout을 사용한 아래 예시에서 this가 어떻게 사라지는지 살펴보자
let user = { firstName: "John", sayHi() { alert(`Hello,${this.firstName}!`); }, }; setTimeout(user.sayHi, 1000); // Hello, undefined!
this.firstName은 "John"이 되어야 하는데, alert창엔 undefined가 출력된다.
이런 현상이 나타나는 이유는 setTimeout에 객체에서 분리된 함수인 user.sayHi가 전달되기 때문이다.
위 예시의 마지막줄은 다음 코드와 같다
let f = user.sayHi; setTimeout(f,1000); // user컨텍스트를 잃어버림
브라우저 환경에서 setTimeout 메서드는 조금 특별한 방식으로 동작합니다. 인수로 전달받은 함수를 호출할 때, this에 window를 할당합니다(Node.js 환경에선 this가 타이머 객체가 되는데, 여기선 중요하지 않으므로 넘어가겠습니다). 따라서 위 예시의 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); //혹은 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)는 함수처럼 호출 가능한 '특수 객체(exotic object)'를 반환합니다. 이 객체를 호출하면 this가 context로 고정된 함수 func가 반환된다.
따라서 boundFunc를 호출하면 this가 고정된 func를 호출하는 것과 동일한 효과를 본다.
let user = { firstName:"John" }; //아래 함수의 this는 아직 아무것도 할당되지 않은 상태 function func(){ alert(this.firstName); } let funcUser = func.bind(user); funcUser(); //john
위 코드의 funcUser에는 this 가 user로 고정된 func가 할당된다.
여기서 func.bind(user)는 func의 this를 user 로 '바인딩한 변형(bound variant)'이라고 생각하면 된다.
user의 인스턴스에 원본 함수 func에 '그대로(as is)' 전달된다.
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로 고정된다.)
객체 메서드에 bind 적용
let user = { firstName: "John", sayHi() { alert(`Hello, ${this.firstName}!`); } }; let sayHi = user.sayHi.bind(user); //(*) // 이제 객체 없이도 객체 메서드를 호출할 수 있습니다. sayHi(); //Hello, John! setTimeout(sayHi,1000); Hello, Jon! //1초 이내에 user 값이 변화해도 //sayHi는 기존 값을 사용한다. user = { sayHi() {alert("또 다른 사용자!");} };
(*)로 표시한 줄에서 메서드 user.sayHi를 가져오고, 메서드에 user를 바인딩한다. sayHi는 이제 묶인(bound)함수가 되어 단독으로 호출할 수 있고 setTimeout에 전달하여 호출할 수도 있다. 어떤 방식이든 컨택스트는 원하는데로 고정이된다.
let user = { firstName: "John", say(phrase){ alert(`${phrase}, ${this.firstName}`); } }; let say = user.say.bind(user); say("Hello"); //Hello, John(인수 "Hello"가 say로 전달되었습니다.) say("Bye"); //Bye, John("Bye"가 say로 전달되었습니다.)
위 예시를 실행하면 인수는 '그대로' 전달되고 bind에 의해 this만 고정된것을 확인할 수 있다.
bindAll로 메서드 전체 바인딩하기
객체에 복수의 메서드가 있고 이 메서드 전체를 전달하려 할 땐, 반복문을 사용해 매서드를 반인딩할 수 있다.
for(let key in user){ if(typeof user[key] == 'function'){ user[key] = user[key].bind(user); } }
자바스크립트 라이브러리를 사용해도 대규모 바인딩을 할 수 있다.(lodash 라이브러리)
_.bindAll(object,methodNames)
부분 적용
this 뿐만 아니라 인수도 바인딩이 가능하다. bind의 전체 문법은 다음과 같다.
let bound = func.bind(context,[arg1],[arg2],...);
bind는 컨텍스트를 this로 고정하는 것 뿐만 아니라 함수의 인수도 고정을 해준다.
예시
곱셈을 해주는 함수 mul(a,b)를 예시로 살펴보자
function mul(a,b){ return a * b }
bind를 사용해 새로운 함수 double을 만든다.
function mul(a,b){ return a * b; } let double = mul.bind(null,2); alert(double(3)); // = mul(2,3) = 6 alert(double(4)); // = mul(2,4) = 8 alert(double(5)); // = mul(2,5) = 10
mul.bind(null,2)를 호출하면 새로운 함수 double이 만들어진다. 함수 double엔 컨텍스트가 null, 첫 번째 인수는 2인 mul 의 호출결과가 전달 된다. 추가 인수는 '그대로(as is)' 전달된다.
이런 방식을 부분적용(partial application)이라고 부른다. 부분 적용을 사용하면 기존 함수의 매개변수를 고정하여 새로운 함수를 만들 수 있다.
위 예시에서는 this를 사용하지 않았다는 점에 주목해야한다. bind함수는 항상 컨텍스트를 넘겨줘야 하므로 null을 사용했다.
부분적용 함수를 만드는 이유
가독성이 좋은 이름(double)을 가진 독립 함수를 만들 수 있다는 이점 때문이다. 게다가 bind를 사용해 첫번째 인수를 고정할 수 있기 때문에 항상 같은 인수가 전달되는 경우라면 매번 인수를 전달할 필요도 없어진다.
이 외에도 부분적용은 매우 포괄적인 함수를 기반으로 덜 포괄적인 변형 함수를 만들 수 있다는 점에서 유용하다.
함수 send(from, to, text)가 있다고 가정해 보면, 객체 user안에서 부분 적용을하면, 전송 주체가 현재 사용자인 함수 sendTo(to,text)를 구현할 수있다.
'개발언어 > JavaScript' 카테고리의 다른 글
JavaScript - 프로토타입 체인 (0) 2022.12.26 JavaScript - 프로토타입 (0) 2022.12.23 JavaScript - 메서드와 this (0) 2022.12.22 JavaScript - 객체 (0) 2022.12.22 async와 await 그리고 유용한 Promise APIs (0) 2022.06.10