치춘짱베리굿나이스

자바스크립트 일반함수 vs 화살표함수 본문

Javascript + Typescript/이론과 문법

자바스크립트 일반함수 vs 화살표함수

치춘 2022. 8. 27. 01:48

일반 함수 vs 화살표 함수 (람다식)

참고로 필자는 자바스크립트 입문을 let, const, 화살표함수로 했기에 그것만 주구장창 썼는데 이걸 왜 쓰는지는 명확히 이해하지 못했다 (그냥 일반함수랑 똑같이 작동한다고 생각했음)

언젠가 두 개의 차이를 정리해봐야겠다 막연한 생각은 했었는데 그게 지금이 될 줄은

일반 함수

function foo() {
  console.log("foo");
}

평범하게 작성한 함수이다

 

const bar = function foo() {
  console.log("foo");
}

변수에 할당하고 싶다면 이렇게 작성한다

화살표 함수

const foo = () => {
  console.log("foo");
}

화살표를 이용해서 조금 더 간결하게 작성되는 함수이다

ES6에서 추가되었으며, 람다 함수라고도 한다

위의 경우는 foo라는 변수에 함수를 담았을 뿐이지 (엄밀하게는 함수의 형태를 띈 객체를 담은 것) 화살표 함수 자체에는 이름이 없다

 

() => { console.log("foo"); }

함수의 본체는 const foo = 부터가 아니라 인자를 넣는 괄호부터이기 때문이다

이름이 없기 때문에 일반 함수와 달리 따로 (위의 foo 예시처럼) 변수에 담지 않는 이상 재호출은 어렵다

대신 일반 함수에 비해 상당히 간결하게 작성할 수 있기 때문에, 짧고 재사용 가능성이 없는 콜백 함수를 작성할 때 많이 사용된다

 

(v) => v + 5

간단한 값을 연산하여 반환만 하고 싶을 경우엔 중괄호조차도 필요가 없다

중괄호를 사용하지 않을 경우, 화살표 오른쪽에 있는 값을 그대로 반환한다

 

(v) => ({ name: v })

객체를 반환하고 싶다면 객체 중괄호를 소괄호로 감싼다

그렇지 않으면 중괄호를 함수 스코프의 시작 부분으로 이해하기 때문이다

 

[1, 2, 3, 4].map((v) => v + 5);
useEffect(() => { console.log("foo"); }, []);

콜백 함수를 인자로 받는 함수론 대표적으로 map, forEach 등의 배열 메서드나 리액트에서의 useEffect 등이 있다

위의 메서드들 특성상 콜백 함수들이 해당 메서드에서만 사용되고 외부에서 재사용이 일어나지 않기 때문에 이러한 경우 화살표 함수를 쓰기 참 좋다

물론 콜백 함수 내부가 변수 선언이나 로직 때문에 과도하게 길어질 경우 가급적 함수를 분리하는 것이 가독성 면에서 훨씬 좋으므로 여러가지 조건을 고려하여 사용하자

차이점?

this 바인딩

class TestClass {
  constructor() {
    this.value = "hello";
  }

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

const testInstance = new TestClass();
testInstance.func();

this는 보통 함수 (메서드) 자신을 소유하고 있는 객체, 또는 인스턴스를 가리킨다

위의 예시에서 testInstance 인스턴스는 testInstance.func 메서드를 소유하고 있으므로, testInstance.func() 메서드를 호출하면 func 내부의 thistestInstance를 가리키게 되고, 따라서 console.logTestClass 클래스의 인스턴스인 testInstance를 출력한다

 

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

const bar = () => {
  console.log(this);
};

일반함수와 화살표함수를 가지고 간단한 예제를 만들어 보았다

일반함수 (foo) 와 화살표 함수 (bar) 는 둘 다 this를 출력하는 기능을 한다

허나 두 함수 모두 전역 스코프에 정의되어 있는데, 위의 두 함수에서 this는 언제 결정되는 걸까?

 

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

class TestClass {
  constructor() {
    this.value = "hello";
    this.func = foo;
  }
}

function test() {
  const testInstance = new TestClass();
  const obj1 = {
    value: "foo",
    func: foo,
  };

  testInstance.func();
  obj1.func();
}

foo();
test();

우선 일반 함수의 this 바인딩 테스트를 위해 전역에 foo를 선언하고,

