1. Function’s ‘this’ 👩‍💻

JavaScript 의 this는 Swift 의 self 또는 타 언어에서의 this와 다른 행동을 보인다. Runtime 에 의해 함수가 실행되는 context 가 바뀌기 때문이다.

  • Normal Function
function printThis() {
  console.log(this)
}

printThis()      // Window
  • Arrow Function
const printThis = () => console.log(this)

printThis()      // Window

window context 상에서 printThis() 가 호출되었으므로(실제로 printThis()window.printThis()로 호출된 것이다), 이때의 this는 호출한 객체 window를 가리킨다. 다른 프로그래밍 언어와 달리 객체 메서드가 아님에도 불구하고 this에 접근되는 것이 의아할 수 있으나 JavaScript 에서 모든 함수는 Function 객체이기 때문이다.


2. Method’s ‘this’ 👩‍💻

1. Synchronous Code

  • Normal Function
const Banana = {
  name: 'Banana',
  color: 'Yellow',
  printThis() {
    console.log(this)
  }
}

Banana.printThis()  // {name: 'Banana', color: 'Yellow', printThis: f}

Banana context 상에서 printThis() 가 호출되었으므로, 이때의 this는 호출한 객체 Banana를 가리킨다.
일반 함수를 메서드로 사용했을 때는 다른 언어와 차이점이 없는 것 같아 보인다.

  • Arrow Function
const Banana = {
  name: 'Banana',
  color: 'Yellow',
  printThis: () => console.log(this)
}

Banana.printThis()  // Window

이상하게도 Banana의 메서드 printThis() 를 호출했는데, thiswindow다!

2. Asynchronous Code

이번에는 위 예제를 조금 변형해보자.

  • Normal Function
const Banana = {
  name: 'Banana',
  color: 'Yellow',
  printThis() {
    setTimeout(function () {
      console.log(this);
    });
  },
};

Banana.printThis()  // Window
const Banana = {
  name: 'Banana',
  color: 'Yellow',
  printThis() {
    setTimeout(() => console.log(this));
  },
};

Banana.printThis()  // {name: 'Banana', color: 'Yellow', printThis: f}
  • Arrow Function
const Banana = {
  name: 'Banana',
  color: 'Yellow',
  printThis: () =>
    setTimeout(function () {
      console.log(this);
    }),
};

Banana.printThis(); // Window
const Banana = {
  name: 'Banana',
  color: 'Yellow',
  printThis: () => setTimeout(() => console.log(this)),
};

Banana.printThis(); // Window

객체의 메서드는 Normal Functions를, 메서드 내에서 비동기 코드(setTimeout)에는 Arrow Functions를 사용했을 때만 객체를 this로 가리키고있다.


3. Arrow Function’s ‘this’ 👩‍💻

Function’s ‘this’Method’s ‘this’ 두 결과를 정리해보자

  1. 객체의 메서드는 Normal Functions를 사용해야 객체 자기 자신을 this로 취한다.
  2. 객체의 메서드 내 비동기 코드는 Arrow Functions를 사용해야 객체 자기 자신을 this로 취한다.

위에서 무슨 일이 발생한걸까?

JavaScript 의 this가 갖는 문제점을 먼저 이해해야한다. JavaScript 는 Compile Language 와 달리 Runtime 에 의해 실행되는 함수의 context 가 정해진다. 즉, this 를 고정할 수 없다는 말이다.

위에서 우리는 객체의 메서드는 Arrow Functions가 아닌 Normal Functions를 사용해야 자신이 속한 객체를 this로 취할 수 있음을 확인했다.

그런데 이 메서드 내 코드가 Synchronous 가 아닌 Asynchronous 로 작동하는 경우, 해당 코드가 Normal Functions를 사용하면 this가 객체를 취하지 않는 문제가 발생했다.

const Banana = {
  name: 'Banana',
  color: 'Yellow',
  printThis() {
    setTimeout(function () {
      console.log(this);
    });
  },
};

