함수

프로그래밍에서의 함수란, 큰 프로그램을 잘게 쪼개어 특정 코드 뭉치를 반복해서 사용할 수 있도록 묶어놓은 코드 뭉치의 단위를 말합니다. 함수를 어떻게 작성하느냐에 따라서 코드의 유지보수성과 가독성이 크게 달라집니다. 또 JavaScript의 함수는 굉장히 많은 기능을 갖고 있는데, 사실 함수의 성질을 모두 이해하면 프로그래밍 언어로서의 JavaScript를 전부 이해한거나 마찬가지라고 할 수 있을 정도입니다. 본 챕터에서는 함수의 기본적인 성질만을 다루고, 이어지는 챕터들에서 나머지 내용을 차근차근 다룰 것입니다.

함수의 구성 요소

두 값을 더하는 아주 간단한 함수를 정의해 보겠습니다.

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

위에서 add라는 이름을 갖는 함수를 정의했습니다. 괄호 안의 xy매개변수(parameter)라 하며, return 뒤에 오는 값을 반환값(return value)이라고 합니다.

함수를 정의했다면, 아래와 같이 함수 이름 뒤에 괄호를 붙여서 이 함수를 실행시킬 수 있습니다. 이를 함수의 호출(function call)이라고 합니다.

add(2, 3); // 5

여기서 괄호 안에 넘겨준 2, 3인수(argument)라고 부릅니다.

실행 순서

JavaScript는 기본적으로는 쓰여진 순서대로 실행되지만, 여러 가지 예외가 존재해서 코드의 실행 순서가 이리저리 옮겨다니기도 합니다. 함수 호출도 그 중 하나로, 함수 호출 코드를 만나면 코드의 실행 흐름이 호출된 함수의 내부로 옮겨갑니다. 함수가 값을 반환하면 다시 이전 위치부터 코드의 실행이 재개됩니다.

// 1 - 함수 정의
function add(x, y) {
  return x + y; // 3 - 함수 실행
}
// 2 - 함수 호출
const result = add(2, 3);
// 4 - 나머지 코드 실행
console.log(result);

여기서 유의할 점은, 함수를 정의하는 것만으로는 함수 내부에 있는 코드가 실행되지 않는다는 것입니다. 함수 내부의 코드를 실행하려면, 반드시 함수를 호출해주어야 합니다.

매개변수와 인수

위 코드의 xy를 가지고 매개변수라고 합니다. 매개변수는 변수의 일종으로, 함수 호출 시마다 인수가 매개변수에 대입됩니다. 위의 코드에서 add(2, 3)과 같이 호출하면 매개변수 x에는 2가, y에는 3이 대입된 채로 나머지 코드가 실행됩니다.

여기서 주의할 점은 매개변수는 바깥에서 선언된 변수와는 관계없는 독립적인 변수라는 것입니다. 예를 들어, 함수 호출 시 인수가 들어갈 자리에 변수를 써주고, 함수 내부에서 매개변수에 새로운 값을 대입한다고 하더라도 인수로 써준 변수의 값이 변경되지 않습니다.

function reassign(x) {
  x = 2;
  return x;
}

const y = 1;
const result = reassign(y);

console.log(y); // 1
console.log(result); // 2

매개변수는 let으로 선언한 변수와 비슷하게 동작하지만 미묘하게 다른 점이 있습니다. 이에 대해서는 값 더 알아보기에서 자세히 다룹니다.

반환값

return 구문은 함수의 반환값을 결정합니다. return 키워드 바로 다음에 오는 값이 함수 호출의 결과값으로 반환되며, 반환되는 즉시 함수 실행이 끝납니다.

function add(x, y) {
  return x + y;
  console.log('이 부분은 실행되지 않습니다.');
}
add(1, 2); // 3
// 3 외에 따로 출력되는 내용이 없습니다.

아래와 같이 return 뒤에 아무 값도 써주지 않거나, 아예 return 구문을 쓰지 않으면 함수는 undefined를 반환합니다.

function returnUndefined() {
  return;
}
function noReturn() {}
returnUndefined(); // undefined
noReturn(); // undefined

스코프 (Scope)

함수의 매개변수를 비롯한, 모든 변수들은 특별한 성질을 갖습니다.

function add(x, y) { // 변수 `x`와 `y`가 정의됨
  return x + y;
}
add(2, 3);
console.log(x); // 에러!

