Arrow Function과 this 키워드

이효범·2022년 4월 14일
0

ES6 - OOP & FRP

목록 보기
8/15
post-thumbnail

Arrow Function

다음은 일반적인 기존의 함수 작성 방식이다.

const add = function(a, b) {
 return a + b; 
}

add(1, 2);

위의 function의 스타일을 다음과 같이 arrow function 형태로 바꿔줄 수 있다.

const add = (a, b) => a + b; 

add(1, 2);

위와 같이 작성해도 기존의 함수의 동작과 동일하게 동작하게된다.
만약 인수를 하나만 받을 경우에는 다음과 같이 인자를 둘러싸던 소괄호 또한 생략해줄수 있다.

const add =a => a + 2; 

add(1);

즉 굉장히 간명하게 조금 더 간명하게 함수를 표현할 수 있고 또한 적은 양의 코드를 쓸 수 있는데, 글자 수가 적으면 그만큼 우리가 코딩하면서 에러가 발생할 확률도 낮아지게 되므로 앞으로는 가능한 이러한 arrow function 형태로 작성을 하는 것이 좋다.

다른 예도 하나 더 살펴보자.

const numbers = [ 2, 4, 6, 8 ];

numbers.map(function(number) {
 return number * 2; 
});

위의 코드를 arrow function을 써서 좀 더 간결한 코드로 작성해보자.

const numbers = [ 2, 4, 6, 8 ];

// add fat arrow and delete function keyword
numbers.map((number) => {
  return number * 2;
});

// OR
// compose in one line and delete return keyword
numbers.map(number => number * 2); 

// 혹은 인자가 없는 경우 다음과 같이 작성해도 문법적으로 아무런 이상이 없다.
numbers.map(number => 2); 
// 다음과 같은 경우도 뭘 받는 간에 2를 리턴하는 함수다.
// 만약 인자가 없으면 다음과 같이 빈 소괄호를 넣어준다.
// 만약 인자가 두 개 이상이라면 빈 소괄호는 생략이 불가능하다. 하나라면 생략이 가능하다.
const returnTwo = () => 2;

this 키워드

다음 코드를 봐보자.

const superHeroes = {
 members: ['Supermen', 'Batman', 'Spiderman', 'Ironman'],
 teamName: 'Super Heroes',
 teamSummary: function() {
  			    return this.members.map(function(member) {
         		  console.log(`${member} is a member of ${this.teamName}`); 
        	    });
			  },
};

  // S + V('this' keyword)
superHeroes.teamSummary();

위의 코드를 보면 superHeroes 는 세 개의 프로퍼티를 가지고 있고,
그 중 teamSummary 라는 메소드를 가지고 있다.
그래서 밑의 superHereos.teamSummary(); 라는 코드는 주어가 superHeroes이고 동사가 teamSummary 인 형태인데, 즉 여기서의 동사는 객체 안의 메소드를 뜻한다.

위의 주석에 적혀있듯 우리는 이 동사 속에 this 라는 키워드를 쓰기도 한다. 그러한 상황에서 이 this는 동사와 어울리고 있는 S, 주어를 가리키게 된다. 따라서 위의 superHereos.teamSummary(); 에서 teamSummary 안에 들어있는 this는 superHeroes를 가리키고 있게 된다. 즉, 어떠한 함수 속에 포함되어있는 this가 가리키는 것은 그 함수를 호출하는 주어 그 자체이다.

이를 쉽게 풀어서 말하자면 teamSummary는 this.members.map.... 의 형태를 리턴하고 있는데, 여기서 this.members의 this가 이 teamSummary 함수(메소드)를 호출하고 있는 주어, 즉 superHeroes 그 자체를 가리키고 있는 것이다.

따라서 superHeroes.members.map(...) 과 동일하게 동작하게 되고 members는 다음과 같다. ['Supermen', 'Batman', 'Spiderman', 'Ironman'].

그리고 이와 마찬가지로 console.log('${member} is a member of ${this.teamName}');this.teamName이 무엇을 가리키는 것인지 이젠 쉽게 해석이 가능해진다. 당연히 'Super Heroes' 아니겠는가?