Banana.printThis()  // Window

window를 가리키는 것일까? Banana context 상에서 printThis() 가 호출되었으나, setTimeout에 의해 비동기로 처리된다.

function () {
  console.log(this);
}

따라서 위 익명 함수는 Queue 의 대기열에 등록된다. 이 Event Loop 는 실행 trigger 가 작동되면 Stack 이 빌 때를 기다린다. 즉, BananaprintThis() 가 실행을 마치고 Stack 에서 제거되기를 기다린다. 이제 Stack 이 비었고, 위 익명함수가 Stack 에 쌓인 후 실행된다.

결론적으로 보면 첫 섹션 1. Function’s ‘this’ 의 함수가 실행되고 있는 것이다. 그렇기 때문에 메서드 내부에 비동기 코드가 포함될 경우 this는 객체를 가리키지 않는다!
다른 언어에서의 this는 이런 방식으로 작동하지 않는다. Runtime 에 의해 함수가 실행되는 context 가 달라지는 문제를 해결하고자 나온 것이 Arrow Functions다.

이것이 바로 JavaScript 의 Arrow Functions가 다른 언어의 Lambda Expressions 과 다른 이유다.
JavaScript 의 Arrow Functions는 단지 Synthetic Sugar 가 아닌 새로 정의된 다른 함수다.

물론 Arrow Functions 없이도 this를 고정시켜 문제를 해결할 수 있다. 하지만 매번 this를 고정하는 것은 번거로울 뿐 아니라 복잡한 코드에서는 이를 추적하기가 쉽지 않을 수 있다.


4. Which one Choose? 👩‍💻

ES6 에서 Arrow Functions 가 소개된 이후로 기존의 Functions 문법을 빠르게 대체하고 있다. Arrow Functions 가 갖는 특징은 다음과 같다.

  1. Lambda Expression 기반으로 가독성이 뛰어나다.
  2. 함수가 호출될 때 동적으로 this 가 binding 되는 일반 함수와 달리 선언할 때 상위 Scope 의 this (= 자기 자신을 감싼 부모 함수this)를 기억한다. 이를 Lexical Scope라 한다.
  3. 자기 자신의 this를 갖고 있지 않다.

letconstvarcontext 문제를 해결했듯이, Arrow FunctionsNormal Functionscontext 문제를 해결하기 위해 나온 것이다. 차이점이 존재한다면, letconstvar를 완전히 대체하는 문법이지만, Arrow FunctionsNormal Functions를 완전히 대체하는 것이 아닌, 서로 다른 두 함수가 공존한다는 것이다.


그러면 언제 어떤 함수를 사용하는 것이 좋을까?

1 ) Functions

일반 함수는 this를 필요로 하지 않는다. 따라서 어떤 Syntax 를 사용하든 무관하다. 단, Hosting 이 필요하다면 Normal Functions를 사용해야하지만 대부분은 선언 순서를 조정하는 것 만으로 문제 해결이 가능하기 때문에 Arrow Functions를 사용해도 문제는 없다.

함수를 Higher-order Functions 로 사용하고자 할 때는 Arrow Functions를 사용하는 것이 유리하다. 물론, Normal Functions를 사용해도 상관 없지만 Arrow FunctionsLambda Expression 기반으로 가독성이 뛰어나기 때문에 주로 Higher-order FunctionsArrow Functions를 사용한다.


2 ) Methods

this를 사용하지 않는 코드라면 어떤 Syntax 를 사용하든 무관하다. 하지만, 메서드는 기본적으로 객체와 상호작용을 하기 위해 존재한다. 그리고 메서드가 수정되며 this 접근이 필요해질 수가 있다. 따라서 메서드는 Normal Functions를 사용하도록 한다.


3 ) Asynchronous Code in Methods

메서드 내 비동기 코드는 Arrow Functions로 작성한다. 비동기 코드는 실행되는 시점에 context 가 현재 메서드가 실행 중인 context 와 다르기 때문이다.


