JavaScript: 어떤 방식으로 함수 정의하는 것을 선호하세요? - (2)

함수 표현식(기명함수, 익명함수), 화살표 함수(this)에 대해

·

6 min read

함수 표현식 - Function Expression

hello();

var hello = function greeting() {
    console.log('Hello world!');
}

변수에 함수를 할당해서 정의하는 방식을 함수 표현식이라고 한다. 그럼 함수 표현식도 함수 정의니깐 함수 호이스팅이 될까? 정답은 '안 된다'이다. 변수에 함수를 할당하는 것이기 때문에 변수 호이스팅 방식을 따라간다. 그래서 위의 코드를 실행하면 undefined가 출력된다.

hello();

// A 함수
const hello = function greeting() {
    console.log('Hello world!');
}

그럼 let 이나 const 키워드를 이용해 함수표현식을 작성하면 어떻게 될까? 우리는 변수 호이스팅에서 TDZ가 무엇인지 알 수 있었다. 그럼 위의 코드를 실행했을때 어떤 걸 볼 수 있을지 짐작 할 수 있다. 바로 ReferenceError: Cannot access 'hello' before initialization 에러가 날 것이다.

기명함수 vs 익명함수

함수를 정의 할 때 함수의 이름을 지정해줄 수도 있고 지정하지 않을 수도 있다. 위의 코드에서는 함수의 이름을 greeting 으로 정해줬고 아래 코드에서는 함수의 이름을 정해주지 않았다. A 함수처럼 이름이 있는 함수를 기명함수라고 하고 아래 B 함수처럼 이름이 없는 함수를 익명함수라고 한다.

// B 함수
const hello = function () {
    console.log('Hello world!');
}

그러면 둘의 차이점은 무엇일까? 함수표현식에서 익명함수로 함수를 정의하면 JS 엔진에서 함수의 이름을 추론한다. hello.name 으로 함수의 이름을 물어보면 A 함수는 greeting 이 나오고 B 함수는 hello 가 나온다. JS 엔진에서 B 함수의 이름을 hello 라고 추론한 것이다.

화살표 함수 - Arrow Function

ES6에서는 화살표 함수가 공개됐고 함수 표현식에서 변수에 할당하는 함수를 화살표 함수로 정의할 수도 있다.

const hello = () => {
    console.log('Hello world!');
}

그냥 한 눈에 봤을 때도 깔끔해 보인다. 그러니 '무조건 화살표 함수를 사용하는게 좋다!'라고 생각할 수도 있다. 하지만 화살표 함수는 무조건 익명함수로 작성해야 하기 때문에 따라오는 어려움이 있다.

let arr = [1, 2, 3, 4, 5];

let arr2 = arr.map(x => y+2);

y 변수가 어디에도 정의 되지 않았기에 위의 식을 실행하면 아래와 같은 에러 메시지가 나타날 것이다.

너무 당연한 거 아닌가? 라고 생각할 수도 있지만 에러 메시지를 자세히 보자. <annoymous> 메시지가 표시된다. 지금은 코드가 짧아 어디서 에러가 났는지 알 수 있지만 만일 코드가 길어진다면 어디서 에러가 나는지 찾기 어려워 디버깅하기 쉽지 않을 것이다.

let arr = [1, 2, 3, 4, 5];

let arr2 = arr.map(function addTwo(x) {
    return y+2;
});

함수에 이름을 지정해준다면 디버깅 할 때 유용한 정보를 얻을 수 있다.

여기서 <annoymous> 메시지가 계속 나타나는 이유는 Array.map()을 구현한 코드가 JS 엔진에 내장 되어 있기 때문이다.

그럼 화살표 함수를 사용하는 이유는 무엇일까? this 바인딩에 차이가 있기 때문이다. JS에서 this가 의미하는 것이 무엇인지부터 알아보자.

this

JS에서 this 키워드는 다른 언어와는 다르게 동작한다. JS에서 this는 함수의 호출방식에 따라 동적으로 결정된다.

  1. 함수 호출

함수 호출 방식으로 함수를 호출하면 this는 전역객체가 된다.

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

example();

위의 코드를 개발자 도구 콘솔 창에서 실행하면 window 가 출력된다. 함수 호출 할 때의 this 는 전역 객체에 바인딩이 되고 웹 브라우저에서 전역 객체는 window 이기 때문이다.

Node 환경에서 코드를 실행하면 Node 환경의 전역객체 global 이 출력된다.

  1. 생성자 함수 호출 (new 키워드를 이용한 함수 호출)

function Example() {
    console.log(this);
    this.value = 10;
    console.log(this);
}

new Example();
// -> {}
// -> { value: 10 }

new 키워드로 함수를 호출하니 새로운 객체를 생성해서 반환했다. 어떻게 된 것일까?

function Example() {
    // this = {};
    // this.__proto__ = Example.prototype;
    console.log(this);
    this.value = 10;
    console.log(this);
    // return this;
}
new Example();

