자바스크립트(javascript)에서 "function" 키워드의 대안을 선택하자

Raymond Yoo·2022년 11월 5일
0

위의 영상은 저의 영상이 아닙니다.
유튜브에서 우연히 봤는데 너무나 감명깊게 봐서 포스팅을 통해
온전히 제 것으로 만들려는 노력의 일환으로
기록으로 남겨두려고 합니다.

ES6 이전까지 function 키워드로 선언한 함수를 사용하는 세 가지 방법

다음과 같은 함수가 있다고 생각해보자.

function Foo(...args) {
  console.log('this:', this)
  if (this !== window) {
    this.args = args
    return
  }
  return args;
}

이 함수를 사용하는 방법은 크게 세 가지 방법이 있다.

  • 일반적인 함수로 사용하기
  • 생성자 함수로 사용하기
  • 객체 메서드로 할당하기

1 일반적인 함수로 사용하기

const fooResult = Foo(1, 2, 3)
console.log(fooResult);

실행 결과는 이렇다.

Foo 함수 2 라인에서 this 가 window 객체를 가리키고 있는 것을 확인할 수 있다.

2 생성자 함수로 사용하기

const fooInstance = new Foo(3, 4)
console.log(fooInstance)

실행 결과는 이렇다.

new 키워드를 사용해서 Foo 함수를 생성자로서 사용하면
2 라인에서 this 가 Foo 타입의 객체를 가리키고 있다는 것을 확인할 수 있다.

3 객체 메서드로 할당하기

const bar = {
  method: Foo,
}
const barMethodResult = bar.method(5, 6)
console.log(bar)
console.log(barMethodResult)

실행 결과는 이렇다.

이렇게 사용하면 this 가 해당 메서드를 감싸고 있는 bar 객체가 된다.

문제점: 너무 범용적이다

function a() {}
console.dir(a)

위의 코드를 실행하면 다음과 같은 결과가 나타난다.

  • 일반적인 함수로 사용하는 경우: 1) prototype 프로퍼티는 불필요하다. 2) this 바인딩은 이 경우에는 불필요한 정보이며 혼란을 가중시킨다.
  • 생성자 함수로 사용하는 경우
  • 객체 메서드로 할당: 감싸고 있는 객체로 this 바인딩이 자동적으로 일어난다.

ES6 문법을 추가하면서
위의 세 가지 경우를 구분해서 사용할 수 있는 방법을 제공하기 시작했다.

ES6 이후 function 키워드에 대안

1 일반적인 함수로 사용하기

function foo2(...args) {
  console.log(args)
}
const bar2 = (...args) => {
  console.log(args)
}

console.dir(foo2)
console.dir(bar2)

실행 결과는 이렇다.

arrow function 으로 선언하는 경우 해당 함수에 prototype 이 아예 없다는 것을 확인할 수 있다.
prototype 이 없다는 것은 해당 함수를 생성자 함수로 사용하지 않고 호출하는 방식으로만 사용해야 한다는 것을 의미한다.

new bar2()

그래서 위와 같이 arrow function 으로 선언한 함수를 생성자 방식으로 사용하려고 시도하면
곧바로 오류 메시지를 확인하게 된다.

Uncaught TypeError: bar2 is not a constructor

프로젝트 규모가 커지고 서비스 사용자가 늘어날 수록 최적화 영역의 요구사항도 늘어날 것이고
작은 부분이지만 prototype 을 제외시키는 arrow function 의 가벼움이 이득을 가져오는 경우가 많을 것이다.

function 과 arrow function 차이점

function 키워드를 사용해서 선언한 함수를 call, apply, bind 등을 사용해서 this 를 특정한 객체에 매핑하는 것이 가능했지만
arrow function 으로 선언한 함수는 call, appy, bind 등을 사용하더라도 언제나 this 가 window, globalThis 를 가리킨다.

함수 내부에서 this 가 특정 객체를 가리키고 있어야 하는 경우에는
function 키워드를 사용해서 선언한 함수를 만들 수도 있지만 아래 3번에서 제안할 객체 메서드롤 선언해두는 것이 바람직하다.
function 키워드를 사용해서 선언한 전역 함수는 초기에 caller, arguments 등의 실행 컨텍스트에 null 값이 들어가 있으므로
런타임에 오류를 발생시킬 가능성이 높기 때문이다.

