JS04 - 함수와 스코프

조예진·2022년 1월 22일
0

JavaScript 스터디

목록 보기
5/16
post-thumbnail

본 시리즈는 모던 자바스크립트 Deep Dive 책을 참고하여 작성하고 있습니다.

함수란?

수학에서의 함수는 “입력”을 받아 “출력”을 내보내는 일련의 과정을 정의한 것이다. 프로그래밍에서의 함수는 일련의 과정을 문으로 구현하고, 이를 코드 블록으로 감싸서, 하나의 실행 단위로 정의한 것이다.

  • 함수: 입력을 받아 출력을 내보내는 일련의 과정을 문으로 정의하고 코드 블록으로 묶어 하나의 실행 단위로 정의한 것
  • 매개변수(parameter): 함수 내부로 입력을 전달받는 변수
  • 인수(argument): 입력
  • 반환값(return value): 출력

함수는 함수 정의를 통해 생성한다. 함수를 정의했다고 해서 함수가 실행되지는 않는다. 함수를 실행하기 위해서는 매개변수를 통해 함수에 인수를 전달하며 함수 실행을 명시적으로 지시해야 한다. 이것을 함수 호출이라고 한다.

  1. 함수 선언문을 통한 함수 정의
// add: 함수 이름 (함수 몸체 안에서만 참조 가능)
// x, y: 매개변수
function add(x, y) { 
  return x + y; // 반환값
}
  1. 함수를 호출하기
var result = add(2, 5); // 인수로 2, 5를 전달하면서 함수 실행을 지시하기

함수를 사용하는 이유

  1. 코드의 재사용

    x = 1;
    y = 2;
    result = x + y;
    
    x = 3;
    y = 2;
    result = x + y;
    
    ... // 100번 반복해야 한다면?
    
    // --> 이렇게 고칠 수 있다
    function add(x, y) {
      return x + y;
    }
    
    result = add(1, 2);
    result = add(3, 2);
  2. 유지보수의 편의성 & 코드의 신뢰성

    만약에 함수를 사용하지 않고 비슷한 로직을 반복해서 사용할 때, 그 로직을 수정해야 한다면, 로직이 사용된 부분을 모두 찾아서 일일이 수정해 주어야 한다. 그 과정에 걸리는 시간은 매우 길 것이고, 누락되는 부분이 있을 수도 있고 실수를 할 수도 있다.

    함수로 로직을 묶어서 관리하면, 로직에 수정 사항이 생겼을 때 함수의 내용만 수정해 주면 되기 때문에 편리하고, 실수를 줄일 수 있어 코드의 신뢰성을 높일 수 있다.

  3. 코드의 가독성 향상

    함수에는 이름을 붙일 수 있다. 그래서 적절한 이름을 잘 붙여 주면, 함수 내부의 코드를 분석하지 않아도 어떤 동작을 하는 함수인지 파악할 수 있다.

함수 리터럴

자바스크립트의 함수는 객체 타입의 값이다. 따라서 함수도 함수 리터럴로 생성할 수 있다. 그리고 변수에 할당할 수도 있다. 왜냐하면 함수 리터럴도 평가되어 값을 생성하며, 그 값은 객체이기 때문이다.

var f = function add(x, y) {
  return x + y;
}

일급 객체

함수는 조금 특별한 객체이다. 일반 객체와는 달리 함수는 호출할 수 있다. 이러한 특징을 가지는 객체를 일급 객체라고 한다. 일급 객체는 다른 객체들에게 적용 가능한 연산을 모두 지원하는 객체이다. 함수가 일급 객체라는 것은, 함수를 객체와 동일하게 사용할 수 있으며, 함수를 값과 동일하게 취급할 수 있다는 것이다. 따라서 값을 사용할 수 있는 곳에서는 어디서든 리터럴로 정의 가능하며 런타임에 함수 객체로 평가된다.