new 키워드와 함께 생성자 함수를 호출하면 우리가 볼 수 없는 암시적 코드를 추가한다. 아래와 같은 방식으로 동작한다.

  1. 빈 객체 생성 및 this 바인딩

  2. 생성된 빈 객체는 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다.

  3. 생성된 객체 반환

    • 함수에서 객체, 배열 또는 함수가 아닌 다른 유형의 변수가 반환되면 함수가 this를 반환하도록 설정

    • 만일 객체, 배열, 함수를 반환할 경우 this가 아닌 해당 유형의 변수가 반환된다. 이때 this를 반환하지 않은 변수는 생성자 함수로서의 역할을 수행하지 못하기 때문에 생성자 함수는 만들 때는 반환문을 사용하지 않음

  1. apply/call/bind 를 이용한 함수 호출

this에 특정 객체를 바인딩 해서 호출하는 방법이다. 아래 코드에서는 obj를 this에 바인딩하라고 알려줬기 때문에 obj를 example의 this는 obj가 된다.

function example() {
    console.log(this);
}
let obj = {
    value: 5
};

example.call(obj);
example.apply(obj);
example.bind(obj)();
// { value: 5 }
// { value: 5 }
// { value: 5 }
  1. 메서드 호출

함수가 메서드로 호출 될 경우(즉, 점 표기법을 사용하여 함수를 호출하는 경우) this는 해당 메서드를 소유한 객체가 된다. 쉽게 얘기하자면 점 왼쪽에 있는 객체가 this에 바인딩되는 것이다.

let obj = {
    value: 5,
    printThis: function() {
        console.log(this);
    }
};

obj.printThis();
// { value: 5, printThis: ƒ }

이렇게 네가지 방법으로 함수를 호출 할 경우 this가 어디에 바인딩 되는지 알아봤다. 그럼 '화살표 함수가 대체 this랑 무슨 상관이야?'라고 생각할 수 있다. this 바인딩이 예상과는 다르게 되는 경우가 있다. 아래의 경우를 보자.

let obj = {
    value: 5,
    foo: function() {
        console.log('foo의 this :', this);
        function bar() {
            console.log('bar의 this :', this);
        }
        bar();
    }
};

obj.foo();
// foo의 this : { value: 5, printThis: ƒ }
// bar의 this : window

갑자기 왜 bar의 this가 window가 나온 것일까? 아래의 두 가지 경우도 보자.

function foo() {
  console.log("foo's this: ",  this);
  function bar() {
    console.log("bar's this: ", this);
  }
  bar();
}

foo();
// foo의 this : window
// bar의 this : window
const value = 1;

let obj = {
  value: 100,
  foo: function() {
    setTimeout(function() {
      console.log("callback's this: ",  this);
      console.log("callback's this.value: ",  this.value);
    }, 100);
  }
};

obj.foo();
// callback's this: window
// callback's this: 1

세 가지 모두에서 알 수 있듯 내부함수의 this는 일반 함수, 메서드, 콜백 함수 어디에서 선언되었든 전역객체에 바인딩된다. 보통은 상위 스코프의 this가 내부 함수의 this가 될 것이라고 예상할 것이다. 하지만 예상과는 다르게 동작한다. 당황스럽다. 이런 경우를 회피할 수 있도록 도와주는 것이 바로 화살표 함수이다.

화살표 함수에는 this 가 존재하지 않는다. 따라서 스코프 체인처럼 선언 시점에서의 상위 스코프의 this를 참조한다. 바로 위에 있는 콜백함수를 화살표 함수로 바꿔보자.

const value = 1;

let obj = {
  value: 100,
  foo: function() {
    setTimeout(() => {
      console.log("callback's this: ",  this);
      console.log("callback's this.value: ",  this.value);
    }, 100);
  }
};

obj.foo();
// callback's this: {value: 100, foo: ƒ}
// callback's this: 100

우리가 예상했던 대로 동작한다. 이런 경우에는 화살표 함수를 매우 유용하게 사용할 수 있다.

그러면 첫번째 두번째 경우는 화살표 함수로 나타낼 수 없는데 어떻게 해야할까? this를 다른 특정 변수에 복사해서 사용하는 방법을 사용하면 된다.

let obj = {
    value: 5,
    foo: function() {
        console.log('foo의 this :', this);
        let that = this;
        function bar() {
            console.log('bar의 that :', that);
        }
        bar();
    }
};

obj.foo();
// foo의 this : { value: 5, printThis: ƒ }
// bar의 that : { value: 5, printThis: ƒ }

마치며

"어떤 방식으로 함수 정의하는 것을 선호하세요?"라고 질문의 의도는 무엇일까? 이 질문은 면접자의 함수 정의 방식에 대한 개인의 선호를 물어보는 것이 아니라 각 함수 정의 방식의 특징을 아는지에 대해 물어보는 것일거라 예상한다. 결국 각 함수 정의 방식의 차이점에 대해 아는지가 중요할 것이다.

참고

https://codeburst.io/the-simple-rules-to-this-in-javascript-35d97f31bde3 https://codeburst.io/javascripts-new-keyword-explained-as-simply-as-possible-fec0d87b2741 https://poiemaweb.com/js-this
https://poiemaweb.com/js-this