본문 바로가기
Language/Javascript

유용하지만 위험한 화살표함수(=>)

by joooing 2021. 1. 16.
반응형

ES6 문법 중 개인적으로 가장 흥미로웠던 화살표 함수의 활용 방안에 대해 좀 더 알아보았다. 코드가 훨씬 간결해지고 읽기도 편하다는 큰 장점이 있어서 요새 많이 사용해보고 있는데, 보통 함수와는 다르게 조심해서 부분이 있어서 아무 때나 사용하기 보다는 용도에 맞춰 사용해야 할 것 같다. 먼저 화살표 함수를 활용하는 다양한 방법을 소개하고, 마지막에는 화살표 함수를 사용하지 말아야 하는 경우에 대해서도 이야기 해보려 한다.

 

화살표 함수


=> 기호로 함수를 축약해서 표현하는 방식이다. 아래 예시를 보면, 전 ⇒ 후 형태로 표시되기 때문에 훨씬 직관적으로 내용을 파악할 수 있다는 걸 알 수 있을 것이다.

 

✔️ 자신의 this, arguments을 바인딩 하지 않음

 

// 기본 구조
(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression

// 매개변수가 하나면 경우 괄호 생략 가능
(singleParam) => { statements }
singleParam => { statements }

// 매개변수가 없으면 괄호만 사용
() => { statements }

 

// 예시
const sum = (a, b) => a + b;

// 매개변수가 하나면 경우 괄호 생략 가능
const evens = [2, 4, 6, 8,];
const odds = evens.map(v => v + 1);   
// [3, 5, 7, 9]
const pairs = evens.map(v => ({even: v, odd: v + 1})); 
// [{even: 2, odd: 3}, ...]

 

 

화살표 함수 활용하기


1. 화살표 함수 & 클로저

클로저는 함수와 함수가 선언된 '어휘적 환경의 조합'이다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다.

클로저는 한 마디로 '외부함수의 변수에 접근 가능한 내부함수' 이다. 자세한 내용

 

간단한 더하기 연산을 하는 클로저 예제를 일반 함수와 화살표 함수를 사용해 작성해보자. 우선 일반적인 함수로 표현한 모습이다.

 

const adder = function(x) {
	return function(y) {
		return x + y;
	}
}

adder(10)(20);   // 30

 

화살표 함수를 사용하면 위의 코드를 단 한줄로 표현할 수 있다. 화살표 함수를 사용하면 이렇게 연속된 여러개의 화살표로 클로저를 구현할 수 있다.

 

const adder = x => y => x + y;

 

 

2. 화살표 함수 & bind

화살표 함수는 자신만의 this값을 생성하지 않는다. 대신 그냥 자신을 포함하는 외부 컨텍스트의 this를 이어 받는다. 모든 함수는 호출될 때 그 함수만의 '실행 컨텍스트'를 생성하는데, 보통 this값을 특정한 값으로 지정하는 바인딩 과정을 거친다. 하지만 화살표함수는 이 과정을 생략해버린다. 대신 사용자가 this값에 접근하려고 하면, 스코프 체인상에서 가장 가까운 곳(보통 상위 스코프)의 this값을 가져와 사용자가 그 값을 this값으로 사용할 수 있게 해준다.

 

기존에는 아래와 같이 this를 생성자 함수(SayHi)가 생성한 인스턴스(greeting)로 지정하기 위해 bind를 써주어야 했다.

 

function SayHi(hi) {
  this.hi = hi;
}

SayHi.prototype.toEveryone = function (arr) {
  return arr.map(function (el) {
    return this.hi + ' ' + el;
  }.bind(this));
};

let greeting = new SayHi('Hi');
console.log(greeting.toEveryone(['주혜', '하랑']));
// ['Hi 주혜', 'Hi 하랑']

 

하지만 화살표 함수을 사용하면 bind 없이도 상위 스코프의 this값(greeting)을 가져오기 때문에 더 간결하게 쓸 수 있다. 여기선 toEveryone메서드 내부에서 this를 사용하는데, 실행부분인 greeting.toEveryone(['주혜', '하랑']) 이 부분에서 이 메서드의 상위스코프를 확인할 수 있다. (메서드는 자신 왼쪽부분(상위객체)을 this로 지정한다)

 

function SayHi(hi) {
  this.hi = hi;
}

SayHi.prototype.toEveryone = function (arr) {
  return arr.map(el => `${this.hi} ${el}`);
};

let greeting = new SayHi('Hi');
console.log(greeting.toEveryone(['주혜', '하랑']));
// ['Hi 주혜', 'Hi 하랑']

 

 

3. 화살표 함수 & setTimeout

setTimeout은 시간 지연을 일으켜 함수를 '비동기적'으로 실행시키는 함수이다. 이 함수는 명시적으로 항상 전역 객체를 this 바인딩하는데, 이로 인해 아래와 같은 에러가 발생할 수 있다.

 

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  
  getArea() {
    return this.width * this.height;
  }

  printArea() {
    console.log('넓이 : ' + this.getArea());
  }
  
  printSync() {         // 사각형 넓이 즉시 표시
    this.printArea();
  }
  
  printAsync() {        // 사각형 넓이 2초 후 표시
    setTimeout(this.printArea, 2000);
  }
}