일급 객체의 특징은 아래와 같다.

  1. 변수나 자료구조(객체, 배열) 안에 담을 수 있다

    var x = function add(x, y) { return x + y }
    var obj = {
      add: function(x, y) { return x + y }
    }
  2. 파라미터로 전달할 수 있다

    function fetchData(callback) { callback(); }
    
    fetchData((data) => console.log(data));
  3. 리턴 값으로 사용할 수 있다

    function calculator(x, y) {
      var add = function add() { return x + y };
      return add;
    }
    var calc = calculator(1, 2)
    calc() // 3
  4. 무명의 리터럴로 생성할 수 있다. 즉, 런타임 생성이 가능하다.

    var increase = function (num) {
      return ++num;
    }
    
    var n = 3
    increase(3); // 4

함수 객체의 프로퍼티

console.dir 메서드를 사용하면 객체의 내부를 확인할 수 있다.

함수는 arguments, caller, length, name, prototype 프로퍼티를 가지고, 이는 함수 객체 고유 프로퍼티이다.

  • arguments: 함수 호출 시 전달된 인수의 정보
    • 매개변수와 인수의 개수가 일치하지 않아도 에러가 발생하지 않는다. 매개변수는 변수와 동일하게 취급되므로, 매개변수는 함수가 호출되었을 때 암묵적으로 선언된 후 undefined로 초기화된 후에 인수가 있으면 할당된다. 인수가 매개변수보다 적게 전달되면 인수를 할당받지 못한 매개변수는 undefined로 남고, 인수가 매개변수를 초과하여 전달되면 초과된 것들은 무시된다. 대신, arguments 객체에 저장된다.

    • 매개변수의 개수를 확정할 수 없는 가변 인자 함수를 구현할 때 유용하다.

      function sum() {
        var result = 0;
      
        for (var i = 0; i < arguments.length; i++) {
          result += arguments[i];
        }
        return result;
      }
      **sum(); // 0**
      sum(2, 43, 5, 7, 8, 9);
    • 유사 배열 객체이다. 즉, 배열 메서드를 사용할 수 없다. (map, filter, reduce 등..)

      var arr = [3, 4, 5, 6]
      var result = 0;
      var sum = arr.forEach(x => result += x) // 18
      
      function sum() {
        var result = 0;
      
      // VM153721:3 Uncaught TypeError: arguments.forEach is not a function
        arguments.forEach(a => result += a); 
        return result;
      }
    • ES6에서는 Rest 파라미터가 도입되었다.

      function(**...arg**) {
        return args.reduce((pre, cur) => pre + cur, 0);
      }
  • caller: (비표준 프로퍼티) 함수 자신을 호출한 함수를 가리킴
  • length: 함수 정의 시 선언한 매개변수의 개수를 가리킴
  • name: 함수 이름 (ES6에서 표준이 되었음)
    • 익명 함수 표현식의 경우,
      • var anonymous = function() {}; anonymous.name;
      • ES5에서 → 빈 문자열 (’’)
      • ES6에서 → 식별자 이름 (anonymous)
  • **__proto__** 접근자 프로퍼티
    • 모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가진다. __proto__ 프로퍼티는 프로토타입 내부 슬롯의 프로토타입 객체에 접간하기 위한 프로퍼티이다. 프로토타입은 19장 프로토타입에서 살펴봅시다
  • prototype 프로퍼티: 함수가 객체를 생성하는 생성자 함수로, 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가리킨다. constructor만이 이 프로퍼티를 가진다.

함수를 정의하는 방법

변수는 선언한다고 말하고, 함수는 정의한다고 말한다. 함수 선언문은 평가되면 식별자가 암묵적으로 생성되고 함수 객체가 할당된다.

  1. 함수 선언문

    • 함수 이름을 생략할 수 없다.
    • 함수 리터럴과 형태가 동일하다.
    • 표현식이 아닌 문이다. 값으로 평가되지 않고, 변수에 할당할 수 없다.
    function add (x, y) {
      return x + y;
    }
  2. 함수 표현식

    • 함수 리터럴로 생성한 함수 객체를 변수에 할당하는 방식이다.
    • 함수 이름을 생략할 수 있다. 그 경우 익명 함수라고 한다.
    • 표현식인 문이다.
    var add  = function (x, y) {
      return x + y;
    }
  3. Function 생성자 함수

    • Function 생성자 함수는 자바스크립트가 기본 제공하는 빌트인 함수이다.
    • 매개변수 목록과 함수 몸체를 문자열로 전달하며 new 연산자와 호출하면 함수 객체를 생성하여 반환한다.
    • 일반적이지 않고 바람직하지 않다. 클로저를 생성하지 않는다.
    var add = new Function('x', 'y', 'return x + y');
  4. 화살표 함수(ES6)

    • 항상 익명 함수로 정의된다.
    • 일반 함수와 다르게 동작한다.
      • 생성자 함수로 사용할 수 없고
      • this 바인딩 방식이 다르고
      • prototype 프로퍼티가 없으며
      • arguments 객체를 생성하지 않는다.
    var add = (x, y) => x + y;

