시스템아 미안해

Chapter 3. Functions 본문

책/Eloquent Javascript

Chapter 3. Functions

if else 2025. 9. 25. 10:27

함수는 자바스크립트 프로그래밍의 핵심이다. 프로그램의 일부를 값으로 감싸는 개념은 여러 가지 용도를 가진다. 더 큰 프로그램을 구조화하고, 반복을 줄이며, 이름을 부여하고, 하위 프로그램을 서로 분리하는 방법을 제공한다.

가장 눈에 띄는 함수의 응용은 새로운 어휘를 정의하는 것이다. 산문에서 새로운 단어를 만드는 일은 보통 나쁜 스타일로 여겨지지만, 프로그래밍에서는 없어서는 안 된다.

보통 성인의 어휘는 약 2만 단어 정도다. 2만 개의 명령어를 내장한 프로그래밍 언어는 거의 없다. 그리고 언어가 제공하는 어휘는 인간 언어보다 정밀하게 정의되며 덜 유연하다. 그래서 보통은 새로운 개념을 소개해야만 반복을 피할 수 있다.


Defining a Function

함수 정의는 값이 함수인 정규 바인딩이다. 예를 들어, 아래 코드는 square라는 이름을 가진 함수를 정의하며, 주어진 숫자의 제곱을 반환한다:

const square = function(x) {
  return x * x;
};

console.log(square(12));
// → 144

 

함수는 function 키워드로 시작하는 표현식으로 만들어진다. 함수는 매개변수 집합(이 경우 x)과 본문을 가진다. 본문에는 함수가 호출될 때 실행될 문장이 들어 있다. 함수 본문은 항상 중괄호로 감싸야 하며, 문장이 하나만 있어도 마찬가지다.

함수는 매개변수를 여러 개 가질 수도 있고 하나도 없을 수도 있다.

 

 
const makeNoise = function() {
  console.log("Pling!");
};

makeNoise();
// → Pling!

const power = function(base, exponent) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
};

console.log(power(2, 10));
// → 1024

어떤 함수는 값을 반환하고(power, square), 어떤 함수는 반환하지 않고 부수효과만 남긴다(makeNoise).

return 문이 함수의 반환값을 결정한다.

return이 실행되면 즉시 함수를 종료하고 호출한 코드에 값을 전달한다.

값이 없는 return은 undefined를 반환한다. 반환문이 없는 함수도 undefined를 반환한다.

매개변수는 일반 바인딩처럼 동작하지만, 그 초기값은 함수 본문이 아닌 호출자가 결정한다.


Bindings and Scopes

함수가 호출될 때마다 새로운 바인딩 인스턴스가 생성된다. 이로 인해 함수끼리 어느 정도 격리된다.

let과 const로 선언된 바인딩은 선언된 블록에만 국한된다. 블록 밖에서는 접근할 수 없다.

ES2015 이전에는 var 키워드로 만든 바인딩만 있었으며, 이는 함수 전체 혹은 전역에서 유효했다.

 
let x = 10;
if (true) {
  let y = 20;
  var z = 30;
  console.log(x + y + z);
  // → 60
}
// y는 여기서 보이지 않음
console.log(x + z);
// → 40

각 스코프는 바깥 스코프를 “바라볼 수” 있다. 예를 들어, 함수 내부에서도 전역 x는 보인다. 하지만 동일한 이름의 변수가 여러 번 선언되면 가장 안쪽 스코프만 보인다:

 
const halve = function(n) {
  return n / 2;
};

let n = 10;
console.log(halve(100));
// → 50
console.log(n);
// → 10

Nested Scope

자바스크립트는 전역지역 스코프만 구분하는 것이 아니라, 블록과 함수 안에 또 다른 블록과 함수를 만들 수 있다. 이렇게 하면 여러 단계의 지역성을 가질 수 있다.

 
const hummus = function(factor) {
  const ingredient = function(amount, unit, name) {
    let ingredientAmount = amount * factor;
    if (ingredientAmount > 1) {
      unit += "s";
    }
    console.log(`${ingredientAmount} ${unit} ${name}`);
  };
  ingredient(1, "can", "chickpeas");
  ingredient(0.25, "cup", "tahini");
  ingredient(0.25, "cup", "lemon juice");
  ingredient(1, "clove", "garlic");
  ingredient(2, "tablespoon", "olive oil");
  ingredient(0.5, "teaspoon", "cumin");
};

