티스토리 뷰

728x90

JS의 this는 함수 실행 방식과 선언 방식에 따라 값이 달라지기에 나를 포함한 여러 주니어 개발자를 혼란에 빠트리는 주범이다. 그래서 코드를 작성할 때마다 의도치 않은 값이 나올 때가 종종 있고, 그럴 때마다 나는 항상 this에 대해 구글에 검색하곤 한다. 하지만 이렇게 찾은 정보들은 연결되지 않고 단편적으로 내 뇌 속에 자리 잡고 있었는데, 이참에 개념을 한 번 쫙 정리해볼까 한다!

 

this 란 무엇인가

this를 한 문장으로 표현하자면, 아래와 같다.

execution context를 참조하는 데 사용되는 키워드

 

▼ execution context란?

더보기

execution context란?

 

여기서 execution context란 콜스택에 저장되는 함수의 실행 정보를 의미한다. JS에서는 특정 함수가 호출될 때마다 콜 스택에 해당 함수의 execution context를 저장하는데, 이때 execution context는 간단하게 말해서 해당 함수 내에서 사용되는 변수와 함수들의 정보에 대한 총 집합체라고 생각하면 된다.

 

즉, this 키워드를 사용하면 이 execution context에서 특정 값을 찾는다.

 

JS 파일이 실행되는 첫 순간에는 global execution context가 생성되고, 여기서 전역 변수 등을 참조한다고 생각하면 된다. 이후 함수가 하나씩 실행될 때마다 function execution context가 하나씩 생성된다.

 

this가 바인딩되는 방식에는 크게 암시적 바인딩명시적 바인딩이 있다.

 

1. 암시적 바인딩

this가 런타임에 함수가 호출되는 방식에 따라 동적으로 바인딩되는 것을 의미.

 

2. 명시적 바인딩

bind, call, apply 등을 사용해서 특정 객체에 대해 this를 바인딩하는 것을 의미.

 

여기까지는 사실 크게 어렵지 않지만, 일반 함수 정의가 아닌 Arrow function으로 정의할 때에는 그 동작이 조금 달라진다. 이에 대해서는 아래에서 더 자세하게 다루겠다.

 

그럼 우선 일반 함수로 정의했을 때 함수가 호출되는 방식에 따라 this가 가리키는 값에 대해 알아보겠다.

 

함수가 호출되는 방식에 따른 this (일반 함수로 정의)

1. Object Method (도트 표기법)

let temp = {
    value: 10,
    funcA: function() {
        console.log("object-funcA")
        console.log(this.value)
    }
}

temp.funcA() // object-funcA와 10 출력

위 예제에서는 temp 객체가 funcA 메서드를 호출하고 있기에 temp 객체가 this에 암시적으로 바인딩된다. 따라서 temp.funcA()를 호출할 시, object-funcA와 10을 출력한다.

대부분의 경우, 도트 왼쪽의 객체가 this라고 생각하면 된다.

 

하지만 여기서 this가 난해한 이유가 하나 나오는데, 일반 함수가 아닌 Arrow Function으로 정의하거나 Callback Function으로 전달하였을 경우에는 위와 다르게 동작하기 때문이다. 이에 대해서는 Global Context에서의 this에서 설명하겠다.

 

2. Global Context

let temp = {
    value: 10,
    funcA: function() {
        console.log("object-funcA")
        console.log(this.value)
    }
}

let funcA = temp.funcA
funcA() // object-funcA와 undefined 출력

위 예제에서는 funcA에 temp.funcA를 할당하고 이를 호출하고 있다. 위에서 다뤘던 Object Method를 떠올리면 object-funcA와 10을 출력할 것 같지만, 실제로는 object-funcA와 undefined를 출력한다. 대체 왜?!!!!

 

왜냐하면 앞서 말했듯이, this의 바인딩은 정의될 때 정해지는 것이 아니라, 런타임에 동적으로 바인딩되기 때문이다.

여기서 temp.funcA는 global execution context에서 정의된 변수 funcA에 할당되었고, global execution context에서 funcA를 호출하고 있다. this의 바인딩은 호출될 때 동적으로 이루어지며, 이는 곧 브라우저 환경이라면 window.funcA()와 같고, Node js 환경이라면 global.funcA()와 같다. 따라서 undefined를 출력한다.

 

 

그렇다면 만약, 아래와 같이 value 값을 전역에서 정의해준다면 undefined가 아닌 해당 값을 출력할까?

let value = 5;

let temp = {
    value: 10,
    funcA: function() {
        console.log("object-funcA")
        console.log(this.value)
    }
}

let funcA = temp.funcA
funcA() // object-funcA와 undefined 출력

정답은 아니오다.

왜냐하면 ES6부터는 strict mode가 기본으로 적용되는데, strict mode에서는 global execution context를 가리키는 this가 undefined를 가리키도록 변경되기 때문이다. 따라서 마찬가지로 undefined를 출력한다.

 

 

