TIL11. JS함수 호출 과정과 this

imloopy·2022년 3월 31일
0

Today I Learned

목록 보기
11/56

Today I Learned

오늘은 자바스크립트에서 우리가 모르고 쓰면 예상하지 않은 결과가 나오는 사례들, 그리고 그런 사례들이 왜 나오는지에 대하여 공부했다. 사례들을 살펴보니, 자바스크립트 함수의 호출 과정을 이해하고 있다면, 그리고 스코프에 대해 이해하고 있다면 충분히 예방할 수 있는 사례들임을 깨달았다. 따라서 이미 1일차에 정리했던 내용들을 복습하면서 추가적으로 this 바인딩과 자바스크립트의 특성에 따라 여러 예상하지 못한 결과들의 사례와 이를 예방하기 위한 방법들에 대하여 정리했다.

함수의 호출 과정

이미 1일차에 정리를 했었지만, 아무리 강조해도 지나치지 않기에 한 번 더 정리하기로 한다.
함수의 호출 과정은 크게 다음 세 가지 과정으로 나눌 수 있다.
1. PrepareForOrdinaryCall
2. OrdinaryCallBindingThis
3. OrdinaryCallEvaluateBody

PrepareForOrdinaryCall

함수가 호출된 뒤, 함수를 실행할 수 있는 환경을 만들고 초기화 하는 단계다. 정확히 말하면 실행 컨텍스트(Execution Context)가 생성되는 단계이다. 이 과정에서는 함수 내부의 식별자 정보들을 바인딩하고 정의한다. (정확히는 Execution Context 내부의 Lexical Environment 내부의 Environment Record에 저장된다)

OrdinaryCallBindingThis

this를 바인딩한다. this는 함수 호출 시점에 바인딩하여 결정된다. 함수 호출 주체가 누군지에 따라, this가 결정된다는 것이다. 이는 다른 언어의 this의 의미와는 다르고, 다른 언어와 가장 큰 차이점 중 하나이다. 이러한 자바스크립트 언어 특성 상 다양한 난감한 사례들이 많이 발생하기도 한다.

OrdinaryCallEvaluateBody

함수가 평가되고 값을 반환한다.

This

자바스크립트의 This는 함수 호출 시점에 결정되기 때문에, 함수 호출의 주체가 무엇인지에 따라 달라진다. 이러한 자바스크립트 특성상 여러 재미있는 재미라기보다는 짜증나는 현상들이 발생하기도 한다. 아래 사례들을 살펴보면서 This가 어떤 특성을 갖고 있는지 살펴보자.

사례 1. 호출 주체가 누군지에 따라 this가 무엇인지 달라진다.

var company = {
  name: "MS",
  category: "IT",
  members: {
    person: {
      memberName: "person",
      work: function () {
        console.log(`company ${this.name} ${this.memberName} go to work`);
      },
    },
  },
};

company.members.person.work(); // ??

이 코드를 출력하면 어떤 결과가 나올까?
답은 company undefined person go to work가 출력된다. 이유를 알기 위해 함수의 호출 주체를 먼저 살펴보면, work함수는 membersperson 객체에 의하여 호출되었다. 따라서 여기서 thisperson이 된다. 그런데 person 객체 내부에는 name 키가 없으므로 undefined가 출력된다.

해결 방안

객체 이름을 명시한다.

var company = {
  name: "MS",
  category: "IT",
  members: {
    person: {
      memberName: "person",
      work: function () {
        console.log(`company ${company.name} ${this.memberName} go to work`);
      },
    },
  },
};

company.members.person.work(); // ok

사례 2. 일반적으로 this는 global(window) 객체를 가리킨다.

// JS의 생성자 함수는 대문자로 시작한다.
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.printInfo = function () {
    console.log(this.name, this.age);
  };
}

const hommer = Person("hommer", 30);
hommer.printInfo(); // ?

다음 함수를 실행하게 되면, 에러가 발생한다. TypeError: Cannot read printInfo of undefined 에러가 발생하게 되는데, 자세히 살펴보면 우리가 생성자 함수를 이용하여 새로운 객체를 정의할 때, new 키워드를 사용하지 않았다. 일반 함수에서 this 키워드는 global객체를 나타낸다. new를 이용하여 객체를 정의하면, 그제서야 this를 우리가 정의한 새로운 객체에 바인딩함을 알 수 있다. 따라서 오류를 해결하기 위해서는 다음과 같이 작성해야 한다.