ingredient 함수 내부에서는 factor를 볼 수 있지만, 외부에서는 ingredientAmount 같은 내부 바인딩을 볼 수 없다.
이런 식으로 바인딩이 보이는 범위는 코드의 블록 위치에 따라 정해진다. 이를 렉시컬 스코핑이라 한다.


Functions as Values

함수 바인딩은 보통 프로그램의 특정 부분을 가리키는 이름 역할을 한다. 하지만 함수는 값이므로, 다른 값처럼 표현식에 사용할 수도 있다.

 
let launchMissiles = function() {
  missileSystem.launch("now");
};
if (safeMode) {
  launchMissiles = function() {/* 아무것도 안 함 */};
}

Declaration Notation

function 키워드를 문장 시작에 놓으면 함수 선언이 된다:

 
function square(x) {
  return x * x;
}

이 방식은 약간 더 짧고 세미콜론이 필요 없다. 또한 호이스팅이 일어난다:

 
console.log("The future says:", future());

function future() {
  return "You’ll never have flying cars";
}

여기서는 함수가 아래에 정의되었음에도 위에서 호출이 가능하다.


Arrow Functions

세 번째 함수 표기법은 화살표 함수다. function 키워드 대신 =>를 사용한다:

const power = (base, exponent) => {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
};

매개변수가 하나라면 괄호를 생략할 수 있고, 본문이 단일 표현식이라면 중괄호와 return도 생략할 수 있다:

 
const square1 = (x) => { return x * x; };
const square2 = x => x * x;

매개변수가 없으면 빈 괄호를 쓴다:

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

화살표 함수는 2015년에 도입되었으며, 짧은 함수 표현식을 간단히 작성하려는 목적이다.


The Call Stack

함수 실행의 제어 흐름은 **호출 스택(call stack)**을 통해 관리된다.

 
function greet(who) {
  console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");

실행 순서는 greet 호출 시 함수 본문으로 점프했다가, 끝나면 호출한 위치로 돌아간다. 스택은 이런 “어디로 돌아갈지” 정보를 저장한다.

스택이 무한히 커지면 “out of stack space” 같은 에러가 발생한다. 이를 스택 오버플로우라 한다.


Optional Arguments

자바스크립트는 함수 호출 시 인수 개수에 관대하다. 인수가 많으면 무시되고, 부족하면 undefined가 된다.

 
function square(x) { return x * x; }
console.log(square(4, true, "hedgehog"));
// → 16

 

예를 들어 기본값처럼 활용할 수 있다:

function minus(a, b) {
  if (b === undefined) return -a;
  else return a - b;
}

console.log(minus(10));
// → -10
console.log(minus(10, 5));
// → 5

기본값 문법(=)을 사용하면 인수를 생략했을 때 기본값을 넣을 수 있다:

 
function power(base, exponent = 2) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
}

console.log(power(4));
// → 16
console.log(power(2, 6));
// → 64

console.log는 전달받은 모든 인수를 출력한다.


Closure

함수를 값으로 다루고, 함수가 호출될 때마다 지역 바인딩이 새로 만들어지는 특성 때문에 **클로저(closure)**가 가능하다.

 
function wrapValue(n) {
  let local = n;
  return () => local;
}

let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2

wrapValue에서 반환된 함수는 여전히 local에 접근할 수 있다. 이를 클로저라고 부른다.

이걸 활용하면 특정 인자를 기억하는 함수를 만들 수 있다:

 
function multiplier(factor) {
  return number => number * factor;
}

let twice = multiplier(2);
console.log(twice(5));
// → 10

여기서 multiplier는 새로운 스코프를 만들고, factor를 기억한다. 그래서 twice를 호출하면 factor = 2가 유지된다.


Recursion

함수가 자기 자신을 호출하는 것은 괜찮다. 단, 너무 자주 호출해서 스택을 넘치게 만들지만 않으면 된다.