그러나..? 실제 위 코드를 실행한 결과값은 다음과 같다.

superHeroes.teamSummary();
// Superman is a member undefined
// Batman is a member undefined
// Spiderman is a member undefined
// Ironman is a member undefined

무엇이 문제이길래 왜 undefined가 나오는 것인가?

이러한 문제가 발생하는 이유가 무엇이냐면,
this.teamName 또한 이를 호출하는 주어를 가리켜야 하는데, 이 문장은 단지 console.log라고 하는 함수에 전달되어진 argument일뿐인 것이다.

즉 console.log라고 하는 함수는 위 superHeroes라고 하는 객체와 아무런 상관이 없는 남남인 것이다.

teamSummary 그 자체는 superHeroes라고 하는 객체의 직접적인 자식처럼 취급을 받게 되기때문에 this가 superHeroes를 잘 가르키지만, console.log 안의 this는 console.log라고 하는 전혀 엉뚱한 함수에 인자로 들어간 문자열 중에 포함되어 있는 것이란 말이다. 그래서 얼핏 보면 console.log 안의 this가 superHeroes를 잘 가르킬 것 같지만, 그렇지 않다는 것이다.

function binding

그래서 이를 해결하는 방법이 이를 다음과 같이 bind 하는 것이다.

const superHeroes = {
 members: ['Supermen', 'Batman', 'Spiderman', 'Ironman'],
 teamName: 'Super Heroes',
 teamSummary: function() {
  			    return this.members.map(function(member) {
         		  console.log(`${member} is a member of ${this.teamName}`); 
        	    }.bind(this));
			  },
};

superHeroes.teamSummary();
// Superman is a member Super Heroes
// Batman is a member Super Heroes
// Spiderman is a member Super Heroes
// Ironman is a member Super Heroes

위처럼 binding을 시켜주면 console.log 안의 this도 또한 몽땅 superHeroes를 강제적으로 가르키게 된다.

근데 이러한 방식은 ES6 이전의 방식이고 ES6가 나타나면서 위처럼 하지 않아도 되게 되었다.

어떻게 하면 되냐면 다음과 같이 하면 된다.

const superHeroes = {
 members: ['Supermen', 'Batman', 'Spiderman', 'Ironman'],
 teamName: 'Super Heroes',
 teamSummary: function() {
  			    return this.members.map((member) => {
         		  console.log(`${member} is a member of ${this.teamName}`); 
        	    });
			  },
};

superHeroes.teamSummary();
// Superman is a member Super Heroes
// Batman is a member Super Heroes
// Spiderman is a member Super Heroes
// Ironman is a member Super Heroes

위처럼 기존의 함수를 작성하던 스타일을 Arrow Function의 형태로 작성해주면 좀 더 적은 양의 코드를 사용하면서도 this를 바인딩해줘야 하는 수고로움을 덜을 수 있게 된다.


Arrow Function과 this 키워드 연습

다음 코드를 보고 코드를 수정하고, 빈 공간을 채워가면서 Arrow Function과 this 키워드 에 대한 연습을 해보도록 하자.

// 1. use fat arrow 
const fibonacci = function(n) {
 if (n < 3) return 1;
 return fibonacci(n - 1) + fibonacci(n - 2);
};

// 2. use fat arrow and 'this' keyword.
const profile = {
 name: 'Superman',
 getName: function() {
   
 },
};

const a = profile.getName();
console.log(a); // 'Superman'

다음은 연습문제를 구현한 소스코드다.

// 1. use fat arrow 
const fibonacci = (n) =>  {
 if (n < 3) return 1;
 return fibonacci(n - 1) + fibonacci(n - 2);
};
console.log(fibonacci(9)); // 34

// 2. use fat arrow and 'this' keyword.
const profile = {
 name: 'Superman',
 getName: function() {
   return this.name;
 },
};

const a = profile.getName();
console.log(a); // 'Superman'