// ...
const hommer = new Person("hommer", 30); // ok

사례 3. this를 내 맘대로 정의하기

function Group(group) {
  this.group = group;
  this.doSomething = function () {
    setTimeout(function () {
      this.group.forEach(function (member) {
        member.doSomething();
      });
    }, 1000);
  };
}

var group1 = new Group([
  {
    name: "group1",
    doSomething: function () {
      console.log("do something");
    },
  },
]);

group1.doSomething();

다음 호출 과정 역시 에러가 발생한다. 그러나 우리는 함수의 흐름만 쫓아갈 수 있으면 어렵지 않다.
doSomething메소드 내부에 setTimeout과 콜백 함수가 존재한다. 콜백 함수 내부에 this.group을 호출하고 있다. setTimeout 내부의 콜백 함수의 thisGroup을 가리키고 있지 않다. this는 함수 스코프를 가지기 때문에, thisglobal객체를 가리킨다.
그런데 global 객체에 group을 정의하지 않았으므로, undefined 에러가 발생하는 것이다.

이를 해결하기 위한 방법은 크게 세 가지가 존재한다.
1. This를 바인딩하지 않는 화살표 함수를 사용한다.
화살표함수는 this를 바인딩하지 않고, 무조건 상위 스코프의 this를 따른다. setTimeout 콜백 함수를 화살표 함수로 작성하면, this는 상위 스코프의 this를 따르게 되고, 상위 scopethisGroup을 정의한 객체를 가리킬 것이므로, 함수를 실행하는데 문제가 생기지 않는다.

function Group(group) {
  this.group = group;
  this.doSomething = function () {
    // 이런식으로
    setTimeout(() => {
      this.group.forEach(function (member) {
        member.doSomething();
      });
    }, 1000);
  };
}
  1. .bind()함수로 this 바인딩하기
    .bind()함수는 호출 주체를 묶어준다. 따라서 .bind(this)를 통해 this를 지정하면, 콜백 함수 내부에서 this를 호출해도 아무런 문제가 생기지 않는다.
function Group(group) {
  this.group = group;
  this.doSomething = function () {
    // 이런식으로
    setTimeout(function() {
      this.group.forEach(function (member) {
        member.doSomething();
      }.bind(this));
    }, 1000);
  };
}
  1. 클로저로 만들기
    클로저는 함수와 주변 lexical environment의 조합이다. 함수가 호출될 때 자신의 주변 환경을 기억하고, 내부 함수에서 외부 스코프의 변수에 접근할 수 있는 함수를 뜻한다. 함수를 클로저로 만들기 위해서는
function Group(group) {
  var that = this;
  this.group = group;
  this.doSomething = function () {
    // 이런식으로
    setTimeout(function() {
      that.forEach(function (member) {
        member.doSomething();
      }.bind(this));
    }, 1000);
  };
}

이런식으로 사용할 수 있다.
몇 가지 사례들을 가지고 실수하기 쉬운 자바스크립트에서 this 호출 과정에 대하여 살펴보았다. 상황에 따라 다르긴 하지만, 기본적으로 다음만 명심하면 될 것 같기도 하다.
1. 일반 function 호출 시, 기본적으로 thiswindow(global)을 가리킨다. 생성자 함수를 통해 new 예약어를 이용하여 객체를 생성하면, 객체를 this에 바인딩한다.
2. 화살표 함수는 this를 바인딩하지 않는다. 무조건 상위 스코프의 this를 따라간다. 따라서 함수의 call, bind, apply 함수를 이용하여 this를 바인딩할 수도 없다.

마치며

원래 this와 클로저를 함께 정리하려고 했는데, 아직 클로저에 대하여 공부가 덜 되었고, this에 대해 정리하는 양이 많아지면서 다음에 따로 정리하기로 했다.

그리고 TIL을 위한 TIL이 아닌, 꼭 내가 정리하면서 얻어가는 TIL이 되었으면 좋겠다.

1개의 댓글

comment-user-thumbnail
2022년 3월 31일

함수의 호출과정에 대해 배우고 갑니다. 좋은 글 감사드려요 🙏

답글 달기