본문 바로가기
Web/에러노트

setTimeout함수에서의 this값 변화

by joooing 2021. 1. 13.
반응형

setTimeout함수에서의 this값 변화


❗️ 에러

일단 setTimeout은 시간 지연을 일으켜 함수를 '비동기적'으로 실행시키는 함수이다. 이 함수는 명시적으로 항상 전역 객체(window)를 this 바인딩하는데, 이로 인해 아래와 같은 에러가 발생할 수 있다.

 

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  
  getArea() {
    return this.width * this.height;
  }

  printArea() {
    console.log('넓이 : ' + this.getArea());
  }
  
  printSync() {         // 사각형 넓이 즉시 표시
    this.printArea();
  }
  
  printAsync() {        // 사각형 넓이 2초 후 표시
    setTimeout(this.printArea, 2000);
  }
}

let box = new Rectangle(40, 20);
box.printSync();   // '넓이 : 800'
box.printAsync();  // TypeError

 

// 에러 메세지
Uncaught TypeError: this.getArea is not a function at printArea (<anonymous>:12:36)

 

 

🤔 의문점

말 그대로 setTimeout 안에서는 this가 전역객체를 가리킨다는 것까지는 이해가 되었다. 에러를 통해서도 this가 Rectangle의 인스턴스가 아니라는 것을 확인할 수 있었다.

 

여기서 헷갈렸던 점은 에러메세지에 왜 this.printArea is not a function 이 아니라 this.getArea is not a function at printArea 이라는 메세지가 뜨는지 였다.

 

일단 맨 처음에는 이 부분이 실행될 것이다. 👇🏻

 

let box = new Rectangle(40, 20);
box.printAsync();

 

그리고 printAsync 함수가 실행된다. 이 함수의 내용은 setTimeout 함수밖에 없으니 setTimeout 함수도 정상적으로 실행될 것이다.

 

printAsync() {        // 사각형 넓이 2초 후 표시
    setTimeout(this.printArea, 2000);
}

 

여기서, 'setTimeout 함수는 명시적으로 항상 전역 객체(window)를 this 바인딩한다'는 것을 고려해서 this값이 전역객체(window)가 나오게 될 줄 알았다. 그래서 this.printArea는 window.printArea가 되어 이 부분에서 에러가 발생해 printArea메서드가 수행되지 않을 것이라고 생각했다.

 

setTimeout(this.printArea, 2000);

 

하지만 에러메세지를 보면, printArea메서드는 정상적으로 실행이 되고 getArea를 호출하는 부분에서 에러가 발생했다고 말해주고 있다. 그래서 setTimeout에서 this값이 전역객체를 가리키게되는 시점이 정확히 언제인지 의문을 갖게 되었다.

 

// 에러 메세지
Uncaught TypeError: this.getArea is not a function at printArea (<anonymous>:12:36)

 

 

👩🏻‍💻 테스트

class Rectangle {
    printSync(){
        console.log('sync', this);
    }
    printAsync(){
        console.log('async', this);
        setTimeout(console.log('setTimeout', this), 1000);
        setTimeout(function(){console.log('setTimeout-func', this)}, 1000);
        setTimeout(() => console.log('setTimeout-arrow', this), 1000);
    }
}

let box = new Rectangle();
box.printSync();
box.printAsync();

 

테스트코드 실행 결과 (브라우저 환경)

 

1. setTimeout 함수를 사용하지 않았을 때

먼저 setTimeout 함수를 사용하지 않은 경우이다. 이 때 결과값을 보면 this값이 Rectangle, 즉 상위 객체를 가리킨다는 것을 확인할 수 있다. 이 경우는 '메서드로 실행된 경우'에 해당하기 때문에 예상한대로 호출 주체(메서드명 앞 객체)인 Rectangle을 가리킨 것이다.

 

class Rectangle {
    printSync(){
        console.log('sync', this);
    }
}

let box = new Rectangle();
box.printSync();

// 출력결과 : sync Rectangle{}

 

이 부분도 이름만 async일 뿐 아직 setTimeout 함수 실행 전이기 때문에, 마찬가지로 this는 호출 주체인 Rectangle을 가리키게 된다.

 

class Rectangle {
    printAsync(){
        console.log('async', this);
    }
}

let box = new Rectangle();
box.printAsync();

// 출력결과 : async Rectangle{}

 

 