5. Fix ‘this’ in Asynchronous Code 👩‍💻

const Banana = {
  name: 'Banana',
  color: 'Yellow',
  printThis() {
    setTimeout(function () {
      console.log(this);
    });
  },
};

Banana.printThis()  // Window

메서드의 비동기 코드에서의 this 문제는 Arrow Functions 없이도 해결할 수 있다고 했다. 해당 문제를 해결할 수 있는 모든 방법을 정리해본다.

1. Closures

const Banana = {
  name: 'Banana',
  color: 'Yellow',
  printThis() {
    const self = this;
    setTimeout(function () {
      console.log(self);
    });
  },
};

Banana.printThis()  // {name: 'Banana', color: 'Yellow', printThis: f}

Closures 를 사용하면 printThis() 의 Closure Scope 가 context 를 기억할 수 있다.

2. bind, call, apply

JavaScript 의 모든 Function 객체는 bind, call, apply 메서드를 갖고 있다.

  • bind: binding this + arguments
  • call: binding this + arguments + trigger
  • apply: binding this + arguments(with array) + trigger


const Banana = {
  name: 'Banana',
  color: 'Yellow',
  printThis() {
    setTimeout(
      function () {
        console.log(this);
      }.bind(this)
    );
  },
};

Banana.printThis(); // {name: 'Banana', color: 'Yellow', printThis: f}

익명 함수의 bind 메서드를 사용해 this를 주입해 고정시킬 수 있다.


위에서 bind 메서드를 사용해 문제를 해결했다. 기본적으로 bind, call, apply 메서드는 this를 주입할 수 있다는 공통점을 갖지만 사용법이 조금 다르다. callapply 메서드와는 어떤 차이가 있을까?

에제를 위해 Banana, Apple, Grape 객체와 priceNow 함수를 준비한다.

const Banana = { name: 'Banana', color: 'Yellow' }
const Apple = { name: 'Apple', color: 'Red' }
const Grape = { name: 'Grape', color: 'Purple' }

function priceNow(price, count) {
    console.log(`${this.name} is ${price} won and there are ${count} left.`)
}
priceNow(2000, 5)
// is 2000 won and there are 5 left.

priceNow는 메서드가 아니라 함수다. 그리고 window에 name 이라는 변수가 존재하지 않으므로 이름이 출력되지 않는다.


1 ) bind

const bananaPrice = priceNow.bind(Banana)
bananaPrice(4500, 3)
// Banana is 4500 won and there are 3 left.

bind 메서드를 사용해 this를 주입시켰다. bind 메서드가 call, apply 메서드와 다른 점은 trigger 를 분리시킬 수 있다는 것이다.

위와 같이 this만 미리 binding 시키는 것은 물론, 아래와 같이 arguments 까지 미리 정의한 다음 trigger 자체만 나중에 호출되도록 분리시키는 것도 가능하다.

const applePrice = priceNow.bind(Apple, 5200, 6)
applePrice()
// Apple is 5200 won and there are 6 left.


2 ) call & apply

call, apply 메서드는 bind 메서드와 달리 trigger 를 분리시킬 수 없다. 함수의 호출까지 한 번에 이루어진다. bind 메서드와 어떻게 다른지 알아보자.

const applePrice = priceNow.bind(Apple)
applePrice(5200, 6)
// Apple is 5200 won and there are 6 left.
const applePrice = priceNow.bind(Apple, 5200, 6);
applePrice();
// Apple is 5200 won and there are 6 left.
priceNow.call(Apple, 5200, 6);
// Apple is 5200 won and there are 6 left.
priceNow.apply(Apple, [5200, 6]);
// Apple is 5200 won and there are 6 left.

함수의 context 를 주입해 새로운 함수를 만들어내거나, 호출을 분리시키는 등 재사용이 필요하다면 bind를 사용하고, 단지 함수를 호출할 때 this의 고정이 필요한 경우는 call 또는 apply를 사용하면 된다. callapply는 함수의 Arguments 를 각각 변수로 받을지, Arrays 로 받을지의 차이만 갖는다.