자바스크립트 엔진은 코드의 문맥에 따라, 같은 형태의 함수 리터럴을 표현식이 아닌 문으로 해석할 때도 있고, 표현식인 문으로 해석할 때도 있다. 표현식이 아닌 문은 함수 선언문이 되고 표현식인 문은 함수 리터럴 표현식이 된다. 즉, 이름이 있는 함수 리터럴은 함수 선언문 혹은 함수 리터럴 표현식으로 해석될 수 있다. 중의적인 코드인 것이다.

함수 선언문으로 해석하는 경우

  • 함수 리터럴이 단독으로 사용되는 경우

함수 리터럴 표현식으로 해석하는 경우

  • 함수 리터럴이 값으로 평가되어야 하는 문맥
    • 변수에 할당될 때
    • 피연산자로 사용할 때

자바스크립트 엔진은 함수 선언문을 토대로 함수 객체를 생성한다. 이 때 선언되는 함수 이름은 함수 몸체 내부에서만 접근이 가능한 식별자이므로, 함수 밖에서는 함수 이름으로 함수를 호출할 수 없다. 즉, 함수에 접근하려면 객체를 가리키는 식별자가 필요하다.

함수 선언문

자바스크립트 엔진은 함수 선언문을 해석해서 함수 객체를 생성하고, 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고, 여기에 생성된 함수 객체를 할당한다. 따라서 함수 이름과 식별자는 별개이다. 함수 이름은 함수 내부에서만 호출 가능하므로, 함수 외부에서 함수를 호출하는 것은 함수 이름이 아니라 식별자로 함수를 호출한 것이다.

// 이런 함수 선언문이 있다면
function add(x, y) {
  return x + y;
}

// 사실은 이런 식으로 동작한다
var add = function add(x, y) {
  return x + y;
}

함수 생성 시점과 함수 호이스팅

변수 챕터에서 살펴봤듯이, 자바스크립트 엔진은 코드를 실행하기 이전에 평가 과정을 거치고, 이 과정에서 선언문을 먼저 실행한다. 그래서 var 키워드로 선언한 변수는, 변수 선언문 이전에 변수에 접근해도 undefined라는 값을 얻을 수 있었다.

console.log(add); // f add(x, y)
console.log(sub); // undefined

console.log(add(2, 3)) // 5
console.log(sub(2, 3)) // sub is not a function

function add(x, y) {
  return x + y;
}

var sub = function(x, y) {
  return x - y;
}

함수 선언문도 선언문이기 때문에 런타임 시점 이전에 자바스크립트 엔진에 의해 먼저 실행된다. 즉, 함수 호이스팅이 발생한다. 그래서 위 예제 코드처럼, 함수 선언문 이전에 함수 이름을 참조하거나 함수를 호출해도 잘 동작한다. 이러한 현상을 함수 호이스팅이라고 한다. 함수 선언문을 통해 암묵적으로 생성된 식별자는 함수 객체로 초기화된다.

함수 표현식은 변수에 함수 객체를 할당하는 문이다. 즉, 변수 선언문과 변수 할당문을 한 번에 축약한 표현이다. 이 때, 변수 선언은 런타임 이전에 실행되어 undefined로 초기화된다. 즉, 함수 표현식에서는 변수 호이스팅이 발생한다. 함수 리터럴은 런타임 시점에 평가되어 함수 객체로 생성되고, 변수에 할당된다. 그래서 위의 sub 변수는 출력했을 때 undefined 값을 출력하고, 실행하려고 하면 함수가 아니라고 한다.

