7. 함수

7-1. 함수 소개

7-1-1. 함수의 정의와 역할

자바스크립트에서 함수(function)는 일련의 절차를 코드 블록으로 감싸서 재사용 가능한 하나의 코드 단위로 정의한 것이다. 함수를 정의하면 특정 작업을 수행하는 코드를 그룹화하고, 필요할 때 호출하여 코드를 실행할 수 있다.
함수는 function 키워드를 사용하여 정의하고, 이때 다른 함수와 구분하기 위해 사용되는 식별자인 함수의 이름, 함수 내부로 전달되는 변수인 매개변수(parameter), 출력되는 값인 반환 값(return value)으로 구성된다.
예를 들어, 다음은 입력받은 정수 num의 제곱 값을 반환하는 간단한 함수이다.
function square(num) { return num * num; } square(5); // 25
위 코드에서 함수의 이름은 square이고, 외부에서 함수 내부로 전달되는 매개변수는 num, 반환 값은 num * num이 평가되는 값이다.
그렇다면 함수는 왜 사용하고, 함수를 사용했을 때의 장점은 무엇일까? 입력받은 두 수를 더하는 코드를 작성한다고 가정해 보자.
const num1 = 2; const num2 = 3; const sum = num1 + num2; console.log('덧셈 결과: ' + sum); // 덧셈 결과: 5
위 예제 코드에서는 두 수를 더한 값을 콘솔에 출력한다. 하지만 이 코드는 특정한 두 개의 숫자에만 동작하며, 이후 다른 숫자에 대해 동일한 작업을 수행하려면 비슷한 코드를 다시 작성해야 하는 번거로움이 있다.
function addNumbers(num1, num2) { return num1 + num2; } const sum = addNumbers(2, 3); console.log('덧셈 결과: ' + sum); // 덧셈 결과: 5
위의 코드에서는 두 수를 더하는 작업을 함수로 정의하고, 함수를 호출하여 수행한다. 이렇게 하면 두 수를 다루는 작업을 함수로 그룹화하고, 이후에 다른 숫자에 대해 동일한 작업을 수행하려면 같은 함수를 호출하면 된다.
이처럼 함수를 사용하면 코드의 재사용성과 가독성이 증가하며, 오류를 줄이고 유지보수를 용이하게 만든다. 함수를 적절하게 활용하면 코드를 보다 모듈화하고 관리하기 쉽게 작성할 수 있다.
 

7-1-2. 함수의 선언과 호출

함수를 선언하고 호출하는 일반적인 방법은 다음과 같다.
// 함수의 선언(정의) function functionName(parameter) { // 실행될 코드 parameter = parameter + ' World!'; // 반환 값 return parameter; } // 함수의 호출 functionName('Hello'); // Hello World!
함수는 function 키워드와 함께 함수의 이름을 정의하여 선언할 수 있다. 함수 이름 뒤에 소괄호를 사용하여 0개 이상의 매개변수를 정의할 수 있다.
매개변수의 개수에는 제한이 없지만 일반적으로 3개 이하로 제한하는 것이 좋다. 너무 많은 매개변수를 지정하게 되면 코드를 이해하고 테스트하는데 어려움이 생길 수 있다. 더 많은 매개변수가 필요하다면, 객체나 배열을 사용하여 여러 데이터를 그룹화하여 사용할 수도 있다.
// 사용자 정보를 객체로 그룹화하여 전달하는 함수 function greetUser(userInfo) { console.log('Hello, ' + userInfo.firstName + userInfo.lastName + '!'); } // 사용자 정보 객체 생성 const user = { firstName: "John", lastName: "Doe" }; // 함수 호출 greetUser(user); // Hello, John Doe!
매개변수(parameter)는 인수(argument)와 구분되는 개념이다. 매개변수는 함수를 정의할 때 함수에 전달되는 값을 나타내는 변수이고, 함수 내부에서 지역변수로 취급되어 사용할 수 있다. 인수는 함수를 호출할 때 함수에 전달하는 실제 값이다. 즉, 매개변수는 함수 정의 시 함수가 어떤 데이터를 받아야 하는지를 나타내는 변수이고, 인수는 함수를 호출할 때 실제로 함수에 전달되는 데이터이다.
// name이라는 이름의 매개변수를 정의한다. function sayHi(name) { console.log(`Hi! I'm ${name}.`); } // 'John'이라는 문자열을 갖는 인수를 name 매개변수에 전달한다. sayHi('John'); // Hi! I'm John.
함수를 호출하는 것은 함수 내부의 코드 블록을 실행하는 것을 의미한다. 함수는 다음과 같이 호출할 수 있다.
greetUser(user); // Hello, John Doe!
함수 호출 시 함수의 이름 뒤에 소괄호를 사용하며, 필요할 경우 매개변수를 사용할 수 있다.
함수는 결괏값을 반환할 수 있는데, 이때 반환 값은 return 키워드를 사용하여 지정하며, 함수 호출을 통해 해당 값을 받을 수 있다. 명시적으로 return 키워드를 지정하지 않으면 undefined를 반환한다.
function addNumbers(num1, num2) { return num1 + num2; } console.log('덧셈 결과: ' + addNumbers(2, 3)); // 덧셈 결과: 5
 

