ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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, 첫 번째 인수는 2mul 의 호출결과가 전달 된다. 추가 인수는 '그대로(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

    댓글

Designed by Tistory.