let box = new Rectangle(40, 20);
box.printSync();   // '넓이 : 800'
box.printAsync();  // TypeError

 

// 에러 메세지
Uncaught TypeError: this.getArea is not a function at printArea (<anonymous>:12:36)

 

이런 에러는 화살표 함수를 통해 해결이 가능하다. setTimeout 함수 내부에서 다른 함수가 호출되더라도, 그게 화살표 함수라면 상황이 달라지기 때문이다. 아래처럼 다시 코드를 다시 작성하면 이번에는 this가 상위 객체인 Rectangle을 가리키게 된다. 사실 2번과 같은 맥락인데 한번 더 복습해보자. 화살표 함수는 내부에 this값을 갖지 않는다. 보통의 함수는 실행 시 this값을 특정값으로 지정하는 과정(바인딩)을 거치지만, 화살표함수는 특이하게 이 과정을 생략한다! 대신 상위 스코프의 this값을 가져온다!

 

printAsync() {
	setTimeout(() => {
		this.printArea()
	}, 2000);
}

 

 

 

화살표 함수를 쓰면 안될 때


1. 객체의 메서드를 정의할 때

객체의 메서드를 정의할 때 화살표 함수를 사용하면, 바인딩 과정을 생략하기 때문에 this가 가리킬 곳이 사라지는 일이 발생한다. 이렇게 가리킬 곳이 없는 this는 this의 기본값인 전역 객체를 가리키기 때문에 여기서도 this값은 전역객체 되어버린다. 따라서 그냥 ES6 문법 중 '향상된 객체 리터럴' 방식으로 축약해서 쓰는 것을 추천한다.

 

const obj = {
    name: "주혜",
    sayHi: () => console.log(`${this.name} 하이`)
};
obj.sayHi(); // undefined 하이

 

 

의도치 않은 결과 (X)

 

const obj = {
    name: "주혜",
    sayHi(){ 
        console.log(`${this.name} 하이`);
    }
};
obj.sayHi(); // 주혜 하이

 

의도한 결과 (O)

 

2. prototype으로 메서드 지정 시

prototype을 통해 메서드를 할당할 때 화살표 함수를 사용하면, 메서드 아닌 '일반 함수'가 할당 된 것처럼 작동한다. 1번과 동일한 원리로 호출한 객체가 아닌 전역객체에 바인딩이 되어버리기 때문이다.

 

const obj = {
  name: '주혜',
};

Object.prototype.sayHi = () => console.log(`${this.name} 하이`);
obj.sayHi(); 
// undefined 하이

의도치 않은 결과 (X)

 

따라서 이처럼 function 키워드를 사용해 메서드를 할당해주어야 한다.

 

const obj = {
  name: '주혜',
};

Object.prototype.sayHi = function() {
  console.log(`${this.name} 하이`);
};
obj.sayHi(); 
// 주혜 하이

 

의도한 결과 (O)

 

 

3. addEventListener

addEventListener는 콜백함수를 this로 지정한다. 콜백함수가 바인딩을 생략하는 화살표 함수로 지정되면 무슨일이 일어날까? 콜백함수를 믿고있던(?) addEventListener까지도 바인딩을 생략하게 되어버린다. 따라서 이 경우도 가리킬 곳이 없어진 this는 전역객체를 가리키게 된다.addEventListener를 예시로 들었지만, 이처럼 콜백 함수를 this로 지정하는 모든 고차 함수가 마찬가지이니 주의해서 사용해야 한다.

 

// 화살표 함수
btn.addEventListener('click', () => {
  console.log(this === window); // true
  console.log(this === button); // false
});

 

// 일반 함수
btn.addEventListener('click', function() {
  console.log(this === window); // false
  console.log(this === button); // true
});

 

이벤트 속성을 선언할 때도 마찬가지이기 때문에 function키워드를 사용해야 한다.

btn.onclick = () => { console.log(this); }  // Window {}
btn.onclick = function() { console.log(this); } // btn {}

 

4. arguments

화살표 함수는 arguments 속성을 생성하지 않기 때문에, 화살표 함수 내부에서 arguments를 사용하면 참조 오류가 발생하거나 상위 스코프의 arguments를 불러오게 된다. (this와 비슷하게 행동한다) 대신 Spread Operator(...)을 매개변수에 사용해 arguments 배열을 참조하도록 할 수 있다. 예시로 확인해보자.

 

 

 

 


이처럼 화살표 함수는 굉장히 유용하지만 조심해서 다루어야 하는 친구이다.. 의도치 않은 결과를 얻고 당황하지 않기 위해서는 상황에 맞게 사용하도록 주의하자!

반응형

댓글