Skip to content
This page has been auto-translated and may contain errors.View in English

함수

docs.scrimba.com

장바구니에 담긴 상품의 가격을 계산한다고 해봅시다. 숫자를 받아서 세금을 더하고, 반올림하고, 통화 기호를 붙여 형식을 맞춥니다. 그런데 다음 상품에서도 똑같은 과정을 반복해야 하고, 그다음 상품에서도 마찬가지입니다. 이 네 단계를 곳곳에 복사해두면 버그가 생기기 딱 좋습니다. 반올림 방식을 한 곳에서 고쳐도 다른 세 군데는 여전히 잘못된 채로 남기 때문입니다. 함수를 쓰면 이런 일련의 단계에 이름을 붙여 한 번만 정의해두고, 필요할 때마다 실행할 수 있습니다. 이번 장에서는 이런 코드 블록을 작성하고, 입력값을 전달하고, 결과값을 돌려받는 방법을 다룹니다.

함수 선언과 호출

**함수**란 원할 때마다 실행할 수 있도록 이름을 붙인 코드 블록입니다. 단계들을 한 번만 작성해서 이름을 붙여두면, 그 이름으로 원하는 만큼 여러 번 실행할 수 있습니다.

function 키워드로 함수를 만듭니다:

js
function greet() {
  console.log("Hi there");
}

이것이 함수 정의의 전부이지만, 아직 아무 일도 일어나지 않았습니다. 함수를 작성하는 것은 단계를 저장해두는 것일 뿐, 실행하는 것이 아닙니다. 실제로 실행하려면 함수 이름 뒤에 괄호를 붙여서 호출해야 합니다:

js
greet(); // Hi there
greet(); // Hi there

두 줄에서 인사가 두 번 나온 것은 하나의 코드 블록에서 나온 결과입니다. 정의하는 것과 호출하는 것은 별개의 일이며, 이 둘을 헷갈리는 것이 처음에 가장 많이 겪는 실수입니다.

**함수**는 코드 블록을 이름으로 감싸서 필요할 때 실행할 수 있게 해줍니다. function 키워드로 한 번 정의해두면, 필요한 만큼 여러 번 호출할 수 있습니다:

js
function logDivider() {
  console.log("----------");
}

logDivider(); // ----------
logDivider(); // ----------

정의 자체는 단계를 설정할 뿐 아무것도 실행하지 않습니다. 함수는 ()로 호출할 때만 실행됩니다. logDivider라는 이름 자체는 함수 값이고, 괄호는 그것을 실행시키는 장치입니다. 함수를 다른 곳에 전달하기 시작하면 이 구분이 중요해지는데, logDividerlogDivider()는 서로 다른 대상이기 때문입니다. 하나는 함수 자체이고, 다른 하나는 그 함수를 실행한 결과입니다.

**함수**는 이름에 묶인 재사용 가능한 코드 블록입니다. 여기서 function 키워드는 **함수 선언(function declaration)**을 만듭니다. 함수 선언은 자신이 속한 스코프의 맨 위로 끌어올려지므로(호이스팅), 정의보다 앞선 줄에서 호출해도 정상 동작합니다.

js
countdown(); // 동작합니다: 선언은 호이스팅되기 때문입니다

function countdown() {
  console.log("Ready");
}

이 호이스팅은 선언 형태에만 해당하는 특성이며, 이번 장에서 다룰 다른 함수 정의 방식들은 이 특성을 갖지 않습니다. 함수 이름과 함수 호출은 서로 다른 값입니다. countdown은 함수 자체로 평가되므로(저장하거나, 전달하거나, 비교할 수 있습니다), countdown()은 함수를 실행한 결과로 평가됩니다. 이 둘을 혼동하는 것이 "왜 undefined가 나오지?"라는 혼란의 상당 부분을 차지하므로, 함수가 다른 함수의 인자로 전달되기 시작하기 전 지금부터 둘을 구분하는 습관을 들여두는 것이 좋습니다.

함수가 하는 일을 잘 드러내는 동사 형태의 이름(greet, logDivider)을 붙이세요. 변수 이름을 지을 때 기울였던 것과 같은 정성이 필요합니다. 열 군데에서 호출하는 함수라도 이름이 명확하지 않으면 읽기 좋은 코드가 될 수 없습니다.