2. setTimeout 함수를 사용했을 때

 

2-1) 바로 this를 출력했을 때

결과를 보면 아직 this는 호출 주체인 Rectangle을 가리키고 있는 것을 확인할 수 있다. 이로써 setTimeout 함수 내부에서 아무런 일도 발생하지 않았을 때, this값은 이전 그대로 자신을 호출한 주체 (Rectangle)를 가리킨다는 것을 알게 되었다.

 

✔️ setTimeout함수 안에서 아무일도 일어나지 않았을 때 바로 확인한 this값 = 호출 주체
printAsync(){
    setTimeout(console.log('setTimeout', this), 1000);
}

// 출력결과 : setTimeout Rectangle{}

 

 

2-2) 함수 실행 후 this를 출력했을 때

드디어!!! 이 때부터 this가 전역객체(window)를 가리키기 시작한다. setTimeout함수 안에서 함수가 실행될 때, this값에 변화가 생긴다. 더이상 이전처럼 호출한 주체를 가리키지 않고 전역객체를 가리키게 되는 것이다.

 

printAsync(){
    setTimeout(function(){console.log('setTimeout-func', this)}, 1000);
}

// 출력결과 : setTimeout-func Window{}

 

(이 부분은 개인적인 생각이라 정확히 알아보고 수정할 예정이다)

일단 내가 생각한 원인은 실행 컨텍스트와 관련이 있는 것 같다. (개념에 대한 설명은 링크 참고) 어떤 함수가 실행(호출)되면 Javascript 엔진은 그제서야 해당 함수와 관련된 정보들을 수집하기 시작한다. 이 수집 대상에는 this로 지정된 객체를 저장하는 'ThisBinding'이라는 객체도 포함된다. 이를 바탕으로 생각해보면, setTimeout 함수 안에서도 this값이 전역객체로 새롭게 수집되는 시점은 어떤 함수가 setTimeout 함수 내부에서 실행되는 시점인 것 같다. (근데 이렇게 생각해보면, 모든 콜백함수에서 이런 문제가 발생하지 않을까....? 이 부분은 질문을 해보도록 해야겠다..)

 

 

2-3. (추가) 화살표 함수 실행 후 this를 출력했을 때

setTimeout 함수 내부에서 다른 함수가 호출되더라도, 그게 화살표 함수라면 상황이 달라진다. 이번에는 다시 this가 상위 객체인 Rectangle을 가리키는 것을 볼 수 있다. 화살표 함수는 내부에 this값을 갖지 않는다. 원래 어떤 함수든 실행될 때 그 함수만의 '실행 컨텍스트'를 생성하는데, 보통의 함수는 이 때 this값을 특정값으로 지정하는 과정(바인딩)을 거치지만, 화살표함수는 특이하게 이 과정을 생략한다.

 

✔️ 화살표함수 = this값을 고정시키는 과정을 생략함 → this값 없음

대신 사용자가 this값에 접근하려고 하면, 스코프 체인상에서 가장 가까운 곳(보통 상위 스코프)의 this값을 가져와 사용자가 그 값을 this값으로 사용할 수 있게 해준다. 이 예제에서는 가장 가까운 스코프의 this값이 'Rectangle' 이다. 그래서 출력결과에서도 this가 Rectangle로 나오게 되는 것이다.

 

printAsync(){
    setTimeout(() => console.log('setTimeout-arrow', this), 1000);
}

// 출력결과 : setTimeout-arrow Rectangle{}

 

✍🏻 결론

setTimeout은 시간 지연을 일으켜 함수를 '비동기적'으로 실행시키는 함수인데, 이 함수는 명시적으로 항상 전역 객체(window)를 this 바인딩한다. 즉 setTimeout 안에서는 this가 전역객체를 가리킨다는 말이다.

여기서 궁금했던 점은 setTimeout에서 this값이 전역객체를 가리키게되는 시점이 정확히 언제인지 였다. 여러 코드들을 통해 테스트 해본 결과, setTimeout함수 안에서 어떤 함수가 실행되고 나서야 this값에 변화가 생긴다는 것을 확인할 수 있었다. setTimeout함수 안이라도 아무일도 일어나지 않은 상태에서는 this값은 여전히 상위 객체를 가리킨다.

 

✔️ setTimeout안에서 함수가 호출되면, this값이 새로 수집되어 전역으로 바뀌게 된다!

 

반응형

댓글