  • 전역 스코프에서 foo가 호출되는 경우 (foo())
  • 클래스 TestClass의 생성자에서 인스턴스의 func 메서드를 지정해주고, 이를 호출하는 경우 (testInstance.func())
  • 객체 obj1func 메서드로 추가하고, 이를 호출하는 경우 (obj1.func())

세 가지 경우에서 각각 this가 어떻게 동작하는지 알아보자

첫 번째 경우 (전역 스코프에서 호출) console.log(this) 는 말그대로 전역 스코프 전체… 를 출력했다

놀랍게도 전역 스코프가 객체의 형태로 존재한다

자바스크립트에서 모든 것은 객체인가요? 에 대해서 알아봤었는데 전역 스코프 마저 객체의 형태로 다뤄지고 있는 것이었다

브라우저에서 코드를 실행할 경우 전역 객체는 Window를 가리키고, 위처럼 node.js 환경에서 실행할 경우 global이라는 객체가 기본값으로 생성된다고 한다

자바스크립트는 객체를 정말로 사랑하는구나

 

두 번째와 세 번째 경우, 각각

  • TestClass 클래스로부터 생성된 인스턴스 testInstance
  • obj1을 가리킨다

예상했던 대로 함수 자신을 가지고 있는 상위 스코프의 멤버를 출력하는 것을 볼 수 있다

 

const bar = () => {
  console.log(this);
};

class TestClass {
  constructor() {
    this.value = "hello";
    this.func = bar;
  }
}

function test() {
  const testInstance = new TestClass();
  const obj2 = {
    value: "bar",
    func: bar,
  };

  testInstance.func();
  obj2.func();
}

bar();
test();

이번에는 화살표 함수의 this 바인딩 테스트를 위해 전역에 bar를 선언하고,

  • 전역 스코프에서 bar가 호출되는 경우 (bar())
  • 클래스 TestClass의 생성자에서 인스턴스의 func 메서드를 지정해주고, 이를 호출하는 경우 (testInstance.func())
  • 객체 obj2func 메서드로 추가하고, 이를 호출하는 경우 (obj2.func())

마찬가지로 세 가지 경우를 모두 살펴보자

 

세 경우 모두 빈 객체가… 출력된다 (당황)

일반 함수와 다르게 화살표 함수는 선언되는 순간 this가 결정되기 때문이다

화살표 함수 자체는 this가 없어서, 상위 객체에서 this를 찾게 된다고 한다

따라서 화살표 함수가 가리키는 this는 화살표 함수가 존재하는 스코프의 상위 스코프를 가리킨다

bar가 선언된 스코프 (전역 스코프) 의 상위 스코프에 해당하는 객체가 빈 객체이므로, 비어있는 객체가 출력되는 것이다

사실 이 객체는 빈 객체가 아니라 Module 객체라 해서 Module.exports 된 값들이 들어있는 객체인데, 전역 스코프에서 this를 찍어보면 Module 객체를 가리키게 된다

 

위의 예제를 브라우저에서 돌릴 경우, 최상위 객체인 Window 객체가 출력되게 된다

 

function test() {
  const testFunc = () => {
    return () => console.log(this);
  };
  testFunc()();
}

test();

이번에는 이런 함수를 작성해 보자

일부러 스코프 단위를 테스트하기 위해 복잡하게 함수 안에 함수 안에 함수… 방식으로 작성하고, 커링으로 호출하였다

이 함수의 출력값은 무엇일까?

 

또다시 전역 객체가 출력되었다

그 이유는 testFunc가 반환하는 화살표 함수의 스코프 (test) 의 상위 스코프가 전역 스코프이기 때문이다

이처럼 화살표 함수는 this가 자기자신을 소유한 객체를 가리키는 것이 아니라, 상위 스코프를 가리키기 때문에 this를 사용해야 하는 경우 화살표 함수로 선언하는 것이 바람직하지 않다

굉장히 까다로운 녀석이다

this 바꾸기

const obj1 = {
  value: "hello",
  func: function () {
    console.log(this);
  },
};

const obj2 = {
  value: "byebye",
};

obj1.func.call(obj2); // this = obj2
obj1.func.apply(obj2); // this = obj2
obj1.func(); // this = obj1

const func2 = obj1.func.bind(obj2);
func2(); // this = obj2

일반 함수는 call, apply, bind 메서드를 통해 this가 가리키는 객체를 바꿔 호출할 수 있다

