본문 바로가기
Modern React Deep Dive/01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트

1.2.2 함수를 정의하는 4가지 방법

by Whiimsy 2024. 4. 2.

1.2.2 함수를 정의하는 4가지 방법

함수 선언문

  • 자바스크립트에서 함수 선언할 때 가장 일반적으로 사용하는 방식
  • function add(a, b) {
      return a + b
    }
  • 함수 선언문은 표현식이 아닌 일반 문(statement)으로 분류됨(\표현식: 무언가 값을 산출하는 구문. 즉, 함수 선언으로는 어떠한 값도 표현되지 않았으므로 표현식이 아닌 일반 문)
  • const sum = function sum(a, b) {
      return a + b
    }
    
    sum(10, 24) // 34
  • 함수 선언문은 말 그대로 '선언'이고 어떠한 값도 표현하지 않으므로 표현식과는 다르게 변수에 할당할 수 없는 것이 자연스러워 보임 BUT!! 가능함
    • 이유) 자바스크립트 엔진이 코드의 문맥에 따라 동일한 함수를 문이 아닌 표현식으로 해석하는 경우가 있음
    • 따라서) 위와 같이 이름을 가진 형태의 함수 리터럴은 코드 문맥에 따라 선언문으로도, 표현식으로도 사용될 수 있음

함수 표현식

  • 자바스크립트에서 함수는 일급 객체임
    • 일급 객체) 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체
    • 함수는 다른 함수의 매개변수가 될 수도 있고, 반환값이 될 수도 있으며, 할당도 가능하므로 일급 객체가 되기 위한 조건을 모두 갖추고 있음
  • const sum = function (a, b) {
      return a + b
    }
    
    sum(10, 24) // 34
  • 함수 표현식에서는 위처럼 할당하려는 함수의 이름을 생략하는 것이 일반적임
    • 이유) 혼란 방지
    • const sum = function add(a, b) {
        // 함수 몸통에서 현재 실행 중인 함수를 참조하는 데 사용할 수 있음
        // 실제 프로덕션 코드에서 절대로 사용해선 안 됨!
        console.log(arguments.callee.name)
        return a + b
      }
      
      sum(10, 24)
      // add
      add(10, 24) // Uncaught TypeError: add is not defined
    • 위 함수 표현식 예제 살펴보면 실제로 함수를 호출하기 위해 사용된 것은 sum임
    • add는 실제 함수 내부에서만 유효한 식별자일 뿐, 함수를 외부에서 호출하는 데에는 사용할 수 없는 식별자임
    • 따라서) 함수 표현식에서 함수에 이름을 주는 것은 함수 호출에 도움이 전혀 안되는, 코드를 읽는 데 방해되는 요소임

함수 표현식과 선언 식의 차이

  • 가장 큰 차이점 : 호이스팅(hoisting) 여부
    • 함수의 호이스팅
      • 함수 선언문이 마치 코드 맨 앞단에 작성된 것처럼 작동하는 자바스크립트의 특징
      • 함수에 대한 선언을 실행 전에 미리 메모리에 등록하는 작업
  • hello() // hello
    
    function hello() {
      console.log('hello')
    }
    
    hello() // hello
    함수를 선언한 hello는 코드 중간에 있음에도 불구하고, 맨 앞에서 호출한 hello()는 어떠한 에러도 없이, 정상적인 hello 함수의 작동을 수행하는 것을 알 수 있음. 함수의 호이스팅 특징 덕분에 함수 선언문이 미리 메모리에 등록됐고, 코드의 순서에 상관없이 정상적으로 함수 호출 가능
  • console.log(typeof hello === 'undefined') // true
    
    hello() // Uncaught TypeError: hello is not a function
    
    var hello = function () {
      console.log('hello')
    }
    
    hello()
    함수 표현식은 함수를 변수에 할당함. 변수도 마찬가지로 호이스팅이 발생하지만 함수의 호이스팅과 다르게, 호이스팅되는 시점에서 var의 경우에는 undefined로 초기화한다는 차이가 있음. 함수와 다르게 변수는, 런타임 이전에 undefined로 초기화되고, 할당문이 실행되는 시점, 즉 런타임 시점에 함수가 할당되어 작동함
  • 그럼 뭐가 더 좋지?
    • 함수 선언문
      • 장점) 함수가 선언된 위치에 상관 없이 함수 호이스팅의 특징을 살리면 어디서든 호출 가능, 변수 선언과 뚜렷하게 구별됨
      • 단점) 함수가 선언되기 전에 호출되는 것이 이상하게 느껴짐. 실제 함수를 어디서 어떻게 선언했는지 해당 스코프를 끝까지 확인하지 않으면 개발자기 찾기 어려움. 관리해야 하는 스코프가 길어질 경우 나쁘게 작용함.
    • 둘 중 어떠한 것이 기능적으로 우위에 있다고 구별지을 만한 점 없음. 현재 자바스크립트 코드를 작성하는 환경을 살펴보고, 상황에 맞는 작성법을 일관되게 사용하면 됨

