[JavaScript] This (바인딩 룰, 화살표 함수)

[10분 테코톡] 🥦 브콜의 This(15분) 를 보고 정리하면서 추가적인 내용이 있는 글입니다 :)


1️⃣ What is This?

🤔 This가 무엇일까요?

일반적으로 객체 지향 언어에서 this라는 예약어는 함수가 속해 있는 객체 자기 자신과 굉장히 관련이 깊습니다. 이러한 사실을 통해 우리는 자바스크립트도 얼추 비슷한 의미겠구나 하고 조심스럽게 예측할 수 있습니다. 그러나 자바스크립트에는 자기 자신이라는 말이 상당히 모호합니다.


☑️ 자바스크립트 함수객체, 그중에서도 일급 객체이다.

  • 자바스크립트 함수는 변수나 데이터에 할당할 수 있습니다.

  • 자바스크립트 함수는 다른 함수의 인수로 전달할 수 있습니다.

  • 자바스크립트 함수는 함수의 반환 값으로 사용 가능합니다.


그래서 자바스크립트 함수는 단독으로 호출되는 것은 물론, 특정 개체에 메소드로 호출 되기도 하고 심지어 동일한 함수가 서로 다른 객체에 의해서 호출되기도 합니다. 이러니 자바스크립트 함수에서 자기 자신이라는 표현을 사용하는게 엄청 까다로운 일입니다.

함수가 선언된 이후에 어떤 환경에서 어떤 객체에 의해 호출되지 예측할 수가 없기 때문입니다. 그만큼 this도 생각보다 단순하지는 않습니다.


This는 '네, 나' 와같은 1인칭 소유격 표현과 관련이 있습니다. 완벽하게 같은 단어 같은 문장으로 이루어진 말이라도 누가 하느냐에 따라 1인칭 소유격 표현의 의미가 달라집니다.

자바스크립트의 함수도 이와 비슷합니다. 완벽하게 같은 함수라도 어떤 실행 환경에서 어떤 객체에 의해 호출되냐에 따라서 this의 의미가 많이 달라집니다.


📝 지금까지 배운 This 정리

  • 자바스크립트에서 모든 함수는 this를 가지고 있습니다. 그리고 함수가 호출 되면 그때 그때 상황에 따라 this가 가리키는 객체가 결정됩니다. 이렇게 함수가 호출 될 때마다 this가 동적으로 결정되는 것을 "this가 그 객체에 binding 된다"라고 표현합니다.

☑️ 자바스크립트 엔진

자바스크립트 엔진 → 실행 가능한 코드 → 실행 문맥

자바스크립트 엔진은 프로그램을 실행 하면 모든 실행 가능한 코드를 평가해서 실행 문맥이라는 것을 만듭니다. 실행 가능한 코드란 전역 코드, 함수 코드, eval 코드 등을 말하는데 각각의 실행 가능한 코드별로 실행 문맥이 하나씩 만들어 집니다.

이때 실행 문맥은 '렉시컬 환경 컴포넌트', 'this 바인딩 컴포넌트' 등 실제로 실행이 필요한 컴포넌트들로 이루어져 있는데 'this 바인딩 컴포넌트'에는 우리가 알고 있는 this에 대한 정보가 담기게 되는 것입니다.


☑️ 프로그램이 실행이 되면

  1. 자바스크립트 엔진은 전역코드를 평가해서 '전역 실행 문맥'을 만듭니다.

  2. 함수가 실행이 되면 전역 코드 실행을 잠깐 멈추고 또 다시 실행 문맥을 만듭니다.

    ❗️ 바로 이 타이밍에서 this 바인딩 컴포넌트의 값이 결정됩니다.


일반적으로 this는 점 앞에 있는 객체 즉 호출 당시 함수를 포함하고 있는 객체에 바인딩됩니다. 근데 여기서 의문이 생길 수 있습니다.

  • 함수가 객체에 의해 호출이 되지 않는 상황에서는 this가 어떻게 바인딩 될까요? 🤔

  • 단독으로 호출되는 함수 혹은 생성자 함수에서는 우리가 this를 어떻게 이해하면 좋을까요? 🤔


2️⃣ This Binding Rules

기본 바인딩 / 암시적 바인딩 / new 바인딩 / 명시적 바인딩