  • callapplythis를 바꿈과 동시에 함수를 호출하며, this의 변경은 일시적이므로 다음 호출때 원래 가리키던 객체로 돌아온다
    • callapply의 차이는 함수의 인자를 가변인자로 받는지 배열로 받는지 차이밖에 없는데, 위 예시에선 인자가 따로 없으므로 결국 같은 동작을 한다
  • bindthis가 영구적으로 바뀐 함수를 반환하며, 해당 함수를 호출하면 this가 가리키는 객체가 바뀐 채로 유지된다

아무튼… 예시를 보면 obj1.functhis가 가리키는 객체가 바뀌는 것을 볼 수 있다

 

const obj1 = {
  value: "hello",
  func: () => console.log(this);
};

const obj2 = {
  value: "byebye",
};

obj1.func.call(obj2);
obj1.func.apply(obj2);
obj1.func();

const func2 = obj1.func.bind(obj2);
func2();

화살표 함수는 세 메서드가 전부 먹통이 된다 = 어떠한 방법으로든 this가 가리키는 객체 (상위 스코프) 를 바꿀 수 없다

화살표 함수의 this는 선언 시에 결정되며, 이를 바꿀 수는 없다

 

func가 선언된 스코프 (전역) 의 상위 스코프니까 빈 객체가 또 출력되고 말았다

화살표 함수의 this는 자기자신을 소유하고 있는 obj1조차도 가리키지 않으므로, 만약 객체 내부의 메서드가 this를 사용해야 한다면 화살표 함수는 매우 좋지 못한 선택이라고 할 수 있겠다

 

const obj1 = {
  value: "hello",
  func() {
    console.log(this);
  },
};

화살표함수의 간결함과 일반 함수의 this 두마리 토끼를 다 잡고 싶다면, ES6부터 지원하는 축약 메서드 표현을 이용하자

굳이 key 값을 따로 입력하거나 function 키워드를 명시할 필요 없이, 함수 선언하듯 keyvalue를 동시에 정의할 수 있다

생성자로 사용하기

function foo(value) {
  this.value = value;
}

const newFoo = new foo("hi!");
console.log(newFoo.value);

이 포스팅에서 함수 객체는 프로퍼티와 메서드를 상속할 수 있는 프로토타입 프로퍼티를 가지고 있다는 사실을 알아보았다

이 프로토타입 프로퍼티를 통해 함수 객체를 생성자로 사용하여 새로운 객체를 만들 수 있었고, 실제로 위의 예시에서 foo 함수를 통해 newFoo 객체를 만들었다

따라서 newFoo.value는 생성자에 넘겨준 문자열인 hi를 가지고 있게 된다

 

const bar = (value) => {
  this.value = value;
};

const newBar = new bar("bye!");
console.log(newBar.value);

화살표 함수는 사정이 다르다

일반 함수와 다르게 프로토타입 프로퍼티가 없기 때문에 생성자로 사용할 수 없다

newBar 객체를 만들려고 해도 bar가 생성자가 아니라는 에러만 뜬다

물론 객체가 생성된다 하더라도 화살표 함수의 this가 가리키는 위치가 이상해지기 때문에 적절한 객체가 반환되지 않았을 것이다

arguments 변수

function foo() {
  console.log(arguments);
}

foo("hello", "byebye", "chichoon");

this와 비슷하게 함수가 기본적으로 가지고 있는 배열 비슷한 객체이며, 함수의 인자를 갖고 있다

foo의 선언부에 따르면 foo는 인자를 받지 않지만, 호출 시에 인자를 넣었다고 해서 오류가 발생하진 않는다

이때 모든 인자들이 arguments에 들어가며, 인덱스를 통해 인자에 접근할 수 있다 (arguments[0] 이런 식)

 

const bar = () => {
  console.log(arguments);
};

bar("hello", "byebye", "chichoon");

반면에 화살표 함수는 arguments가 존재하지 않기 때문에 오류를 반환한다

(중간의 undefined는 선언 부분에서 반환하는 값이 아무도 없기 때문에 출력된다)

 

위의 테스트를 브라우저에서 진행한 이유는… node에서 테스트하면 arguments가 존재할 뿐더러 그 안에는 알수 없는 이상한 값들이 출력되기 때문이었다

오류가 발생하지 않아 적잖이 당황했다

 

console.log(arguments) // 전역 스코프

저 값들이 대체 무엇인고 알아보니 전역 스코프의 arguments 값이었다

온갖 키워드로 (arguments, ancestor, parent, global arguments, function require…) 다 검색을 해봤는데 그나마 현재 상황과 맞는 답안은 “현재 함수 스코프에 arguments가 존재하지 않을 때, 부모 스코프에서 arguments를 찾는다" 였다

 

node와 브라우저가 다르게 동작하는 이유는 node에서의 전역 스코프 (global object) 와 브라우저의 전역 스코프 (window) 가 다르므로,