그런데 함수 호이스팅 현상은 함수를 호출하기 전에 함수를 선언해야 한다는 규칙을 무시한다. 그래서 함수 선언문 대신 함수 표현식을 사용할 것이 권장된다.

다양한 함수의 형태

순수 함수와 비순수 함수

  • 순수 함수(pure function): 외부 상태를 변경하지 않고 외부 상태에 의존하지도 않는 함수
    • 동일한 인수가 전달되면 언제나 동일한 값을 반환
    • 매개변수에만 의존해 반환값을 만든다
  • 비순수 함수(impure function): 외부 상태에 의존하거나 외부 상태를 변경하는, 부수 효과가 있는 함수
    • 함수 외부 상태에 따라 반환값이 달라진다
var count;

function pure_increase(n) {
  return ++n;
}

function impure_increase() {
  return ++count;
}

함수형 프로그래밍: 순수 함수를 통해 부수효과를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이려는 프로그래밍 패러다임. 불변성을 지향

  • 로직 내의 반복문과 조건문을 제거 → 복잡성 해결
    • 반복문, 조건문은 로직의 흐름을 이해하기 어렵게 한다
  • 변수 사용 억제, 생명주기 최소화 → 상태 변경 피함, 오류 최소화
    • 변수 값은 언제든 변경될 수 있어 오류 발생 여지가 있다

원시 타입 인수는 값 자체가 복사되어 넘어가므로, 즉 값에 의한 전달이 수행되므로 원본이 훼손되지 않는다. 부수 효과도 발생하지 않는다. 객체 타입 인수는 참조 값이 복사되어 넘어가므로, 즉 참조에 의한 전달이 수행되므로 함수 내부에서 객체를 수정하면 원본이 훼손된다. 따라서 부수 효과가 발생할 수도 있다. 이렇게 외부 상태를 변경하게 되면 상태 변화를 추적하기가 어려워지고, 코드가 복잡해지며, 가독성을 해친다.

객체의 변경을 추적하기 위해서 옵저버 패턴으로 대응을 하기도 한다. 객체가 변경되면 객체를 참조하는 모든 이들에게 변경되었다는 사실을 알려준다.

객체를 불변 객체로 만들어 사용하기도 한다. 객체를 마치 원시 값처럼 동작하게 만드는 것이다. 객체의 상태 변경이 필요하다면 객체의 방어적 복사(defensive copy), 즉 깊은 복사를 통해서 원본 객체를 완전히 복제한 새로운 객체를 생성하고 재할당해 교체한다. 외부 상태가 변경되는 부수 효과를 없앨 수 있지만, 객체를 만드는 비용이 든다.

즉시 실행 함수

  • 함수 정의와 동시에 즉시 호출됨
  • 한 번만 실행되고 다시 호출 불가
  • 그룹 연산자 (...) 으로 감싸주어야 함
    function foo() {
     ...
    }();
    
    // --> 세미콜론 자동 삽입으로 인해, 이렇게 해석된다
    function foo() {}; ();
// 가장 선호되는 방식 - 함수 리터럴 전체를 괄호로 묶기
(function () {
  ...
}());

(function () {
  ...
})();

// 아래 단일 연산자의 경우 함수 리터럴로 평가되어 함수 객체가 생성된다
!function () {
  ...
}();

+function () {
  ...
}();

중첩 함수

  • 함수 내부에 정의된 함수 = 내부 함수
  • ES6부터, 함수 정의는 문이 위치할 수 있는 문맥이라면 어디든 가능. 따라서 for 문이나 if 문 등의 코드 블럭 내에서도 선언 가능하다
    • 호이스팅 발생으로 인한 혼란이 발생할 수 있으므로 바람직한 방법은 아니다
    • 기존에는 코드 최상위 혹은 다른 함수 내부에서만 가능
// 외부 함수
function outer() {

  // 내부 함수 = 중첩 함수
  function inner() {
    ...
  }
  
  inner();
}