✻ Callback Function에서의 this

여기서 앞서 Object Method에서 언급했던 Callback Function에서의 동작이 사뭇 다른 이유도 함께 알 수 있다.

let temp = {
  value: 10,
  funcA: function () {
    console.log("object-funcA");
    console.log(this.value);
  },
};

setTimeout(temp.funcA, 100) // object-funcA와 undefined 출력

이 예제에서는 객체는 동일하지만, setTimeout 함수의 Callback Function으로 temp.funcA를 전달하고 있다. Object Method 방식을 사용하고 있어서 this.value가 10이 출력되어야 할 것 같지만, 실제로는 undefined를 출력한다.

 

왜냐하면 temp.funcA는 setTimeout의 첫 번째 파라미터에 callback = temp.funcA 형식으로 전달될 것이고, setTimeout 함수의 내부에서 callback()을 호출하여 temp.funcA를 호출할 때 this가 동적으로 바인딩되기 때문이다.

 

이와 같은 이유로 JS의 내장 객체 함수인 filter 메서드나 map 메서드와 같이 Callback Function을 인자로 전달받는 메서드의 경우, 추가로 thisArg도 전달받아 직접 this를 바인딩할 수 있게끔 한다.

filter 메서드
map 메서드

 

this는 이처럼 복잡한 동작 방식을 갖고 있다. 하지만 복잡하다고 해서 사용하지 않을 수도 없다. 그렇다면 어떻게 this를 안전하게 사용할 수 있을까? 명시적 바인딩을 사용하면 된다.

 

3. 명시적 바인딩

let temp = {
  value: 10,
  funcA: function () {
    console.log("object-funcA");
    console.log(this.value);
  },
};

let funcA = temp.funcA.bind(temp);
funcA(); // object-funcA와 10 출력

위 예제와 같이 temp.funcA를 할당할 때 bind 메서드를 사용하고 인자로 객체를 전달해주면, this는 해당 객체에 명시적으로 바인딩된다. 이는 Callback Function을 사용할 때에도 마찬가지이다.

let temp = {
  value: 10,
  funcA: function () {
    console.log("object-funcA");
    console.log(this.value);
  },
};

setTimeout(temp.funcA.bind(temp), 100); // object-funcA와 10 출력

만약 this의 동작 방식이 너무 복잡해서 머리가 지끈거린다. 그러면 그냥 편하게 bind 메서드를 사용하자.

bind 뿐만 아니라 call, apply 메서드도 명시적 바인딩에 사용되는데 이들의 차이점은 추가적인 인자를 전달하느냐, 안 하느냐이다.

 

 

지금까지 일반 함수로 정의했을 때, 함수가 호출되는 방식에 따른 this의 동작 방식을 알아보았다. 여기서 끝이라면 좋겠지만, this는 Arrow Function으로 정의했을 때 그 동작 방식이 완전히 달라진다. 따라서 Arrow Function에서의 this에 대해 알아보자!

 

Arrow Function에서의 this

앞서 일반 함수로 정의하였을 경우에 this는 런타임에 동적으로 바인딩된다고 설명했다. 하지만 Arrow Function의 경우에는 함수가 정의되는 시점에 this가 가리키는 값이 정해진다. 따라서 이를 컴파일 타임에 정적으로 바인딩된다고 표현한다.

이때 this의 값은 상위 execution context의 lexical environment이다.

 

즉 간단하게 말하자면, 정의되는 시점에서 자신을 둘러싸고 있는 함수의 변수를 사용한다고 보면 된다.

 

먼저 아래 예제를 한 번 살펴보자.

class Temp {
  constructor() {
    this.value = 10;
  }
  funcA = function () {
    console.log("class-funcA");
    console.log(this.value);
  };
  funcB = () => {
    console.log("class-funcB");
    console.log(this.value);
  };
}

const classTemp = new Temp();

classTemp.funcA(); // class-funcA와 10 출력
classTemp.funcB(); // class-funcB와 10 출력

이 예제에서는 funcA와 funcB에 대해 각각 일반 함수 정의 및 Arrow Function 정의 방식을 사용해 클래스 필드로 함수를 정의하고 있다.

 

여기서 classTemp.funcA()의 경우 위에서 언급한 Object Method 방식을 사용하고 있기에 당연히 class-funcA와 10을 출력한다.

 

classTemp.funcB()의 경우에도 class-funcB와 10을 출력하고 있는데, 이는 funcB가 정의될 때 상위 context인 Temp 클래스의 lexical environment에 value 값이 저장되어 있으므로 해당 값이 this에 정적으로 바인딩된다.

 

 

만약 Callback Function으로 전달할 경우에는 this가 어떻게 동작할까?

class Temp {
  constructor() {
    this.value = 10;
  }
  funcA = function () {
    console.log("class-funcA");
    console.log(this.value);
  };
  funcB = () => {
    console.log("class-funcB");
    console.log(this.value);
  };
}