  • global object에서 arguments는 파일명, 경로, 모듈 정보 등의 정보를 반환하지만
  • window는 에러처리를 한다고 보면 되겠다

node 환경에서 arguments의 0, 1, 2, 3, 4번째 인덱스에 무엇이 담기는지는 이 Node.js 문서를 참고하면 얼추 맞다

 

아무튼 결론은 node에서 화살표 함수의 arguments가 얼추 동작한다고 해서 화살표 함수 내에서 arguments를 사용하면 안된다는 것이다

화살표 함수의 인자를 담은 것이 아니라 전역 스코프의 정보가 담겨 있으므로 사실상 arguments가 존재하지 않다고 봐야 맞고, 따라서 함부로 쓰면 에러가 이놈한다

결론

화살표 함수가 쓰기 편해서 (…) 온갖 컴포넌트나 함수에 남용했었는데, 함부로 쓰면 안된다는 생각을 좀 했고 반성했다

지금까지 진행했던 프로젝트에선 함수 내에서 this를 건드릴 일이 좀처럼 없어 화살표 함수와 일반 함수의 차이점도 아예 모르고 있었는데, this를 사용하는 경우가 있었더라면 큰일날 뻔했다

지금이라도 알게 되었으니 간단한 콜백 함수 작성 외에는 가급적 일반 함수를 사용하도록 습관을 들여야겠다


참고자료

자바스크립트 람다식(화살표 함수) 사용 방법 주의점

JavaScript - 화살표 함수와 일반 함수의 차이

화살표 함수와 일반 함수의 차이

Arrow function | PoiemaWeb

ZeroCho Blog

'Javascript + Typescript > 이론과 문법' 카테고리의 다른 글

모듈과 모듈 번들러  (0) 2022.09.12
콜 스택 (호출 스택)  (0) 2022.08.27
자바스크립트에서의 Symbol  (0) 2022.08.20
자바스크립트에서의 싱글톤 패턴과 static  (0) 2022.08.08
Set, Map  (0) 2022.07.30
Comments