자기 자신을 호출하는 함수를 재귀 함수라고 한다.

 
function power(base, exponent) {
  if (exponent == 0) {
    return 1;
  } else {
    return base * power(base, exponent - 1);
  }
}

console.log(power(2, 3));
// → 8

이 방식은 수학적 정의와 비슷하고 반복문보다 개념적으로 명확하다. 하지만 반복문보다 느리다.

따라서 단순한 문제에는 반복문이 적합하지만, 복잡한 개념에는 재귀가 더 직관적일 수 있다.

효율성 걱정은 종종 산만하다. 처음에는 올바르고 읽기 쉬운 코드를 작성하고,

성능이 문제라면 나중에 측정해서 개선하는 것이 낫다.


재귀는 반복문보다 비효율적인 대체물이 아니라, 어떤 경우에는 반복문보다 더 나은 방법이다. 특히 여러 갈래로 분기하는 탐색 문제에서 그렇다.

 
function findSolution(target) {
  function find(current, history) {
    if (current == target) {
      return history;
    } else if (current > target) {
      return null;
    } else {
      return find(current + 5, `(${history} + 5)`) ||
             find(current * 3, `(${history} * 3)`);
    }
  }
  return find(1, "1");
}

console.log(findSolution(24));
// → (((1 * 3) + 5) * 3)

Growing Functions

함수를 만드는 두 가지 주요 이유는 다음과 같다.

  1. 같은 코드를 여러 번 작성하지 않기 위해.
  2. 새로운 기능을 분리해서 구현하기 위해.

예시: 농장 동물을 세 자리 수로 출력하기.

 
function printFarmInventory(cows, chickens) {
  let cowString = String(cows);
  while (cowString.length < 3) {
    cowString = "0" + cowString;
  }
  console.log(`${cowString} Cows`);

  let chickenString = String(chickens);
  while (chickenString.length < 3) {
    chickenString = "0" + chickenString;
  }
  console.log(`${chickenString} Chickens`);
}

printFarmInventory(7, 11);

개선:

 
function zeroPad(number, width) {
  let string = String(number);
  while (string.length < width) {
    string = "0" + string;
  }
  return string;
}

function printFarmInventory(cows, chickens, pigs) {
  console.log(`${zeroPad(cows, 3)} Cows`);
  console.log(`${zeroPad(chickens, 3)} Chickens`);
  console.log(`${zeroPad(pigs, 3)} Pigs`);
}

printFarmInventory(7, 16, 3);

Functions and Side Effects

함수는 크게 두 가지다.

  1. 부수효과 때문에 호출되는 함수 – 예: console.log.
  2. 반환값 때문에 호출되는 함수 – 예: zeroPad.

**순수 함수(pure function)**는 부수효과가 없고 동일 입력에 동일 출력을 준다. 테스트와 재사용이 쉽다.
**비순수 함수(nonpure function)**는 전역 상태를 읽거나 외부에 영향을 준다. 하지만 실제로는 부수효과가 필요한 경우가 많다.


Summary

  • function 키워드와 화살표 함수로 함수를 정의할 수 있다.
  • 스코프는 바깥 변수를 볼 수 있지만, 안쪽 변수는 바깥에서 볼 수 없다.
  • 프로그램을 여러 함수로 나누면 반복을 줄이고 구조화할 수 있다.

예시:

 
const f = function(a) {
  console.log(a + 2);
};

function g(a, b) {
  return a * b * 3.5;
}

let h = a => a % 3;

Exercises

Minimum
두 수를 받아 더 작은 값을 반환하는 min 함수를 작성하라.

Recursion
숫자가 짝수인지 홀수인지 판별하는 isEven을 재귀적으로 작성하라.

  • 0은 짝수
  • 1은 홀수
  • 그 외 숫자는 N - 2와 동일

50과 75로 테스트하고, -1에서 어떻게 동작하는지 확인해 보라.

Bean Counting
문자열에서 문자의 개수를 세는 함수 작성:

  • countBs(string) → 문자열에서 'B'가 몇 개인지 반환
  • countChar(string, char) → 특정 문자가 몇 개인지 반환
  • countBs를 countChar를 이용해 다시 작성