참고로 다음과 같이 this를 포함한 arrow function의 형식은 아직 ES6가 지원하지 않는다.

const profile = {
 name: 'Superman',
 getName: () => {
   return this.name;
 },
};

그리고 위의 profile의 메소드인 getName에 this.name 대신 그냥 profile.name을 넣어줘도 당연히 잘 동작한다.

그리고 다음과 같이 return과 curly bracket을 없앤 간명한 형태로도 getName을 리팩토링 할 수 있다.

const profile = {
 name: 'Superman',
 getName: () => profile.name  // 이러한 형태에서는 this는 사용할 수 없다.
};

const a = profile.getName();
console.log(a); // 'Superman', 잘 작동하는 것을 확인할 수 있다.

위와 같은 형태가 너무 허전한 듯 싶으면 () => profile.name을 소괄호로 한번 더 감싸준 뒤 맨 뒤에 쉼표를 넣어주어도 잘 동작한다.

하지만 결국 this는 가급적 사용하지 않는 편을 권장하는 바이다. 이는 코드를 다시 봐야할 때나 다른 사람과 협업하는 과정에서 this 키워드 자체는 직관적이지 못하고 다시금 코드의 구조를 살펴봐야하기 때문이다. 따라서 가급적 직관적인 방법을 선택하는 함수형 프로그래밍 방식이 좀 더 최신의 방식이 아닌가 싶다. 반대로 OOP와 같은 경우는 this를 계속해서 써야함을 기억하자.

하나의 연습 문제를 더 보도록 하자.
히어로들이 긴급출동서비스를 오픈했다. 각 히어로들에 따른 출동 비용이 나와있다. 히어로들을 긴급하게 911 전화로 출동시킬 수 있게 하는 함수를 작성해보도록 하자.
그리고 이러한 함수를 Arrow Function 방식으로 리팩토링 해보자.

const superHeroes = [
  { name: 'Superman', price: 1000 },
  { name: 'Batman', price: 700 },
  { name: 'Spiderman', price: 1500 },
  { name: 'X-man', price: 1100 },
];

const superheroes2 = [
  { name: 'Birdman', price: 1100 },
  { name: 'Antman', price: 7100 },
  { name: 'Aquaman', price: 1550 },
  { name: 'Wonderwoman', price: 11000 },
];

function hero911(arg) {
  return {
   heroes: arg,
   totalHeroesPrice: function() {
      return arg.reduce(function(total, hero) {
       return total + hero.price; 
      }, 0)
   },
   eachHeroPrice: function(name) {
     return arg.find(function(hero) {
      return hero.name === name; 
     }).price;
   }
  }
};

const shopA = hero911(supderHeroes1);
const shopB = hero911(supderHeroes2);

console.log(shopA.totalHeroesPrice());  // 4300
console.log(shopB.totalHeroesPrice());  // 20850
console.log(shopB.eachHeroPrice('Birdman'));  // 1100
// Arrow Function
function hero911(arg) {
  return {
   heroes: arg,
   totalHeroesPrice: function() {
      return arg.reduce((total, hero) => total + hero.price, 0)
   },
   eachHeroPrice: function(name) {
     return arg.find(hero => hero.name === name).price;
   }
  }
};

const shopA = hero911(supderHeroes1);
const shopB = hero911(supderHeroes2);

console.log(shopA.totalHeroesPrice());  // 4300
console.log(shopB.totalHeroesPrice());  // 20850
console.log(shopB.eachHeroPrice('Birdman'));  // 1100

그리고 위의 코드는 또한 더욱 간결하게 다음과 같이 리팩토링 할 수 있다.

function hero911(arg) {
  return {
   heroes: arg,
   totalHeroesPrice: () => arg.reduce((total, hero) => total + hero.price, 0),
   eachHeroPrice: name => arg.find(hero => hero.name === name).price,
  }
};

이러한 형태까지 가면, 처음의 코드에 비해 아주 간결해진 코드가 되고 동작 또한 아주 잘 하는 것을 확인할 수 있다.


profile
I'm on Wave, I'm on the Vibe.

0개의 댓글