Juno함수 선언과 호출 함수를 작성하는 것은 단계를 저장해두는 것일 뿐, 실행하는 것이 아닙니다. 실행하려면 greet()처럼 괄호를 붙여 이름으로 호출해야 합니다. 처음엔 이 구분이 헷갈렸는데, 정의는 레시피이고 호출은 실제로 요리를 하는 것이라고 생각하면 됩니다. 같은 함수를 원하는 만큼 여러 번 호출할 수 있습니다.
Juno함수 선언과 호출 함수를 정의하는 것과 호출하는 것은 별개의 행위입니다. function 블록은 단계를 설정할 뿐, ()를 붙이기 전까지는 아무것도 실행하지 않습니다. greetgreet()를 머릿속에서 확실히 구분해두세요. 하나는 함수 값이고, 다른 하나는 그 실행 결과입니다. 이 차이는 함수를 어딘가에 전달하는 순간 문제가 될 수 있습니다.
Juno함수 선언과 호출function 키워드는 호이스팅되는 선언을 만들기 때문에 정의보다 위에서 호출할 수 있습니다. 이는 이 형태에만 해당하는 특성으로, 함수 표현식이나 화살표 함수는 같은 방식으로 호이스팅되지 않습니다. 그리고 countdown은 함수 자체이고 countdown()은 그 결과인데, 함수를 값으로 전달하기 시작하면 이 구분이 점점 더 중요해집니다.

매개변수와 반환값

함수는 무언가를 전달받을 수 있을 때 훨씬 쓸모가 커집니다. 이렇게 전달하는 값을 **매개변수(parameters)**라고 부릅니다. 괄호 안에 나열된 이름으로, 호출할 때 전달하는 값을 대신 나타냅니다.

js
function greet(name) {
  console.log("Hi " + name);
}

greet("민준"); // Hi 민준
greet("서연"); // Hi 서연

name이 매개변수입니다. greet("민준")을 호출하면 그 실행 동안 name"민준"이라는 텍스트가 들어갑니다. 함수는 하나지만, 전달하는 값에 따라 결과가 달라집니다.

지금까지 본 함수들은 무언가를 출력할 뿐입니다. 하지만 나중에 사용할 수 있도록 함수가 값을 돌려주었으면 할 때가 많습니다. 이때 쓰는 것이 return입니다:

js
function double(number) {
  return number * 2;
}

const result = double(5);
console.log(result); // 10

return은 함수 밖으로 값을 내보냅니다. double(5)10이 되고, 이 값을 저장하거나 출력하거나 다른 곳으로 넘길 수 있습니다. 출력은 값을 화면에 보여주는 것이고, 반환은 그 값을 코드에서 계속 사용할 수 있도록 돌려주는 것입니다.

함수는 **매개변수(parameters)**를 통해 입력값을 받습니다. 매개변수는 괄호 안에 나열된 이름입니다. 호출할 때 전달하는 값은 이 매개변수를 채우는 **인자(arguments)**입니다.

js
function priceWithTax(price, rate) {
  return price + price * rate;
}

const total = priceWithTax(40, 0.2);
console.log(total); // 48

pricerate는 매개변수이고, 400.2는 이번 호출의 인자입니다. return 문은 함수를 호출한 쪽에 값을 돌려주므로, priceWithTax(40, 0.2)48로 평가되어 total에 저장할 수 있습니다.

로그를 남기는 것과 값을 반환하는 것의 차이는 명확히 짚어둘 필요가 있습니다. console.log를 호출하는 함수는 화면에 무언가를 보여줄 뿐 undefined로 평가되고, return을 사용하는 함수는 프로그램이 계속 활용할 수 있는 값을 만들어냅니다. return은 실행되는 즉시 함수를 종료시키므로, 그 뒤에 있는 코드는 실행되지 않습니다. return을 아예 쓰지 않으면 함수는 여전히 undefined를 반환하는데, 값을 기대했는데 아무것도 돌아오지 않아 당황하는 흔한 상황이 바로 이 경우입니다.

매개변수는 함수가 이름을 붙여둔 입력값이고, 인자는 호출 시점에 그 이름에 실제로 묶이는 구체적인 값입니다. return 문은 함수의 출력값을 만들어내는 동시에 실행을 즉시 종료시키므로, 조기 종료 용도로도 쓰입니다:

js
function classify(score) {
  if (score < 0) {
    return "invalid";
  }
  if (score >= 60) {
    return "pass";
  }
  return "fail";
}

console.log(classify(72)); // pass
console.log(classify(-3)); // invalid

return은 값을 반환하는 동시에 함수를 멈추게 하므로, 한 분기가 실행되면 그 아래 코드는 실행되지 않습니다. 이런 조기 종료 패턴(먼저 검증하고 걸러낸 뒤, 마지막에 핵심 로직을 처리하는 방식)을 쓰면 조건문이 여러 겹으로 깊게 중첩되는 것을 피할 수 있습니다.