각 실행 문맥에서 this를 바인딩하는 데에는 일정한 규칙들이 존재합니다. this는 기본적으로 네가지 규칙에 의해서 바인딩 됩니다. 함수가 호출되는 상황에 따른 규칙들이고 이 규칙들 사이에는 분명하게 우선순위가 존재합니다.

1. 기본 바인딩

자바스크립트 함수를 호출하는 가장 기본적인 방법은 바로 단독 실행하는 것입니다. 함수를 단독 실행하게되면 this는 기본적으로 전역객체에 바인딩 됩니다. 따라서 브라우저 실행 환경에서는 전역 객체인 윈도우 객체에 바인딩 됩니다.

그런데 이때 조심해야 될것은 use strict 키워드를 통해서 엄격모드를 사용하게 되면 전역객체가 기본 바인딩 대상에서 아에 제외됩니다. 따라서 이 경우에는 this가 바인딩 될 객체가 존재하지 않기 때문에 undefined 값을 가지게 됩니다.


그럼 노드 환경에서는 어떨까요? 마찬가지로 this가 전역객체에 바인딩이 되는데 노드의 전역객체는 글로벌이므로 global 객체에 바인딩 됩니다.

특별하게 노드 환경에서는 상황마다 다르게 바인딩 되는 경우가 있습니다. 함수 코드 안에서가 아니라 젼역 코드상에서 this를 출력해보면 빈 객체가 나오게 됩니다. 이 빈 객체는 사실 모듈 객체에 있는 exports 객체와 완벽하게 동일한 객체입니다.

이를 통해서 우리는 Node JS 환경에서 this가 두 가지 방향으로 바인딩 되는구나 라고 알 수가 있습니다.


2. 암시적 바인딩

자바스크립트의 함수는 단독으로 호출될 수 있을 뿐만 아니라 객체의 메소드로도 호출이 됩니다. 이 경우에는 아까 언급했듯이 this가 점 바로 앞에 있는 객체에 바인딩 됩니다. 그리고 이렇게 바인딩 되는 방식을 '암시적 바인딩'이라고 부릅니다.


우리가 암시적 바인딩 되는 경우에서 이제 함수를 사용할 때는 조금 조심해야 할 부분이 있습니다.

const obj = {
  name: 'beuccol',
  getName() {
    return this.name;
  }
};

function showReturnValue(callback) {
  console.log(callback());
}

showReturnValue(obj.getName); //undefined
  1. obj라는 객체가 있고 그 안에 name이라는 프로퍼티가 있습니다. getName이라는 메소드가 있어서 name이라는 프로퍼티를 반환해 줍니다.

  2. 전역에는 showReturnValue라는 함수가 있어서 callback을 인자로 받고 callback에 대한 반환값을 콘솔에다가 출력을 해주는 간단한 함수입니다.


그런데 여기서 showReturnValue에다가 obj.getName을 인수로 넘기면 예상과는 다르게 undefined가 출력됩니다 😮??

  1. 자바스크립트에서 객체를 할당한 변수는 해당 객체에 대한 참조값을 저장합니다.

  2. obj.getName프로퍼티에는 getName함수에 대한 참조가 들어있습니다.

  3. 참조 값을 함수의 인수로 넘기게 되면 함수는 이 참조값을 복제해서 사용을 하게 됩니다.
    (같은 객체를 참조하는 또다른 변수를 만들어서 함수안에서 사용하게 됩니다.)

  4. showReturnValue에 obj.getName을 전달하면 동일한 함수를 참조하는 또 다른 reference가 콜백 변수에 저장 되어서 사용됩니다.

  5. obj.getName과 callback은 완전히 동일한 함수 getName을 참조하고 마찬가지로 동일한 방식으로 괄호를 붙여서 함수를 호출합니다.
    (❗️ 그런데 한 번이라도 복제가 일어나고 다른 변수를 거치면 감쪽같이 바인딩이 안 되는 상황입니다.)


이런 상황이 발생하는 비밀은 바로 점 연산에 있습니다. 점 연산이나 대괄호 연산을 통해서 객체의 프로퍼티에 우리가 접근을 하면 '참조 타입(reference type)'이라고 하는 특별한 값을 반환 해 줍니다.

참조 타입은 자바스크립트 명세서에서만 사용되는 타입인데 '프로퍼티를 가지고 있는 객체'와 'strict 모드인지 아닌지'에 대한 여부를 같이 가지고 있는 일종에 하나의 타입입니다.

(base, name, strict)

base: 객체 / name: 프로퍼티 이름 / strict: 엄격모드 true