3. Arrow Functions

const Banana = {
  name: 'Banana',
  color: 'Yellow',
  printThis() {
    setTimeout(() => console.log(this));
  },
};

Banana.printThis(); // {name: 'Banana', color: 'Yellow', printThis: f}

메서드 내 비동기 코드를 작성할 때 Arrow Functions가 갖는 장점이다. 위의 두 방법보다 훨씬 간편하다! 단지 함수의 Syntax 를 바꾸는 것 만으로 this 를 직관적으로 고정시킬 수 있다.


6. Prototypes 👩‍💻

1. Normal Functions

Arrow Functions 를 사용할 때 주의해야하는 경우 한 가지를 더 알아보자. 바로 Prototype 에 메서드를 등록할 때다. 일반적으로 Prototype 에 메서드를 등록하는 방법은 두 가지가 존재한다.

  • 방법 1: ES5 Constructor Function
function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.greet = function () {
  console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`)
}

const person = new Person('홍길동', 25)
person.greet()  // Hello, my name is 홍길동 and I'm 25 years old.

아래의 Object Literal 을 사용한 객체 생성과 혼동하지 않도록 주의한다.

const person = {
  name: '홍길동',
  age: 25,

  greet() {
    console.log(
      `Hello, my name is ${this.name} and I'm ${this.age} years old.`
    );
  },
};

person.greet(); // Hello, my name is 홍길동 and I'm 25 years old.
  • 방법 2: ES6 Class Syntax
class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  greet() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`)
  }
}

const person = new Person('홍길동', 25)
person.greet()  // Hello, my name is 홍길동 and I'm 25 years old.

ES6 의 Class Syntax 는 생성자 함수와 Prototype 을 통한 메서드 등록을 내부적으로 처리해준다.

2. Arrow Functions

Arrow Function 은 Lexical Scope를 갖기 때문에 다음과 같이 재미있는 결과를 얻는다.

  • ES5 Constructor Function
function Person(name, age) {
  this.name = name
  this.age = age

  Person.prototype.greet = () => {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`)
  }
}

const person = new Person('홍길동', 25)
person.greet()  // Hello, my name is 홍길동 and I'm 25 years old. 

Person 의 prototype 에 메서드를 등록하는 코드를 Person 생성자 함수 안에 작성하면, Arrow Functions를 사용함에도 this가 객체 자기 자신을 가리킨다.
Which one Choose? 에서 설명했듯이, Arrow Functions 는 Lexical Scope로 자기 자신을 감싼 부모 함수this를 사용하기 때문에, 부모 함수인 Person 생성자 함수의 this를 사용한다. 따라서 Object Literal 방식과 달리 this가 객체 자신을 가리키게 된다.

  • ES6 Class Syntax
class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  greet = () => {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`)
  }
}

const person = new Person('홍길동', 25)
person.greet()  // Hello, my name is 홍길동 and I'm 25 years old.

결과를 보면 this가 정상적으로 객체 자신을 가리킨다. 즉, ES6 의 Class Syntax 는 바로 위 코드와 동일하게 prototype 을 통한 메서드 등록 코드가 생성자 함수 내부에 존재하도록 정의되는 Synthetic Sugar 라고 볼 수 있다.

그렇기 때문에 단순히 Synthetic Sugar 에 가까운 문법이라고 하더라도, JavaScript 의 Classes 는 다른 언어의 Classes 와 유사한 경험을 제공해줄 수 있다.


주의!

하지만 Prototype 에 메서드를 등록할 때 해당 코드가 생성자 함수 밖에 존재할 때는 Arrow Functions를 사용해서는 안 된다.

function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.greet = () => {
  console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`)
}

const person = new Person('홍길동', 25)
person.greet()  // Hello, my name is  and I'm undefined years old.

선언할 때 상위 Scope, 즉 코드상 상위 Scope 가 window이기 때문이다!