2 생성자 함수로 사용하기

console.log('생성자 함수 대신에 ES6 클래스로 사용')
function Foo1(...args) {
  if (this !== window) {
    this.args = args
    return
  }
  return args
}
Foo1.prototype.getArgs = function () {
  return this.args
}
const foo1 = new Foo1(1, 2)
console.dir(foo1)

class Bar1 {
  constructor(...args) {
    if (this !== window) {
      this.args = args
      return
    }
    return args
  }

  getArgs() {
    return this.args
  }
}
const bar1 = new Bar1(1, 2)
console.dir(bar1)

위의 코드 실행 결과를 확인해보면 이렇게 된다.

실행 결과를 자세히 보면
function 키워드 함수를 생성자로 사용해서 객체를 생성하면 프로토타입의 getArgs 프로퍼티가 진한색으로 표시되어 있지만
class 키워드를 통해 객체를 생성하면 프로토타입의 getArgs 프로퍼티가 흐린색으로 표시되어 있는 것을 확인할 수 있습니다.

이것의 의미를 명시적으로 확인하기 위해서 다음과 같은 코드를 실행해보자.

const foo1 = new Foo1(1, 2)
console.dir(foo1)
for (const prop in foo1) {
  // if (foo1.hasOwnProperty(prop)) {
  //   console.log('    prop of foo1', prop)
  // }
  console.log('    prop of foo1', prop)
}

const bar1 = new Bar1(1, 2)
console.dir(bar1)
for (const prop in bar1) {
  console.log('    prop of bar1', prop)
}

프로토타입의 개별 프로퍼티는 기본적으로 { enumerable: true } 라는 속성이 기본값으로 세팅이 되는데
위에서 프로퍼티가 진한색으로 표시되는 것은 enumerable === true 인 것이고
흐린색으로 표시되는 것은 enumerable === false 인 것이다.

해당 객체에 포함되어 있는 모든 프로퍼티를 하나씩 순회화는 경우에
function 함수를 생성자로 사용해서 객체를 생성한 경우에는 prototype 에 있는 프로퍼티까지 객체 자체의 프로퍼티인 것처럼 처리된다.
이를 막으려면 위에서 주석처리해둔 것처럼 hasOwnProperty 메서드를 사용해서 분기처리를 해야 한다.
반면에 class 키워드로 선언한 클래스 타입으로 객체를 생성한 경우에는 prototype 에 있는 프로퍼티는 드러나지 않고
해당 객체에 포함된 프로퍼티만 다루게 된다.

ES6 에서 도입한 class 키워드를 사용하면
개발자가 신경써야 하는 부분이 하나 줄어들게 된다.

추가적인 이점 1

console.dir(Foo1)
console.dir(Bar1)

선언한 함수 자체에 대한 정보를 출력해보면
function 키워드를 사용해서 선언한 Foo1 함수에는 caller, arguments 와 같은 실행 컨텍스트 정보에 null 이 담겨있다.
그러나 class 키워드를 사용해서 선언한 Bar1 클래스에서 caller, arguments 와 같은 실행 컨텍스트 정보를 확인하려고 하면
오류가 발생한다.

Bar1.arguments

위의 코드를 실행했을 때 발생하는 오류 코드는 다음과 같다.

Uncaught TypeError: 'caller', 'callee', and 'arguments' properties 
may not be accessed on strict mode functions or the arguments objects for calls to them

추가적인 이점 2

Bar1()

생성자로서 사용하려고 function 키워드를 사용해서 어떤 함수를 만들었지만
실수로 new 없이 해당 함수를 생성하려고 시도하는 경우에 코드는 아무런 오류도 발생하지 않은 채 계속 진행된다.

그러나 바로 위에 있는 코드처럼 class 키워드를 사용해서 만든 클래스를 실수로 함수처럼 호출한다면
아래의 오류 메시지를 만나게 된다.