예를 들어서 엄격 모드에서 obj.getName에 접근을 하면 obj.getName(obj, getName, true)와 같은 것을 얻게 됩니다. obj.getName 같은 참조 타입에 괄호를 붙여서 함수를 호출하게 되면, 함수안에 있는 this가 참조 타입에 있는 객체를 찾아서 바인딩 됩니다. 이러한 방식의 바인딩을 암시적 바인딩이라 부르게 되는 것입니다.


근데 점 연산이나 대괄호 연산을 제외한 다른 연산들은 참조 타입이 아닌 해당 프로퍼티의 값만 전달을 하게 됩니다. 따라서 아무리 점 연산을 통해 참조 타입을 얻어냈다고 하더라도 다른변수에 할당을 하는 순간 프로퍼티의 값 혹은 그 프로퍼티가 참조하고 있는 참조 값만 남게 됩니다.

그래서 이 상황에서 점 연산을 통해 얻은 값은 바로 함수로 호출을 하지 않고서는 우리가 암시적 바인딩을 기대할 수가 없습니다. 마찬가지로 인수로 전달된 콜백의 참조도 객체에 대한 어떠한 정보도 포함을 하지 않습니다. 그래서 함수로 단독 호출한 것과 같이 동작을 하게 되는 것입니다.

쉽게 정리하자면 점 연산 이외의 연산(할당 연산 등)은 참조 타입을 통째로 버리고 obj.getName 값(함수)만 받아 전달합니다. 이 때문에 점 이외의 연산에선 this 정보가 사라집니다.

obj.getName() 같이 점을 사용하거나, obj[getName]() 같이 대괄호를 사용해 함수를 호출했을 때만 this 값이 의도한 대로 전달됩니다. 이런 문제는 func.bind() 등을 이용하면 해결 할 수 있는데, 이에 대해선 추후에 알아보도록 하겠습니다.


정상적인 코드는 다음과 같습니다.

자세한 설명은 다음 챕터인 명시적 바인딩에서 확인하실 수 있습니다!

const obj = {
  name: "beuccol",
  getName() {
    return this.name;
  }
};

function showReturnValue(callback) {
  console.log(callback.call(obj));  // ⭐️ call 메소드 사용
}

showReturnValue(obj.getName); // beuccol
const obj = {
  name: "beuccol",
  getName() {
    return this.name;
  }
};

function showReturnValue(callback) {
  console.log(callback()); 
}

// ⭐️ bind 메소드 사용
showReturnValue(obj.getName.bind(obj)); // beuccol

3. 명시적 바인딩

다행히도 자바스크립트는 this를 특정 객체에 암시적으로 바인딩할 뿐만 아니라 명시적으로도 바인딩할 수 있는 방법을 제공합니다. 이를 통해 this가 소실되는 문제를 해결할 수 있습니다.

☑️ call, apply, bind

함수 객체는 call, apply, bind등의 메서드를 통해서 명시적으로 this를 바인딩 할 수 있는 방법을 제공합니다.

call(context (this 👀), arg1, arg2, ...)

apply(context (this 👀), args)

call과 apply를 사용하면 this를 바인딩할 객체를 지정한 상태로 함수를 호출할 수 있습니다.

첫 번째 인자로 this를 바인딩할 객체를 전달하고 그 다음에 arg1, arg2, ... 에 인수를 넣어줍니다.

call 과 apply는 뒤에 인수를 전달하는 방식만 차이가 있습니다.

  • call은 인수를 하나하나 전달하는 방식을 채택하고 있습니다.

  • apply는 인수들을 배열 형태나 유사배열 형태로 전달 해 주고 있습니다.

func.call(context, ...args); // 전개 문법을 사용해 인수가 담긴 배열을 전달하는 것과
func.apply(context, args);   // call을 사용하는 것은 동일합니다.

func.bind(context (this 👀), arg1, arg2, ...)

bind 메서드는 this가 참조하는 객체를 고정시켜 줍니다. 마찬가지로 첫 번째 인자의 this를 바인딩할 객체를 넣어주고 이후에는 인수로 사용될 값들을 넣어줍니다.

func.bind(context)는 함수처럼 호출 가능한 '특수 객체(exotic object)'를 반환합니다. 이 객체를 호출하면 this가 context로 고정된 함수 func가 반환됩니다. 이렇게 항상 같은 객체에 바인딩 되도록 강제 하는 방법을 '하드 바인딩'이라고도 부릅니다.