이렇게, 매개변수와 같이 함수 안에서 정의된 변수는 함수 바깥에서는 접근할 수 없기 때문에 함수 안에서만 사용할 수 있습니다. 즉, 변수는 코드의 일정 범위 안에서만 유효하다는 성질이 있는 것입니다. 이렇게, 특정 변수가 유효한 코드 상의 유효 범위를 가지고 스코프(scope)라고 합니다.

위 예제에서의 xy는 함수 add의 내부 코드 안에서만 접근할 수 있습니다. 즉, 매개변수는 함수 스코프를 갖습니다.

스코프의 성질이 미묘해서, 이를 잘 이해하지 못하면 코드를 작성하거나 읽기 어려울 수 있습니다. 아래에서 스코프의 몇 가지 성질들을 살펴보겠습니다.

스코프 연쇄 (Scope Chain)

함수 내부 코드에서, 매개변수 혹은 그 함수 안에서 정의된 변수만 사용할 수 있는 것은 아닙니다.

const five = 5;
function add5(x) {
  return x + five; // 바깥 스코프의 `five` 변수에 접근
}
add5(3); // 8

add5 함수의 return 구문에서 함수 바깥에 있는 변수 five의 값을 가져와 사용했습니다. 이는 심지어 함수가 여러 겹 중첩(nested)되어 있다고 하더라도 가능합니다.

const five = 5;
function add5(x) {
  function add(y) {
    return x + y;
  }
  return add(five);
}
add5(3); // 8

코드의 실행 흐름이 식별자에 도달하면, 먼저 그 식별자와 같은 이름을 갖는 변수를 현재 스코프에서 찾아보고, 변수가 존재하면 그것을 그대로 사용합니다. 만약 현재 스코프에서 변수를 찾지 못하면 바로 바깥쪽 스코프에서 변수를 찾아봅니다. 있으면 사용하고 없으면 바깥쪽 스코프로 올라가서 다시 찾아보는, 이 과정이 되풀이됩니다. 이런 과정을 스코프 연쇄(scope chain)라 하고, 이 과정은 가장 바깥쪽에 있는 스코프를 만날 때까지 반복됩니다. 가장 바깥쪽 스코프까지 찾아봤는데도 같은 이름의 변수를 찾지 못하면, 그제서야 에러가 발생됩니다.

가장 바깥에 있는 스코프를 최상위 스코프(top-level scope) 혹은 전역 스코프(global scope)라고 부릅니다. 위 코드에서 five가 정의된 스코프가 바로 전역 스코프입니다.1

변수 가리기 (Variable Shadowing)

단일 스코프에서는 같은 이름을 갖는 서로 다른 변수가 존재할 수 없습니다. 하지만 스코프 연쇄가 일어나면 이야기가 달라집니다. 아래의 코드에서는 x라는 이름을 갖는 변수가 세 번 정의되었습니다.

const x = 3;

function add5(x) { // `x`라는 변수가 다시 정의됨
  function add(x, y) { // `x`라는 변수가 다시 정의됨
    return x + y;
  }
  return add(x, 5);
}

add5(x);

위와 같이, 바깥쪽 스코프에 존재하는 변수와 같은 이름을 같는 변수를 안쪽 스코프에서 재정의할 수 있습니다. 그렇게 되면 안쪽 스코프에서는 바깥쪽 스코프에 있는 이름이 무시됩니다. 이런 현상을 변수 가리기(variable shadowing)라고 합니다.

어휘적 스코핑 (Lexical Scoping)

스코프는 코드가 작성된 구조에 의해서 결정되는 것이지, 함수 호출의 형태에 의해 결정되는 것이 아닙니다. 예를 들어 봅시다.

function add5(x) {
  const five = 5;
  return add(x);
}

add5(3); // 8

function add(x) {
  return five + x; // ReferenceError: five is not defined
}

add라는 함수가 add5라는 함수 안에서 호출되었다고 해서, add 함수 내부에서 add5 함수의 스코프 안에 있는 변수에 접근할 수 있는 것은 아닙니다. 스코프는 코드가 작성된 구조에 의해 결정되는 성질입니다. 위 코드를 동작시키려면, 아래와 같이 작성해야 합니다.

function add5(x) {
  const five = 5;
  function add(x) {
    return five + x;
  }
  return add(x);
}

스코프의 종류

이 챕터에서는 매개변수에 대한 함수 스코프를 중점적으로 다루었는데, 사실 스코프의 종류가 더 있습니다. 특히, letconst로 선언된 변수는 함수 스코프가 아니라 조금 다른 종류의 스코프를 가집니다. 이에 대해서는 값 더 알아보기에서 자세히 다룹니다.