실제 코드에서 중요한 구분은 값을 반환하는 함수와 부수 효과(side effect)(콘솔에 출력하거나, 함수 바깥의 무언가를 변경하는 것)만 만들어내는 함수 사이의 차이입니다. console.log는 부수 효과를 일으킬 뿐 함수는 undefined로 평가되고, return은 호출한 쪽에서 조합해 쓸 수 있는 값을 만들어냅니다. return이 없는 함수, 혹은 return 뒤에 아무것도 없는 경우 모두 undefined를 내놓습니다. 로그만 남기는 함수에서 반환값을 얻으려 하는 것은 흔히 발생하는 버그이며, 대부분의 경우 해결책은 값을 return하고 출력은 호출한 쪽에서 하도록 바꾸는 것입니다.

Juno매개변수와 반환값 매개변수는 괄호 안에 있는 이름으로, 전달받은 값을 대신 나타내는 자리이기 때문에 하나의 함수로도 다양한 결과를 만들 수 있습니다. return은 값을 돌려주어 나중에 코드에서 쓸 수 있게 해줍니다. console.log로 출력하는 것은 화면에 값을 보여주는 것이고, 반환은 값을 실제로 돌려주는 것입니다. 저도 이 차이를 체감하는 데 시간이 좀 걸렸습니다.
Juno매개변수와 반환값 매개변수는 정의부에 있는 이름이고, 인자는 호출할 때 전달하는 실제 값입니다. return은 호출한 쪽에 값을 돌려주고 그 자리에서 함수를 종료합니다. 로그만 남기는 함수는 undefined로 평가되므로, 값을 기대했는데 아무것도 받지 못했다면 실제로 반환했는지 확인해보세요.
Juno매개변수와 반환값return은 값을 만들어내면서 함수를 종료하므로, 검증 후 바로 빠져나가는 패턴을 깔끔하게 구현할 수 있습니다. 값을 반환하는 것과 console.log 같은 부수 효과를 일으키는 것의 차이를 눈여겨보세요. 로그를 남기는 함수는 undefined로 평가됩니다. 결과값을 받으려 했는데 아무것도 얻지 못했다면, 거의 항상 반환해야 할 자리에 로그를 남긴 경우입니다.

화살표 함수

함수를 더 짧게 쓰는 방법이 있는데, 이를 **화살표 함수(arrow function)**라고 부릅니다. 변수에 저장하고, function 키워드 대신 => 화살표를 사용합니다:

js
const add = (a, b) => {
  return a + b;
};

console.log(add(2, 3)); // 5

이렇게 함수 본문이 하나의 표현식으로만 이루어져 있다면, 중괄호와 return을 생략해도 화살표가 알아서 값을 돌려줍니다:

js
const add = (a, b) => a + b;

console.log(add(2, 3)); // 5

둘 다 같은 동작을 합니다. 최신 JavaScript 코드에서는 화살표 함수를 어디서든 볼 수 있으므로, 그 형태를 알아보는 것이 도움이 됩니다. 이름, =, 입력값, 화살표, 그리고 본문 순서로 이루어져 있습니다.

**화살표 함수(arrow function)**는 =>를 사용해 변수에 저장하는, 함수를 작성하는 더 짧은 문법입니다:

js
const formatPrice = (amount) => `$${amount.toFixed(2)}`;

console.log(formatPrice(9.5)); // $9.50

본문이 하나의 표현식으로 이루어져 있으면 화살표 함수는 **암묵적 반환(implicit return)**을 합니다. 중괄호도, return 키워드도 없이 표현식의 값을 자동으로 돌려줍니다. 중괄호를 붙이면 일반 함수 본문처럼 동작하며, 직접 return을 작성해야 합니다:

js
const formatPrice = (amount) => {
  const rounded = amount.toFixed(2);
  return `$${rounded}`;
};

화살표 함수는 짧고 단일 목적을 가진 함수, 특히 다른 함수에 전달하는 작은 함수들(배열 메서드에서 이런 함수들을 만나게 됩니다)에 잘 어울리는 관용적인 선택지입니다. 최상위에 위치한, 재사용 가능한, 이름 있는 루틴이라면 일반 function 선언이 명확하게 읽히고 잘 동작합니다. 간결함이 도움이 되고 함수가 짧을 때 화살표 함수를 선택하세요.

**화살표 함수(arrow function)**는 =>로 작성하는 간결한 함수 표현식입니다. 본문이 단일 표현식이면 암묵적 반환이 일어나(return 키워드 없이 표현식의 값이 반환됩니다), 중괄호로 감싼 본문이면 일반 함수와 마찬가지로 명시적으로 반환해야 합니다.

js
const applyDiscount = (price, percent) => price * (1 - percent / 100);

console.log(applyDiscount(80, 25)); // 60