7-1-3. 기본값 매개변수와 나머지 매개변수

ES6에서 추가된 기본값 매개변수(default function parameter)를 이용하여 함수 매개변수의 기본값을 지정할 수 있다. 값이 전달되지 않거나 undefined인 경우 지정된 매개변수를 기본값으로 초기화할 수 있다.
function addNumbers(num1, num2 = 0) { return num1 + num2; } console.log('덧셈 결과: ' + addNumbers(2, 3)); // 덧셈 결과: 5 console.log('덧셈 결과: ' + addNumbers(2)); // 덧셈 결과: 2
addNumbers 함수의 num2 매개변수는 인수로 값을 전달하지 않으면 기본값 0이 할당된다.
ES6에서 추가된 나머지 매개변수(rest parameter)는 정해지지 않은 여러 개의 매개변수의 값을 배열로 전달받을 수 있다.
function addNumbers(...nums) { let sum = 0; // for...of 문을 통해 배열 내의 요소들을 반복 순회 for (const num of nums) { sum += num; } return sum; } console.log(addNumbers(1, 2, 3)); // 6 console.log(addNumbers(1, 2, 3, 4, 5)); // 15
매개변수 앞에 …으로 시작하는 nums가 나머지 매개변수이다. 즉, 인수로 전달된 숫자 값은 nums에 배열 형태로 전달된다.
 

7-2. 함수를 정의하는 방법

함수를 정의하는 방법에는 함수 선언문, 함수 표현식, 화살표 함수 등이 있다.

7-2-1. 함수 선언문

함수 선언문(Function Declaration)은 function 키워드와 함께 함수의 이름과 매개변수, 함수의 본문을 사용하여 함수를 정의하는 방법이다.
function func(parameter) { // 함수의 본문 (실행할 코드) }
함수 선언문은 자바스크립트에서 가장 일반적이고 기본적인 함수 정의 방법이다.
 

7-2-2. 함수 표현식

함수 표현식(Function Expression)은 함수를 값으로 할당하여 표현식(expression) 형태로 정의하는 방법이다.
// 함수 선언문 function func(parameter) { // 함수의 본문 (실행할 코드) } // 함수 표현식 const func = function func(parameter) { // 함수의 본문 (실행할 코드) }
이때 함수의 이름은 생략할 수 있는데, 이처럼 함수의 이름이 생략된 함수를 익명 함수(anonymous function)라고 한다. 일반적으로 함수 표현식으로 정의된 함수는 익명 함수로 표현한다.
// 기명 함수 표현식 (named function expression) const func = function func(parameter) { } // 익명 함수 표현식 (anonymous function expression) const func = function(parameter) { }
함수 표현식으로 정의된 함수는 반드시 함수를 할당한 식별자의 이름으로 호출해야 한다. 함수의 이름은 함수 내부에서만 사용할 수 있고, 함수 외부에서는 참조할 수 없다.
const sayHi = function sayHello() { console.log('안녕하세요!'); } sayHi(); // 안녕하세요! sayHello(); // Uncaught ReferenceError: sayHello is not defined
 

7-2-3. 화살표 함수

화살표 함수(Arrow Function)는 ES6에서 새로 도입된 자바스크립트의 새로운 함수 정의 방식이다. 화살표 함수는 기존의 함수 정의 방식을 간결하게 표현하고, 정적으로 this를 바인딩하는 등 일반적인 함수와 다른 성격을 가지고 있다.
화살표 함수는 함수를 정의할 때 function 키워드와 함수의 이름을 생략하고, 화살표(fat arrow, ⇒)를 사용하여 표현한다. 화살표 함수는 함수 정의 시 함수 이름을 지정할 수 없기 때문에 익명 함수로만 사용할 수 있다.
// 일반 함수 표현식 const add = function(x, y) { return x + y; }; // 화살표 함수 const add = (x, y) => { return x + y; }
함수의 매개변수가 한 개일 경우, 소괄호를 생략할 수 있다.
const add = x => { return x; }
함수의 본문이 한 줄의 표현식만 반환한다면 중괄호와 return 키워드를 생략할 수 있다.
const add = x => x;
화살표 함수는 자체적인 this를 생성하지 않고, 언제나 자신의 상위 스코프의 this를 참조한다. 이 때문에 화살표 함수는 주로 콜백 함수를 정의할 때 유용하게 사용된다. this와 스코프에 대해서는 이후 8장 스코프와 12장 실행 컨텍스트에서 다시 살펴보겠다.
 

7-2-4. 함수 호이스팅

앞서 9장 호이스팅에서 살펴봤듯이 변수 선언문이 마치 해당 스코프의 최상단에 올려진 것처럼 동작하는 것을 변수 호이스팅(variable hoisting)이라고 한다. 이와 마찬가지로 함수 역시 런타임 이전에 함수 호이스팅이 일어난다.
sayHello(); // Hello! function sayHello() { console.log("Hello!"); }
위 예제 코드와 같이 함수 선언문으로 정의된 함수는 선언문 이전에 참조하고 호출할 수 있다. 자바스크립트 엔진은 코드를 실행하기 이전에 변수와 함수의 선언을 먼저 메모리에 올려놓는다. 함수 선언문은 변수 선언문과 달리 자체적인 식별자를 갖지 않지만, 함수를 호출하기 위해 함수의 이름과 같은 식별자를 자체적으로 생성하고, 해당 함수 객체를 식별자에 할당한다. 다시 말해서, 함수가 실행되려는 시점에는 이미 함수의 이름과 동일한 식별자가 생성되고 함수 객체가 할당된 상태이기 때문에 함수 선언문 이전에도 호출이 가능하다. 이와 같이 함수 선언문이 마치 해당 스코프의 최상단에 올려진 것처럼 동작하는 것을 함수 호이스팅(function hoisting)이라고 한다.
같은 함수 객체를 정의하지만 함수 선언문과 함수 표현식에는 약간의 차이가 있다.
console.log(multiply); // undefined multiply(2, 3); // TypeError: multiply is not a function var multiply = function (x, y) { return x * y; }
위 예제 코드에서는 함수 표현식을 사용하여 함수를 정의하고 있다. 식별자에 할당되는 값은 함수 객체이지만 함수 표현식은 근본적으로 함수 객체 리터럴이 할당되는 변수 할당문이기 때문에 함수 호이스팅이 아닌 변수 호이스팅이 일어나게 된다. 따라서 변수 선언문 이전에 변수를 참조하게 되면 undefined가 평가되고, 함수 호출 시 타입 에러가 발생하게 된다.
이처럼 함수 정의 방식에 따라 참조와 호출의 범위에 차이가 있기 때문에 함수 생성 원리에 대해 명확하게 이해하고 사용하는 것이 중요하다.
 

7-3. 여러 가지 함수

7-3-1. 즉시 실행 함수

함수의 정의와 동시에 실행되는 함수를 즉시 실행 함수(IIFE, Immediately Invoked Function Expression)라고 한다.
(function () { const a = 1; const b = 2; return a + b; }());
즉시 실행 함수는 기명 함수와 익명 함수로 모두 사용할 수 있지만, 함수 외부에서 다시 호출할 수 없고 단 한 번만 호출이 가능하기 때문에 일반적으로 익명 함수를 사용한다.
(function func() { const a = 1; const b = 2; return a + b; }()); func(); // ReferenceError: func is not defined
즉시 실행 함수는 스코프의 보호와 전역 변수 충돌 방지 목적으로 주로 사용된다.
(function func() { const secret = "hidden value"; console.log(secret); // hidden value }()); console.log(secret); // ReferenceError: secret is not defined
위 예제 코드에서는 secret 변수는 func 함수 내부에서만 유효하기 때문에 외부에서는 접근이 불가능하다. 내부 변수는 함수 외부에 노출되지 않고 안전하게 보호된다. 또한 다른 스크립트 또는 라이브러리에서도 동일한 이름의 전역 변수가 사용되더라도 즉시 실행 함수를 통해 변수가 캡슐화되기 때문에 전역 변수 충돌을 방지할 수 있다.
 

7-3-2. 재귀 함수

재귀 함수(recursive function)는 자기 자신을 호출하는 함수를 의미한다.
다음 팩토리얼(factorial)을 재귀 함수를 구현한 코드이다.
function factorial(n) { // 종료 조건: n이 1 이하일 때 1을 반환 if (n <= 1) { return 1; } // 재귀 호출: n * (n - 1)의 팩토리얼을 계산 return n * factorial(n - 1); } console.log(factorial(0)); // 0! = 1 console.log(factorial(1)); // 1! = 1 console.log(factorial(2)); // 2! = 2 console.log(factorial(3)); // 3! = 6
재귀 함수는 문제를 더 작은 문제로 분할하고, 반복적인 패턴을 간단하게 표현할 때 유용하지만, 무한 루프로 인한 스택 오버플로우(stack overflow) 에러와 같은 문제가 발생할 수 있다. 따라서 재귀 함수를 사용할 때는 함수의 종료 조건을 반드시 지정해야 한다. 또한 반복문을 사용하는 것이 재귀 함수를 사용하는 것보다 성능 면에서 효율적인 경우도 있으므로, 주어진 문제의 특성과 상황을 고려하여 선택해야 한다.
 

7-3-3. 콜백 함수

다른 함수에 매개변수로 전달되어 이후에 실행되는 함수를 콜백 함수(callback function)라고 한다. 콜백 함수를 사용하면 함수가 매개변수로 전달받은 함수의 호출 시점을 지정하여 호출할 수 있다.
function sayHi(name, callback) { console.log('Hi!'); callback(); } function greet() { console.log('Nice to meet you.'); } sayHi('John', greet); // Hi! // Nice to meet you.
다음과 같이 익명 함수 리터럴을 매개변수로 전달하여 콜백 함수를 정의할 수도 있다.
function sayHi(name, callback) { console.log('Hi!'); callback(); } sayHi('John', function () { console.log('Nice to meet you.'); }); // Hi! // Nice to meet you.
배열을 다루는 다양한 고차 함수에서도 콜백 함수가 사용된다.
const arr = [1, 2, 3, 4, 5]; const newArr = arr.filter(function (item) { return item % 2; }); console.log(newArr); // [1, 3, 5]
콜백 함수의 장점은 비동기 작업을 처리할 때 코드의 효율성을 향상시킬 수 있다는 것이다. 하지만 콜백 함수를 연속적으로 사용하면 콜백 지옥(callback hell)과 같은 코드 가독성 문제가 발생할 수 있으므로 이를 해결하기 위해 Promise, async/await 등의 패턴을 사용하기도 한다. 이에 대해서는 이후 18장 비동기에서 다시 살펴보겠다.