Function 생성자

  • 자바스크립트 오래 사용했다 하더라도 거의 사용해 본 적 없을 만한 함수 선언 방식
  • const add = new Function('a', 'b', 'return a + b')
    
    add(10, 24) // 34
  • 코드 작성 관점에서만 보더라도 매개변수, 함수의 몸통을 모두 문자열로 작성해야 함. 코드 양 길어지면 더욱 혼란스러워짐.
  • 이렇게 생성자 방식으로 함수를 만들게 되면 함수의 클로저 또한 생성되지 않음
  • 자바스크립트의 eval만큼이나 실제 코딩에서 사용되지 않는 방법

화살표 함수

  • ES6에서 새롭게 추가된 함수 생성 방식. 최근 자바스크립트 개발자들 사이에서 각광받는 함수 정의 방식.
  • function이라는 키워드 대신 =>라는 화살표 활용해 함수를 만드는데, 타이핑할 글자 수가 줄어든다(..!)는 측면에서 많이 사용되는 듯
  • const add = (a, b) => {
      return a + b
    }
    
    const add = (a, b) => a + b
  • 그러나 화살표 함수는 겉보기와 다르게 앞서 언급한 함수 생성 방식과 몇 가지 큰 차이점이 있음
    • 화살표 함수에서는 constructor를 사용할 수 없음. 즉, 생성자 함수로 화살표 함수를 사용하는 것 불가능
    • const Car = (name) { this.name = name } // Uncaught TypeError: Car is not a constructor const myCar = new Car('하이')
    • 화살표 함수에서는 arguments가 존재하지 않음
    • function hello() { console.log(arguments) } // Arguments(3) [1, 2, 3, callee: f, Symbol.iterator): f] hello(1, 2, 3) const hi = () => { console.log(arguments) } // Uncaught TypeError: arguments is not defined hi(1, 2, 3)
    • 화살표 함수와 일반 함수의 가장 큰 차이점은 this 바인딩. 이 차이로 인해 뒤에 설명할 클래스형 컴포넌트에서 이벤트에 바인딩할 메서드 선언 시 화살표 함수로 했을 때와 일반 함수로 했을 때 서로 다르게 작동함
      • this) 자신이 속한 객체나 자신이 생성할 인스턴스를 가리키는 값
      • 이 this는 화살표 함수 이전까지는 함수를 정의할 때 결정되는 것이 아니라, 함수가 어떻게 호출되느냐에 따라 동적으로 결정됨. 만약 함수가 일반 함수로서 호출된다면, 그 내부의 this는 전역 객체를 가리킴
      • 그러나 이와 달리 화살표 함수는 함수 자체의 바인딩을 갖지 않음. 화살표 함수 내부에서 this를 참조하면 상위 스코프의 this를 그대로 따르게 됨. 이에 대한 예제를 아래에서 리액트의 클래스형 컴포넌트 기반으로 살펴봄.
      • // 클래스형 컴포넌트에서 일반 함수와 화살표 함수로 state를 갱신하는 예제
        class Component extends React.Component {
          constructor(props) {
            super(props)
            this.state = {
              counter: 1,
            }
          }
        
          functionCountUp() {
            console.log(this) // undefined
            this.setState((prev) => ({ counter: prev.counter + 1 }))
          }
        
          ArrowFunctionCountUp = () => {
            console.log(this) // class Component
            this.setState((prev) => ({ counter: prev.counter + 1 }))
          }
        
          render() {
            return (
              <div>
                {/* Cannot read properties of undefined (reading 'setState') */}
                <button onClick={this.functionCountUp}>일반 함수</button>
                {/* 정상적으로 작동함. */}
                <button onClick={this.ArrowFunctionCountUp}>화살표 함수</button>
              </div>
            )
          }
        }
      • 위 두 메서드 functionCountUp과 ArrowFunctionCountUp은 모두 state를 하나씩 올리는 작업을 동일하게 하고 있으나 일반 함수에서의 this는 undefined를, 화살표 함수에서의 this는 우리가 원하는 대로 클래스의 인스턴스인 this를 가리키는 것을 볼 수 있음
      • 즉, 별도의 작업을 추가로 하지 않고 this를 접근할 수 있는 방법이 바로 화살표 함수인 것. 이러한 차이점은 바벨에서도 확인 가능
      • // 바벨 트랜스파일링으로 확인해 볼 수 있는 화살표 함수와 일반 함수의 차이
        // before:
        const hello = () => {
          console.log(this)
        }
        
        function hi() {
          console.log(this)
        }
        
        // after: 바벨에서는 이렇게 변환함
        var _this = void 0
        
        var hello = function hello() {
          // 바벨에서 화살표 함수 내부의 _this 자체를 undefined로 바꿔버림
          console.log(_this)
        }
        
        function hi() {
          console.log(this)
        }
      • 화살표 함수는 this가 선언되는 시점에 이미 상위 스코프로 결정돼 있어 미리 _this를 받아 사용하는 모습과 다르게, 일반 함수는 호출하는 런타임 시점에 결정되는 this를 그대로 따르는 모습
      • 이처럼 화살표 함수의 this는 선언 시점에 결정된다는 일반 함수와 대비되는 큰 차이점이 있기 때문에 단순히 일반 함수의 축약형으로 보기엔 무리가 있음
      • 따라서) 화살표 함수와 일반 함수를 사용할 때, 특히 this를 사용할 수 밖에 없는 클래스형 컴포넌트 내부에서 각별한 주의 필요요