자바스크립트 함수 호출에 따른 this 와 명시적 바인딩

    반응형

    this 바인딩

     

    자바스크립트에서 헷갈리는 내용을 뽑는다고 하면 가장 우선순위에 들어갈 내용이 바로 this 키워드라고 생각한다. 자바스크립트에서의 this는 호출 방식에 따라 다르게 사용되기 때문에 헷갈리더라도 정확하게 공부하고 용도에 맞게 사용할 수 있도록 알아두어야 한다.

     

    this


    this 키워드는 자신이 속한 객체를 가리키는 식별자이다. 하지만 자바스크립트에서의 this는 다른 언어와 조금 다르게 동작한다. 자바스크립트에서의 this는 함수가 어떻게 호출되었는지에 따라 this가 가리키는 객체가 결정된다. 즉 this 바인딩은 함수의 호출 방식에 따라(누가 어떻게 호출했는지에 따라) 동적으로 결정된다. this는 객체의 프로퍼티나 메서드를 참조하기 위한 자기 참조 변수이기 때문에 객체의 메서드 내부, 생성자 함수 내부에서만 의미가 있다. this는 실행 컨텍스트가 생성되고 평가되는 과정에서 바인딩이 결정되기 때문에 함수가 정의된 위치에 영향을 받지 않는다.

     

    1. 전역에서의 호출

    전역 범위에서 this를 호출하면 런타임 환경에 따라 다르게 가리킨다. 브라우저에서는 window 객체를 Node.js 환경에서는 global 객체를 가리킨다. 

     

    브라우저에서의 this
    Node.js 에서의 this

    2. 일반 함수에서의 this

    전역에서의 this를 보았듰이 일반적으로 this에는 전역 객체가 바인딩 된다. 이와 마찬가지로 함수를 일반적으로 호출하면 함수 내부에서의 this는 전역 객체를 가리키게 된다. strict mode가 적용된 일반 함수에서의 this는 undefined가 바인딩 된다.

    function name() {
      console.log(this);
    }
    
    name();  // 브라우저에서는 window, Node.js에서는 global 객체를 의미한다.
    
    function name() {
      'use strict'
      console.log(this);
    }
    
    name();  // undefined

     

    메서드 내에서 정의된 중첩 함수도 일반 함수처럼 호출된다면 중첩 함수가 가지고 있는 this 또한 전역 객체에 바인딩 되는 모습을 확인할 수 있다. 

     

    const user = {
      name: "minhoo",
      getName() {
        console.log(this); // {name: 'minhoo', getName: ƒ}
    
        function consoleName() {
          console.log(this);
        }
    
        consoleName(); // Window {0: Window, 1: Window, window: Window, …}
      },
    };
    
    user.getName();

     

    user의 getName 메서드 내부에 consoleName 함수가 일반 함수처럼 호출되어 내부에서의 this가 window 객체를 가리키고 있는 모습이다. 메서드 내부의 뿐만 아니라 콜백함수도 그러하다. 즉 일반적인 함수로 호출된 모든 함수에 내부 this는 전역객체에 바인딩 된다.

     

    3. 메서드에서의 this

    일반적인 함수로서의 호출이 아닌 객체의 메서드로서 호출되는 함수의 경우 this는 메서드를 호출한 객체에 바인딩 된다. this는 호출방식에 따라 결정된다고 했다. 그렇기 때문에 this를 소유한 객체가아니라 메서드를 호출한 객체에 바인딩 된다.

     

    const user = {
      name: "minhoo",
      getName() {
        console.log(this); // {name: 'minhoo', getName: ƒ}
      },
    };
    
    user.getName();

     

    객체의 메서드를 변수에 할당하거나 다른 함수의 인자로 사용하면 this가 가리키는 값이 변할 수 있다. 그렇기 때문에 메서드를 호출한 객체에 바인딩 되는 것을 기억해야 한다. 또한 프로토타입 메서드 내부에서 사용된 this 또한 해당 메서드를 호출한 객체에 바인딩 된다.

     

    const user = {
      name: "minhoo",
      getName() {
        console.log(this); // {name: 'minhoo', getName: ƒ}
      },
    };
    
    user.getName(); // {name: 'minhoo', getName: ƒ}
    
    let name = user.getName
    name() // Window {0: Window, 1: Window, window: Window, self: Window, …}

     

    4. 생성자 함수에서의 this

    생성자 함수에서의 this는 생성자 함수를 통해 생성할 인스턴스에 바인딩 된다. 아래의 코드를 보면 쉽게 이해할 수 있다.

     

    function User(name, age) {
      this.name = name;
      this.age = age;
    }
    
    const user1 = new User("minhoo", 30);
    
    console.log(user1.name); // minhoo
    console.log(user1.age); // 30

     

    user1 은 생성자 함수 User를 통해 생성한 인스턴스가 되고 User 내부에서 this가 가리키게 되는 값은 User의 인스턴스인 user1이 되고 user1의 값이 바인딩 된다.

     

    5. 화살표 함수에서의 this

    이렇게 함수에서의 this가 여러 불편함을 제공했기 때문에 ES6에서 화살표 함수가 새로 도입되었다. 화살표 함수와 일반 함수의 가장 큰 차이점을 이야기한다면 바로 this 바인딩의 방식 혹은 차이라고 이야기할 수 있다. 여태까지 일반 함수에서는 함수가 어떻게 호출되는지에 따라 this가 동적으로 결정되었다. 하지만 화살표 함수에서는 함수가 정의된 시점의 렉시컬 스코프에서의 this를 그대로 사용한다. 이것을 렉시컬 this라고 부른다.

     

    const person = {
      name: "minhoo",
      sayHello: function() {
        console.log("안녕하세요, " + this.name + "입니다.");
      },
      sayHelloArrow: () => {
        console.log("안녕하세요, " + this.name + "입니다.");
      }
    };
    
    person.sayHello();        //  안녕하세요, minhoo입니다.
    person.sayHelloArrow();   //  안녕하세요, 입니다. 혹은 안녕하세요, undefined입니다.

     

    위 코드는 모두 sayHellow와 sayHellowArrow를 메서드로 호출해서 사용되고 있지만 일반 함수와 화살표 함수로 정의했다. 그래서 출력되는 결과를 살펴보면 출력되는 결과가 다르다는 것을 확인할 수 있다. 왜 그런 것일까?

     

    const counter = {
      count: 0,
      
      start: function() {
        setInterval(function() {
          this.count++; 
          console.log(this.count);  // NaN
        }, 1000);
      },
      
      startArrow: function() {
        setInterval(() => {
          this.count++;  
          console.log(this.count);  // 1, 2, 3, ...
        }, 1000);
      }
    };
    
    counter.start();      // NaN이 매 초마다 출력
    counter.startArrow(); // 1, 2, 3, ...이 매 초마다 출력

     

    counter.start( )부터 살펴보면 start는 일반 함수이다. counter.start( )를 통해 메서드로 활용되었고 start( )의 this는 counter 객체로 바인딩 된다. 하지만 setInterval 함수의 콜백 함수로 일반 함수를 호출한다. 이 콜백 함수는 새로운 스코프를 만들고 이 this는 전역 객체를 가리키게 된다. 그렇게 this는 전역 객체 this.count는 undefined가 되고 NaN이 출력된다.

     

    이제 counter.startArrow( )를 살펴보자. startArrow 또한 일반 함수이다. counter.startArrow( )를 통해 메서드로 활용되었고 startArrow( )의 this는 counter 객체로 바인딩 된다. 그리고 setInterval 함수의 콜백 함수로 화살표 함수가 들어오는데 이때 화살표 함수는 일반 함수처럼 this를 가지지 않는다. 일반 함수로 호출되면 this는 전역객체를 가르킨다고 이야기 했다. 화살표 함수는 자신만의 this를 가지지 않고 상위 렉시컬 환경의 스코프 startArrow가 메서드로 활용되면서 고정된 counter 객체를 가르키고 정상적으로 출력되는 모습을 볼 수 있다.

     

    이러한 특징을 가지고 있기 때문에 몇 가지 상황에서는 화살표 함수를 사용할 수 없다. 첫 번째로 메소드로 화살표 함수를 사용할 수 없다 아래의 코드를 예시로 볼 수 있다.

     

    const user = {
      age: 30,
      getName: () => {
        console.log(this.age);
        console.log(this);
      },
    };
    
    user.getName(); // undefined

     

    getName을 메서드로 활용해서 user 객체에 바인딩 된다고 생각할 수 있지만 화살표 함수는 자신만의 this를 가지지 않고 상위 스코프의 this를 활용한다고 말했다. 그렇다면 이 코드에서의 상위 스코프는 전역이 된다. 그렇기 때문에 window 객체가 출력되고 window 객체에 age 가 없기 때문에 undefined가 출력된다.

     

    이벤트 핸들러에서의 this는 이미 해당 이벤트가 호출될 때 엘리먼트가 바인딩 되도록 정의되어 있기 때문에 이벤트 리스너의 콜백 함수로 화살표 함수를 사용하게 되면 기존 정의된 바인딩이 사리지고 새로운 상위 스코프의 this가 바인딩 되기 때문에 문제가 발생할 수 있다.

    const btn = document.getElementById('btn');
    
    // 화살표 함수 사용 (문제 발생)
    btn.addEventListener('click', () => {
      console.log(this); // Window 객체 출력
    });
    
    // 일반 함수 사용 (정상 동작)
    btn.addEventListener('click', function() {
      console.log(this); // 버튼 요소 출력
    });

     

    이 외에도 생성자 함수로 화살표 함수를 사용할 수 없고, 객체의 메서드를 정의할 때는 화살표 함수보다 단축 구문을 사용하는 것을 더 권장한다.

     

    6. call / apply /bind 메서드

    함수의 호출 방식에 따라 달라지는 this를 살펴보았다. 이러한 this를 명시적으로 바인딩 할 수 있는 여러 메서드가 있는데 위와 같다. call, apply, bind는 모두 Function.prototype의 메서드로 모든 함수가 상속받아 사용할 수 있다. 

    • call, apply
      call, apply 메서드는 this로 바인딩 할 객체와 인자를 받아 함수의 호출 결과를 반한환다. 둘 메서드는 동일하게 동작하고 인자를 전달하는 방식만 다르기 때문에 무엇을 사용하든 크게 문제없다. call 메서드는 쉼표로 구분한 리스트 형식으로 인자를 받고 apply 메서드는 배열의 형태로 인자를 받아 사용한다. call, apply 모두 즉시 실행 함수로 결과값을 반환한다.

    const obj = {
      name: "minhoo",
    };
    
    function getUser(age) {
      console.log(`이름은 ${this.name} 나이는 ${age}`);
    }
    
    getUser.call(obj, 30); // 이름은 minhoo 나이는 30
    getUser.apply(obj, [30]); // 이름은 minhoo 나이는 30

     

    • bind
      bind 메서드는 call, apply와 달리 즉시 실행 함수가 아닌 새로운 함수를 반환한다. 이 반환된 새로운 함수는 원본 함수를 변경하지 않은 새로운 함수이다. 또한 메서드를 사용해서 인자를 미리 설정할 수 도 있다.

    const obj = {
      name: "minhoo",
    };
    
    function getUser(age, city) {
      console.log(`이름은 ${this.name}, 나이는 ${age}, 사는 곳은 ${city}`);
    }
    
    console.log(getUser.bind) //ƒ bind() { [native code] } 혹은 [Function: bound getUser]
    
    const boundGetUser = getUser.bind(obj);
    boundGetUser(30, "서울"); // 이름은 minhoo, 나이는 30, 사는 곳은 서울
    
    // bind로 일부 인수 미리 지정
    const boundGetUserWith40 = getUser.bind(obj, 40);
    boundGetUserWith40("서울"); // 이름은 minhoo, 나이는 40, 사는 곳은 서울
    
    // 원본 함수는 변경되지 않음
    getUser(50, "서울"); // 이름은 undefined, 나이는 50, 사는 곳은 서울

     

    call, apply, bind 메서드를 통해 객체를 명시적으로 바인딩 할 수 있으며 bind( ) 메서드는 즉시 실행 함수가 아니며, 원본 함수를 변경하지 않은 새로운 함수를 리턴한다는 것을 기억해야 한다.

    반응형

    댓글