함수를 정의하는 세 가지 방식이 공존하며, 그 차이는 겉모습이 아니라 실질적인 동작에 있습니다:

  • 함수 선언(function declaration) (function total() {})은 호이스팅되므로, 정의보다 위에서도 호출할 수 있습니다.
  • 함수 표현식(function expression) (const total = function () {})은 같은 방식으로 호이스팅되지 않습니다. 변수 자체는 존재하지만 해당 줄이 실행되기 전까지는 undefined를 담고 있으므로, 그전에 호출하면 오류가 발생합니다.
  • 화살표 함수(arrow function) (const total = () => {})는 호이스팅 측면에서는 표현식처럼 동작하며, 여기에 중요한 규칙 하나가 더해집니다.

화살표 함수를 구별짓는 규칙은 이것입니다. 화살표 함수는 자신만의 this를 갖지 않습니다. 일반 함수의 this는 호출되는 방식에 따라 결정되지만, 화살표 함수는 대신 주변 코드에서 this를 물려받습니다. 이 차이는 객체의 메서드를 작성하기 전까지는 크게 와닿지 않으므로, 지금은 이 사실만 기억해두면 그때 가서 이유가 이해될 것입니다.

한 가지 더 알아둘 만한 도구가 있습니다. **기본 매개변수(default parameters)**는 인자가 빠졌을 때 대신 쓸 값을 지정해, 함수가 예측 가능하게 동작하도록 해줍니다:

js
const greet = (name = "there") => `Hi ${name}`;

console.log(greet("민준")); // Hi 민준
console.log(greet()); // Hi there

그리고 가장 큰 도움이 되는 습관은 이것입니다. 출력값이 오직 입력값에만 의존하고, 자기 자신 바깥의 무엇도 바꾸지 않는(부수 효과가 없는) **순수 함수(pure functions)**를 선호하세요. 위에 나온 applyDiscount는 순수 함수입니다. 같은 입력이면 언제나 같은 결과가 나옵니다. 순수 함수는 독립적으로 테스트할 수 있고, 프로그램의 나머지 부분을 일일이 추적하지 않아도 이해할 수 있으며, 예상치 못한 부작용 없이 재사용할 수 있습니다. 모든 것을 순수하게 만들 수는 없습니다(어딘가에서는 로그를 남기고, 데이터를 가져오고, 상태를 바꿔야 합니다). 하지만 로직은 순수 함수 안으로 밀어넣고 부수 효과는 가장자리에 몰아두는 것이야말로 이 언어에서 가장 효과가 큰 습관 중 하나입니다.

Juno화살표 함수 화살표 함수는 function 키워드 대신 =>를 써서 변수에 저장하는, 함수를 더 짧게 쓰는 방법입니다. 본문이 하나의 표현식이라면 중괄호와 return을 생략할 수 있고, 화살표가 알아서 값을 돌려줍니다. 이 형태는 정말 자주 보게 되므로, 익숙해지면 빠르게 도움이 됩니다.
Juno화살표 함수 화살표 함수는 함수를 짧게 쓰는 형태이고, 본문이 단일 표현식이면 암묵적 반환이 일어나서 중괄호도 return도 필요 없습니다. 중괄호를 붙이면 평소처럼 직접 return을 써야 합니다. 작고 단일 목적의 함수에는 화살표 함수를 쓰고, 최상위에 있는 이름 있는 루틴에는 일반 function 선언이 여전히 잘 읽힙니다.
Juno화살표 함수 함수 선언은 호이스팅되지만 함수 표현식과 화살표 함수는 그렇지 않고, 화살표 함수는 자신만의 this도 갖지 않는데 이는 객체 메서드를 작성할 때에야 중요해집니다. 인자가 빠졌을 때는 기본 매개변수에 기대고, 로직은 출력값이 오직 입력값에만 의존하는 순수 함수 안으로 밀어넣으세요. 그런 함수들이야말로 독립적으로 테스트할 수 있고 프로그램 전체를 추적하지 않고도 재사용할 수 있는 함수입니다.

함수, 그다음은

함수는 같은 일을 반복하지 않게 해주는 도구입니다. 일련의 단계에 이름을 붙이고, 입력값을 전달하고, 결과값을 돌려받습니다. 이 개념이 이해되고 나면, 여러분이 작성하는 코드 대부분이 각자 명확한 하나의 역할을 맡은 작은 함수들이 서로를 호출하는 형태가 됩니다.

여기서부터 함수는 어디서든 등장합니다. 배열 메서드에 함수를 전달해 데이터 목록을 변형하게 될 것이고, 조건문의 분기 로직을 정리하는 데 함수를 활용하게 될 것이며, 변수에서 익힌 이름 짓기 원칙도 계속 이어가게 될 것입니다. 다음에 코드 블록을 복사하고 있는 자신을 발견한다면, 그것이 바로 함수로 감싸야 할 신호입니다.