let user = {
  firstName: "John"
};

function func() {
  alert(this.firstName);
}

let funcUser = func.bind(user);
funcUser(); // John

4. new 바인딩

자바스크립트 함수를 new 연산자와 함께 호출 하게 되면 생성자 함수로서의 역할을 수행할 수 있게 됩니다. 이 과정을 아주 간략하게 설명해보겠습니다.

  1. new 연산자로 호출하면 새로운 객체가 생성됩니다.

  2. 새로 생성된 객체의 prototype이 연결됩니다.

  3. 함수의 코드를 실행합니다.(this 바인딩 👀)

  4. 새로 생성한 객체를 반환합니다.

프로퍼티가 정해진 객체를 반환하는 생성자의 역할을 수행할 수 있습니다. 이렇게 new 연산자로 함수를 호출할 때 this가 바인딩 되는 규칙을 new 바인딩 이라고 합니다.

function User(name) {
  this.name = name;
  this.hello = function(){
    console.log(this.name);
  }
}

const user1 = new User('kim');

user1.hello() //'kim'

💪🏻 바인딩의 우선 순위

new 바인딩 > 명시적 바인딩 > 암시적 바인딩 > 기본 바인딩

함수가 매번 위의 4가지 케이스중 하나만 해당하면 좋을 것 같습니다. 하지만 코딩을 하다 보면 중복으로 해당되는 경우가 있을 수 있습니다. 이 경우에는 this가 우선순위에 따라 바인딩 됩니다. 그리고 어떠한 것에도 해당되지 않을 때에는 전역 객체나 undefined값을 가지게 됩니다.

☑️ this 의 확정규칙

  1. new 로 함수를 호출 했는가 ?
    => 새로 생성된 객체가 this 다.

  2. call과 apply로 함수를 호출, bind 하드 바인딩 내부에 숨겨진 형태로 호출 했는가 ?
    => 명시적으로 지정된 객체가 this다.

  3. 함수를 constext 즉 객체를 소유 또는 포함하는 형태로 호출 했는가 ?
    => constext가 this

  4. 그 외의 경우에는 this 는 기본값(비엄격모드에서는 전역, 엄격은 undefined) 로 셋팅
    => 기본 바인딩


3️⃣ This in Arrow Function

화살표 함수로 일어나는 바인딩은 특별 합니다.

const obj = {
  name: 'beuccol',
  showNameInSec(sec) {
    setTimeout(() => {
      console.log(this.name);
    }, sec);
  },
};

obj.showNameInSec(1000); // beuccol

만약에 setTimeout안의 콜백함수에서 쓰이는 화살표함수가 다른 일반 함수처럼 바인딩 했으면, 딱봐도 문제가 생길 것 같습니다. 아까 위에서 말했듯이 callback으로 넘겨버리면 this가 의미를 잃어버린 상황이기 때문입니다. 그런데 자바스크립트는 정상적으로 1초 뒤에 'beuccol'이라는 문자열을 출력합니다.

화살표 함수는 일반 함수와 많은 차이점을 가지고 있고 그만큼 사용 목적도 다릅니다. 화살표 함수를 사용하는 가장 큰 목적중 하나는 상위 실행 문맥을 유지하는 것이라고 합니다. 렉시컬 스코프와 관계없이 당시에 의존하는 기존의 binding 규칙은 화살표 함수 안에서 this에게 전혀 영향을 끼치지 않습니다.

화살표 함수 안에서 this는 선언될 당시에 상위 실행 문맥, 그 스코프에 해당하는 실행 문맥 상의 this 바인딩 컴포넌트를 참조합니다. ❗️쉽게 말해서 상위 스코프의 this를 가르킨다고 보면 됩니다. 따라서 이 코드 상에서는 화살표 함수 안에 있는 this가 showNameInSec 함수 실행 문맥에 있는 this와 같은 것입니다.

이러한 this의 동작하는 모습을 보고 있으면 마치 외부 렉시컬 환경(상위 스코프)가 변수를 참조하는 모습과 비슷합니다. 이런 특징을 갖는 this를 어휘적 this(= 렉시컬 this) 라고도 부릅니다. this의 특징들을 잘 파악하고 적절한 곳에 사용한다면 조금 더 유연하게 프로그래밍을 할 수 있습니다.



🌐 참조 링크

좋은 웹페이지 즐겨찾기