콜백 함수

  • 콜백 함수(callback function): 매개변수를 통해 다른 함수 내부로 전달되는 함수
  • 고차 함수(Higher-Order function, HOF): 함수의 외부에서 콜백 함수를 전달받은 함수

고차 함수는 콜백 함수를 자신의 일부분으로 합성한다. 고차 함수는 매개변수를 통해 콜백 함수를 전달받고, 함수 내부에서 콜백 함수의 호출 시점을 결정해서 호출한다. 콜백 함수는 고차 함수에 의해 호출되고, 호출될 때 인수를 전달할 수도 있다. 고차 함수에 콜백 함수를 전달할 때는 함수 객체 자체를 전달해야 한다. 함수는 일급 객체이므로 값으로 다룰 수 있고, 매개변수를 통해 전달할 수 있다. 비동기 처리나 배열 고차 함수에서 사용된다.

어떤 함수들은 공통 로직을 가지고 있으면서 일부분만 다른 경우가 있다. 이러한 경우 공통의 로직이 있더라도 함수를 새롭게 정의해야 한다는 불편함이 있다. 따라서 공통 로직을 고차 함수로 정의해 두고, 변경되는 로직은 추상화하여 콜백 함수 형태로 함수 외부에서 함수 내부로 전달하는 것이다.

function repeatAndLogOdd(n) {
  for (var i = 0; i < n; i++) {
    if (i % 2) console.log(i);
  }
}

function repeatAndLogEven(n) {
  for (var i = 0; i < n; i++) {
    if (i % 2 === 0) console.log(i);
  }
}

repeatAndLogOdd(7);
repeatAndLogEven(7);

// --> 이렇게 바꿀 수 있다

function repeat(n, callback) {
  for (var i = 0; i < n; i++) {
    callback(i);
  }
}

function logOdd(n) {
  if (i % 2) console.log(i);
}

function logEven(n) {
  if (i % 2 === 0) console.log(i);
}

repeat(7, logOdd);
repeat(7, logEven);

콜백 함수를 전달할 때는 함수 리터럴 방식으로 전달하는 방식이 일반적이다. 그런데 이렇게 전달되는 콜백 함수는 고차 함수가 호출될 때마다 평가되어 함수 객체가 생성된다. 따라서 고차 함수가 자주 호출되는 함수라면 함수 외부에서 콜백 함수를 정의한 후에 함수의 참조를 고차 함수에 전달하는 것이 효율적이다.