index.js:72 Uncaught TypeError: Class constructor Bar1 cannot be invoked without 'new'

즉 어떤 객체를 생성하는 로직으로 사용하려는 경우에
ES6 class 키워드를 사용하면 function 키워드를 사용할 때보다
훨씬 더 빨리 코드 상에 존재하는 오류를 발견할 수 있게 된다.

3 객체 메서드로 할당하기

const obj1 = {
  name: '이르 1',
  method: function() {
    console.log(this.name)
  }
}

const obj2 = {
  name: '이르 1',
  method() {
    console.log(this.name)
  }
}

console.dir(obj1.method)
console.dir(obj2.method)

실행 결과

위의 코드에서 obj2.method 처럼 ":"(콜론, colon), function 키워드 없이 객체 메서드를 정의하는 경우에
축약형으로 메서드를 표현하는 방법이 ES6 에서 추가되었다.
메서드 축약형을 사용하는 경우에 arrow function 일 때와 마찬가지로 prototype 은 사라지지만
arrow function 과는 다르게 this 바인딩이 해당 객체로 잘 이어지기 때문에 개발자가 의도하는 로직대로 구현하는 과정에서 이슈가 전혀 발생하지 않는다.

추가적인 이점

new obj2.method()

실행해보면

Uncaught TypeError: obj2.method is not a constructor

라는 오류메시지가 밣생한다.
그러므로 보다 빠르게 로직상의 오류를 발견할 수 있게 된다.

function 키워드를 사용하지 않을 수 없는 경우

generator 를 정의하는 경우

function* generator() {
  const arr = [1, 2, 3, 4, 5]
  for (const value of arr) {
    yield value
  }
}

const generatorInstance = generator();
console.log(generatorInstance.next().value)
console.log(generatorInstance.next().value)
console.log(generatorInstance.next().value)

함수 형태의 generator 를 정의할 때는
function* name() {}
형태로 표현해야 하기 때문에 function 키워드를 사용할 수 밖에 없다.

const generatorObject = {
  arr: [1, 2, 3, 4, 5],
  *generator2() {
    while (0 < this.arr.length) {
      yield this.arr.shift()
    }
  },
}

const generator2Instance = generatorObject.generator2()
console.log(generator2Instance.next().value)
console.log(generator2Instance.next().value)
console.log(generator2Instance.next().value)

대신에 객체 메서드로서 generator 를 정의한다면
메서드 축약형으로 쓰되 메서드 이름 앞에 * 을 붙여주기만 하면
generator 를 구현할 수 있다.

자바스크립트에서 "function" 키워드를 쓰지 않아야 하는 이유

각자의 취향이라는건 당연히 존중받아야 하지만
여러명의 개발자가 하나의 코드베이스를 공유하며 개발 및 유지보수하는 협업 환경이라면
조금이라도 가벼운 기능을 선택하고
작성한 코드의 목적을 명확하게 드러내는 스타일을 선택하는 것이 바람직하다.

예를 들어서 function a() {} 라고 정의한 함수를 봤을 때
이를 일반적인 함수로 사용하려고 하는지, 생성자 함수로 사용하려고 하는지, 아니면 다른 객체에 메서드로 할당하려고 하는지, call apply bind 를 활용해서 this 를 동적으로 바인딩해서 사용하려는 함수인지 명확하게 드러나지 않을 수 있다.
그러나 arrow function 으로 선언되어 있으면 this 바인딩이 불가능하므로 일반적인 함수로 사용하려는 것을, class 키워드로 선언되어 있으면 생성자로서 객체를 만들기 위해서 정의한 클래스라는 것을 보다 빠르게 파악할 수 있게 된다.

그러므로 자바스크립트 프로젝트에서 function 키워드는 최대한 지양하는 것이 좋다.

profile
세상에 도움이 되고, 동료에게 도움이 되고, 나에게 도움이 되는 코드를 만들고 싶습니다.

1개의 댓글

comment-user-thumbnail
2023년 9월 25일

감사합니다. 요약이 명쾌하여 빠르게 도움받고 갑니다.

답글 달기