값으로서의 함수

JavaScript에서는 함수도 값입니다!

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

const plus = add;
plus(1, 2); // 3

다른 값과 마찬가지로, 함수를 선언한 뒤 변수에 대입해서 호출할 수도 있고, 혹은 배열이나 객체에 넣을 수도 있고, 심지어는 함수를 다른 함수에 인수로 넘기거나, 함수에서 함수를 반환할 수도 있습니다.

// 함수를 배열이나 객체에 넣기
function add(x, y) {
  return x + y;
}
[add];
{addFunc: add};

// 함수를 인수로 넘기기
function isEven(x) {
  return x % 2 === 0;
}
[1, 2, 3, 4, 5].filter(isEven); // [2, 4]

// 함수에서 함수 반환하기
function createEmptyFunc() {
  function func() {}
  return func;
}

컴퓨터 과학 분야에서 사용되는 용어 중에 1급 시민(First-Class Citizen)이라는 특이한 용어가 있습니다. 값으로 사용할 수 있는 JavaScript의 함수는 1급 시민입니다. 1급 시민인 함수를 줄여서 1급 함수라 부르기도 합니다.

JavaScript에서는 1급 함수의 성질을 여러 가지 흥미로운 방식으로 활용할 수 있습니다. 이에 대한 자세한 내용은 함수형 프로그래밍에서 다룹니다.

익명 함수 (Anonymous Function)

JavaScript에서 함수를 선언할 때 꼭 이름을 붙여주어야 하는 것은 아닙니다. 아래와 같이 이름을 붙이지 않은 함수를 가지고 익명 함수(anonymous function), 혹은 함수 리터럴(function literal)이라고 합니다.

// 두 수를 더해서 반환하는 익명 함수
function(x, y) {
  return x + y;
}
// 위의 익명 함수는 이름이 없어서 이름을 가지고 호출을 할 수 없습니다.

// 호출을 하려면 변수에 저장한 후에 변수의 이름을 통해 호출해야 합니다.
const add = function(x, y) {
  return x + y;
}
add(1, 2); // 3

익명 함수는 함수를 만든 쪽이 아니라 다른 쪽에서 그 함수를 호출할 때 많이 사용됩니다. 대표적인 경우는 함수를 인수로 넘겨줄 때입니다. 예를 들어, 배열의 filter 메소드에 필터링할 조건을 표현하는 함수를 넘겨주면, filter 메소드쪽에서 배열 각 요소에 대해 함수를 호출한 뒤, true를 반환한 요소만을 필터링해서 반환합니다.

[1, 2, 3, 4, 5].filter(function (x) {
  return x % 2 === 0;
}); // [2, 4]

화살표 함수 (Arrow Function)

함수 정의를 위한 새로운 표기법인 화살표 함수(arrow function)은 ES2015에서 도입되었습니다.

// 여기에서 x + y 는 **바로 반환됩니다.**
const add = (x, y) => x + y;
// 바로 반환시키지 않고 function 키워드를 통한 함수 정의처럼 여러 구문을 사용하려면 curly braces({...}) 로 둘러싸주어야 합니다.
// `=>` 다음 부분을 중괄호로 둘러싸면, 명시적으로 `return` 하지 않는 한 아무것도 반환되지 않습니다.
const add = (x, y) => {
  const result = x + y;
  return result;
}
// 매개변수가 하나밖에 없다면, 매개변수 부분의 괄호를 쓰지 않아도 무방합니다.
const negate = x => !x;

화살표 함수는 표기법이 간단하기 때문에 익명 함수를 다른 함수의 인수로 넘길 때 주로 사용됩니다.

[1, 2, 3, 4, 5].filter(x => x % 2 === 0);

일반적인 함수와 화살표 함수는 표기법에서만 다른 것이 아니고, 몇 가지 미묘한 차이점이 있습니다. 이에 대해서는 함수 더 알아보기 챕터에서 자세히 다룹니다.

1. 다만, JavaScript 파일의 가장 바깥쪽에서 선언된 변수가 항상 전역 스코프를 갖는 것은 아닙니다. 어떤 스코프를 가질 지는 JavaScript 파일이 사용되는 방식에 따라 달라지는데, 이에 대해서는 모듈 챕터에서 자세히 다룹니다.

results matching ""

    No results matching ""