// repeatAndLogEven을 바꾸면
repeat(6, (n) => {
  if (n % 2) console.log(n);
}

스코프

모든 식별자는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정된다. 스코프는 식별자가 유효한 범위이다. 식별자에는 변수 이름, 함수 이름, 클래스 이름 등이 있다.

var a = 3;

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

foo(5);

위의 상황처럼 같은 이름의 식별자가 있을 때, 자바스크립트 엔진이 어떤 것을 참조해야 할 지 결정하는 과정을 식별자 결정이라 한다. 스코프는 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙이라고 할 수도 있다. 스코프라는 개념이 없다면 같은 변수 이름을 프로그램 내에서 한 번 밖에 사용하지 못할 것이다. 스코프는 변수 이름의 충돌을 방지해 같은 이름의 변수를 사용 가능하게 한다. 스코프는 네임스페이스이다.

💡 **렉시컬 환경**이란 코드가 어디에서 실행되고 주변에 어떤 코드가 있는지를 말한다. 코드의 문맥은 렉시컬 환경으로 이루어진다. 이것을 구현한 것이 **실행 컨텍스트**

스코프의 종류

변수는 자신이 선언된 위치에 의해 스코프가 결정된다. 전역에서 선언되면 전역 스코프를 가지고, 함수의 내부인 지역에서 선언된 변수는 지역 스코프를 가진다.

  • 전역 스코프
    • 전역: 코드 가장 바깥 영역
    • 전역 변수는 어디서든 참조 가능
  • 지역 스코프
    • 지역: 함수 몸체 내부
    • 자신의 지역 스코프와 하위 지역 스코프에서 유효

스코프 체인

함수는 중첩될 수 있다. 따라서 지역 스코프도 중첩될 수 있다. 즉, 스코프는 함수의 중첩에 의해 계층 구조를 가진다. 중첩 함수의 외부 함수의 스코프를 상위 스코프라고 부른다. 모든 스코프는 하나의 계층적 구조로 연결되고, 최상위 스코프는 전역 스코프이다. 이처럼 계층적으로 연결되는 것을 스코프 체인이라고 한다.

변수를 참조하면, 자바스크립트 엔진은 스코프 체인을 통해 변수를 검색한다. 변수를 참조하는 코드의 스코프에서 시작해서 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.

var x = 3;
var y = 2;

function outer() {
  var x = 65;
  var y = 33;

  console.log(x, y);
  
  function inner() {
    var x = 333;
    
    console.log(x, y);
  }
}

console.log(x, y);

자바스크립트 엔진은 코드를 실행하기에 앞서서 렉시컬 환경이라는 자료구조를 실제로 생성한다. 선언된 변수가 있다면 렉시컬 환경에 키 값이 식별자 이름으로 등록되고, 변수 할당이 일어나면 변수 식별자에 해당하는 값이 변경된다. 스코프 체인은 렉시컬 환경을 단방향으로 연결한 것이다. 함수의 렉시컬 환경은 함수가 호출되면 바로 생성된다.

스코프 체인을 검색할 때는 현재 스코프에서 상위 스코프로 향하는 방향으로만 검색한다. 따라서 현재 스코프보다 하위 스코프에 유효한 변수는 참조할 수 없다.

함수도 객체이다. 함수 선언문으로 함수를 정의하면 함수 객체가 생성되고, 이 객체는 함수 이름과 동일한 이름으로 암묵적으로 선언되는 식별자에 할당된다. 따라서 함수도 식별자에 할당되므로 위의 그림처럼 스코프를 가진다. 따라서 스코프는 식별자를 검색하는 규칙이라고 보는 것이 적합하다.

함수 레벨 스코프

지역 스코프는 함수에 의해서만 생성된다. 지역은 함수 내부를 말한다. var 키워드로 선언된 변수는 함수 몸체만을 지역 스코프로 인정한다. 이러한 특성을 함수 레벨 스코프라고 한다. 반면에 대부분의 프로그래밍 언어에서는 모든 코드 블록(if, for, while, ...)이 지역 스코프를 만든다. 이러한 특성은 블록 레벨 스코프라고 한다.

var i = 10;

for (var i = 0; i < 5; i++) {
  ...
}

console.log(i); // 5

위의 예시처럼 함수 레벨 스코프는 전역 변수의 값을 의도치 않게 변경하게 될 여지가 있다. 따라서 ES6부터는 블록 레벨 스코프를 지원하는 const, let 을 사용하기를 권장한다.

렉시컬 스코프

자바스크립트는 렉시컬 스코프를 따르므로 함수를 어디서 정의했는지에 따라 상위 스코프가 결정된다. 함수의 상위 스코프는 항상 자신이 정의된 스코프이다. 함수의 상위 스코프는 함수 정의가 실행될 때 정적으로 결정된다. 함수 객체는 정의될 때 결정된 상위 스코프를 기억한다.

var x = 1;

function foo() {
  var x = 10;
  bar();

function bar() {
  console.log(x);
}

foo()
bar()

위 예제에서 bar 함수는 두 번 호출된다. 호출의 결과는 두 가지로 예측이 가능하다.

// 1) 호출한 위치에 의해 스코프가 결정된다면
10, 1

// 2) 정의한 위치에 의해 스코프가 결정된다면
1, 1

1)의 경우를 동적 스코프라고 한다. 함수가 호출되는 시점에 동적으로 상위 스코프를 결정하는 방식이다.

2)의 경우는 렉시컬 스코프 혹은 정적 스코프이다. 함수 정의가 평가되는 시점에 스코프가 정적으로 결정된다. 대부분의 프로그래밍 언어가 렉시컬 스코프를 따른다.

profile
https://oooooroblog.com 으로 이사갔어요

0개의 댓글