const classTemp = new Temp();

setTimeout(classTemp.funcA, 100); // class-funcA와 undefined 출력
setTimeout(classTemp.funcB, 100); // class-funcB와 10 출력

classTemp.funcA의 경우 예상한 대로 this.value를 출력할 경우, undefined가 출력된다. 하지만, classTemp.funcB의 경우에는 정상적으로 10이 출력되는데 이는 funcB가 Arrow Function으로 정의되었기에 동적으로 바인딩되지 않고 정의되는 시점에 이미 정적으로 바인딩되었기 때문이다.

 

즉, funcB에서의 this.value는 funcB가 정의되는 시점에 이미 10이라는 값으로 바인딩되었으므로 그 값이 변하지 않는다.

이러한 이유로 Callback Function을 전달할 때 명시적 바인딩을 사용하기도 하지만, 런타임에 this가 바뀌지 않는다는 특성 탓에 대부분 Arrow Function을 전달한다.

 

 

Class에서의 Arrow Function에 대해 알아보았는데, 이번에는 객체에서의 Arrow Function에 대해 한 번 살펴보자.

아래와 같은 예제에서 temp.funcB를 호출할 시, this.value로 어떤 값이 출력될까? 객체 내부에 value: 10으로 정의되어 있으므로 10이 출력될까?

let temp = {
  value: 10,
  funcA: function () {
    console.log("object-funcA");
    console.log(this.value);
  },
  funcB: () => {
    console.log("object-funcB");
    console.log(this.value);
  },
};

temp.funcA();
temp.funcB();

정답은 undefined이다. 대체 왜...?

왜냐하면 객체는 execution context를 생성하지 않기 때문이다.

 

앞서 Arrow Function에서의 this는 상위 execution context의 lexical environment에서 해당 값을 찾는다고 하였는데, 이 예제에서 funcB 메서드는 함수가 아닌 객체 내부에서 정의되어 있다. 따라서 상위 execution context는 global execution context이기 때문에 undefined가 출력된다.

 

그렇다면 아까 Class에서는 왜 정상적으로 동작이 되었을까? Class는 JS에서 function 타입으로서 Class가 생성될 때 function execution context가 생성되기 때문이다.

 

객체의 메서드를 정의할 때 Arrow Function으로 정의하면 안 된다는 이유가 바로 여기서 살펴본 this의 동작 방식 때문이다.

 

 

자 그렇다면 이런 의문을 가질 수 있다.

"객체는 execution context를 생성하지 않으니까 Arrow Function을 일반 함수로 한 번 감싸주면, 일반 함수가 Object Method에 의해 암시적으로 객체의 값이 바인딩되니까 괜찮지 않을까?"

 

위 의문에 대해 아래 예제를 통해 다뤄보겠다.

let temp = {
  value: 10,
  funcB: () => {
    console.log("object-funcB");
    console.log(this.value);
  },
  funcC: function () {
    console.log("object-funcC");
    setTimeout(this.funcB, 100);
  },
  funcD: function () {
    console.log("object-funcD");
    setTimeout(() => {
      console.log("object-funcD-1");
      console.log(this.value);
    }, 100);
  },
};

temp.funcC();
temp.funcD();

funcB는 Arrow Function으로 정의되어 있고, funcC에서는 이를 일반 함수로 감싸고 있다. 그리고 아래에서 Object Method를 사용하고 있기에 temp 객체가 funcC의 this에 동적으로 바인딩될 것이다. 여기서 Arrow Function은 상위 Context의 lexical environment를 참조한다고 했으므로 왠지 10이 정상적으로 출력될 것 같다.

 

하지만 마찬가지로 undefined가 출력된다.

왜냐하면 Arrow Function의 this가 정적으로 바인딩되는 시점은 함수가 정의되는 시점이기 때문이다. funcB는 이미 위에서 먼저 정의되었고, 이때 global execution context에 정적으로 바인딩되었다. 따라서 funcC에서 이를 호출하더라도 funcB의 this 값은 undefined로 고정되어 있다.

 

대신에 funcD는 funcB를 호출하는 것이 아니라 Arrow Function을 직접 작성했는데, 이 경우에는 funcD의 this 값이 temp 이므로 Arrow Function이 정의될 때 상위 Context의 lexical environment가 temp 여서 10을 출력하게 된다.

 

 

지금까지 this의 동작 방식에 대해 알아보았다.

 

정리하자면, this는

1. 일반 함수로 정의했을 때와 Arrow Function으로 정의했을 때 그 동작 방식이 다르며,

2. 또한 일반 함수 정의 방식에서는 함수가 호출되는 방식에 따라 그 동작 방식이 달라진다.

 

긴 글 읽어주셔서 감사합니다! 제 실력이 부족하여 본문의 내용이 실제 동작 원리와 다를 수 있습니다. 그러한 경우에는 댓글로 정정해주시면 감사하겠습니다!!!

728x90