javascript

자바스크립트 기초 - 6

Dec 18, 2019

자바스크립트 기초 - 6

목차

1. 함수
2. 함수 표현식과 화살표 함수

1. 함수

함수는 프로그램을 구성하는 주요 “구성 요소(building blocks)“입니다. 함수를 이용하면 중복 없이 유사한 동작을 하는 코드를 여러 번 호출할 수 있습니다.

1.1 함수 선언

함수 선언(function declaration) 방식을 이용하면 함수를 만들 수 있습니다.

function showMessage() {
  alert("Hello js!!");
}

function 키워드, 함수 이름, 괄호로 둘러싼 매개변수를 차례로 써주면 함수를 선언할 수 있습니다. 위 함수에는 매개변수가 없는데, 만약 매개변수가 여러 개 있다면 각 매개변수를 콤마로 구분해줍니다. 이어서 함수를 구성하는 코드의 모임인 “함수 본문(body)“을 중괄호로 감싸 붙여줍시다.

function name(parameters) {
  ...함수 본문...
}

새롭게 정의한 함수는 함수 이름 옆에 괄호를 붙여 호출할 수 있습니다. showMessage()같이 말이죠.

function showMessage() {
  alert("Hello js!!");
}

showMessage();
showMessage();

1.2 지역 변수

함수 내에서 선언한 변수(지역 변수, local variable)는 함수 안에서만 접근할 수 있습니다.

1.3 외부 변수

함수 내부에서 함수 외부의 변수(외부 변수, outer variable)에 접근할 수 있습니다.
함수 내부에서 외부의 변수에 접근하는 것뿐만 아니라, 수정도 할 수 있습니다.

let userName = 'John';

function showMessage() {
  userName = "Bob"; // (1) 외부 변수를 수정함

  let message = 'Hello, ' + userName;
  alert(message);
}

alert( userName ); // 함수 호출 전이므로 John 이 출력됨
showMessage();
alert( userName ); // 함수에 의해 Bob 으로 값이 바뀜

함수 내부에 외부 변수와 동일한 이름을 가진 변수가 선언되었다면, 내부 변수는 외부 변수를 가립니다.

1.4 매개변수

매개변수(parameter)를 이용하면 임의의 데이터를 함수 안에 전달할 수 있습니다. 매개변수는 인수(argument) 라고 불리기도 합니다(역주: 매개변수와 인수는 엄밀히 같진 않지만, 본 튜토리얼 원문을 토대로 번역하였습니다).

function showMessage(from, text) { // 인수: from, text
  alert(from + ': ' + text);
}

showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
showMessage('Ann', "What's up?"); // Ann: What's up? (**)

//===========================================================
function showMessage(from, text) {

  from = '*' + from + '*'; // "from"을 좀 더 멋지게 꾸며줍니다.

  alert( from + ': ' + text );
}

let from = "Ann";

showMessage(from, "Hello"); // *Ann*: Hello

// 함수는 복사된 값을 사용하기 때문에 바깥의 "from"은 값이 변경되지 않습니다.
alert( from ); // Ann

1.5 기본값

매개변수에 값을 전달하지 않으면 그 값은 undefined가 됩니다.

위에서 정의한 함수 showMessage(from, text)는 매개변수가 2개지만, 아래와 같이 인수를 하나만 넣어서 호출할 수 있습니다.

showMessage("Ann");

이렇게 코드를 작성해도 에러가 발생하지 않습니다. 두 번째 매개변수에 값을 전달하지 않았기 때문에 text엔 undefied가 할당될 뿐입니다. 따라서 에러 없이 “Ann: undefined”가 출력됩니다.

매개변수에 값을 전달하지 않아도 그 값이 undefined가 되지 않게 하려면 “기본값(default value)“을 설정해주면 됩니다. 매개변수 오른쪽에 =을 붙이고 undefined 대신 설정하고자 하는 기본값을 써주면 되죠.

function showMessage(from, text = "no text given") {
  alert( from + ": " + text );
}

showMessage("Ann"); // Ann: no text given

이젠 text가 값을 전달받지 못해도 undefined대신 기본값 “no text given”이 할당됩니다.

위 예시에선 문자열 “no text given”을 기본값으로 설정했습니다. 하지만 아래와 같이 복잡한 표현식도 기본값으로 설정할 수도 있습니다.

function showMessage(from, text = anotherFunction()) {
  // anotherFunction()은 text값이 없을 때만 호출됨
  // anotherFunction()의 반환 값이 text의 값이 됨
}

매개변수 기본값 평가 시점
자바스크립트에선 함수를 호출할 때마다 매개변수 기본값을 평가합니다. 물론 해당하는 매개변수가 없을 때만 기본값을 평가하죠.

위 예제에선 매개변수 text에 값이 없는 경우 showMessage()를 호출할 때마다 anotherFunction()이 호출됩니다.

과거 방식으로 기본값 설정하기
지금까지 소개한 매개변수 기본값 설정 방법은 최근에 도입된 문법입니다. 과거엔 많은 개발자가 아래와 같은 방법을 사용해 기본값을 지정해주었습니다. 오래된 코드에서 종종 살펴볼 수 있는 방법이죠. 아래는 undefined를 명시적으로 확인하는 방법을 사용한 방법입니다.

function showMessage(from, text) {
  if (text === undefined) {
    text = 'no text given';
  }

  alert( from + ": " + text );
}

// || 연산자를 사용하는 방법도 있습니다.
function showMessage(from, text) {
  // text가 falsy일 때 "기본값"을 할당합니다.
  text = text || 'no text given';
  ...
}

1.6 반환 값

함수를 호출했을 때 함수를 호출한 그곳에 특정 값을 반환하게 할 수 있습니다. 이때 이 특정 값을 반환 값(return value)이라고 부릅니다.

function sum(a, b) {
  return a + b;
}

let result = sum(1, 2);
alert( result ); // 3

지시자 return은 함수 내 어디서든 사용할 수 있습니다. 실행 흐름이 지시자 return을 만나면 함수 실행은 즉시 중단되고 함수를 호출한 곳에 값을 반환합니다.

1.6.1 return문이 없거나 return 지시자만 있는 함수는 undefined를 반환합니다.
return문이 없는 함수도 무언가를 반환합니다. undefined를 반환하죠.

function doNothing() { /* empty */ }
alert( doNothing() === undefined ); // true

return 지시자만 있는 경우도 undefined를 반환합니다. return은 return undefined와 동일하게 동작하죠.

function doNothing() {
  return;
}

alert( doNothing() === undefined ); // true

1.6.2 return과 값 사이에 절대 줄을 삽입하지 마세요.
반환하려는 값이 긴 표현식인 경우, 아래와 같이 지시자 return과 반환하려는 값 사이에 새 줄을 넣어 코드를 작성하고 싶을 수도 있습니다.

return
 (some + long + expression + or + whatever * f(a) + f(b))

자바스크립트는 return문 끝에 세미콜론을 자동으로 넣기 때문에 이렇게 return문을 작성하면 안 됩니다. 위와 코드는 아래 코드처럼 동작합니다.

return;
 (some + long + expression + or + whatever * f(a) + f(b))

따라서 반환하고자 했던 표현식을 반환하지 못하고 아무것도 반환하지 않는 것처럼 되어버립니다.

표현식을 여러 줄에 걸쳐 작성하고 싶다면 표현식이 return 지시자가 있는 줄에서 시작하도록 작성해야 합니다. 또는 아래와 같이 여는 괄호를 return 지시자와 같은 줄에 써줘도 괜찮습니다.

return (
  some + long + expression
  + or +
  whatever * f(a) + f(b)
  )

1.7 함수 이름짓기

1.7.1 함수는 동작 하나만 담당해야 합니다.

1.8 함수 == 주석

함수는 간결하고, 한 가지 기능만 수행할 수 있게 만들어야 합니다. 함수가 길어지면 함수를 잘게 쪼갤 때가 되었다는 신호로 받아들이셔야 합니다. 함수를 쪼개는 건 쉬운 작업은 아닙니다. 하지만 함수를 분리해 작성하면 많은 장점이 있기 때문에 함수가 길어질 경우엔 함수를 분리해 작성할 것을 권유합니다.

함수를 간결하게 만들면 테스트와 디버깅이 쉬워집니다. 그리고 함수 그 자체로 주석의 역할까지 합니다!

같은 동작을 하는 함수, showPrimes(n)를 두 개 만들어 비교해 봅시다. showPrimes(n)은 n까지의 소수(prime numbers)를 출력해줍니다.

첫 번째 showPrimes(n)에선 레이블을 사용해 반복문을 작성해보았습니다.

function showPrimes(n) {
  nextPrime: for (let i = 2; i < n; i++) {

    for (let j = 2; j < i; j++) {
      if (i % j == 0) continue nextPrime;
    }

    alert( i ); // 소수
  }
}

두 번째 showPrimes(n)는 소수인지 아닌지 여부를 검증하는 코드를 따로 분리해 isPrime(n)이라는 함수에 넣어서 작성했습니다.

function showPrimes(n) {

  for (let i = 2; i < n; i++) {
    if (!isPrime(i)) continue;

    alert(i);  // a prime
  }
}

function isPrime(n) {
  for (let i = 2; i < n; i++) {
    if ( n % i == 0) return false;
  }
  return true;
}

두 번째 showPrimes(n)가 더 이해하기 쉽지 않나요? isPrime 함수 이름을 보고 해당 함수가 소수 여부를 검증하는 동작을 한다는 걸 쉽게 알 수 있습니다. 이렇게 이름만 보고도 어떤 동작을 하는지 알 수 있는 코드를 자기 설명적(self-describing) 코드라고 부릅니다.

1.9 함수 요약

깔끔하고 이해하기 쉬운 코드를 작성하려면 함수 내부에서 외부 변수를 사용하는 방법 대신 지역 변수와 매개변수를 활용하는 게 좋습니다.

개발자는 매개변수를 받아서 그 변수를 가지고 반환 값을 만들어 내는 함수를 더 쉽게 이해할 수 있습니다. 매개변수 없이 함수 내부에서 외부 변수를 수정해 반환 값을 만들어 내는 함수는 쉽게 이해하기 힘듭니다.

함수 이름을 지을 땐 아래와 같은 규칙을 따르는 것이 좋습니다.

함수는 스크립트를 구성하는 주요 구성 요소입니다. 지금까진 다룬 내용은 함수의 기초입니다.

2. 함수 표현식과 화살표 함수

자바스크립트는 함수를 특별한 종류의 값으로 취급합니다. 다른 언어에서처럼 “특별한 동작을 하는 구조”로 취급되지 않습니다.

함수 선언 방식 외에 함수 표현식(Function Expression) 을 사용해서 함수를 만들 수 있습니다.

let sayHi = function() {
  alert( "Hello" );
};

함수를 생성하고 변수에 값을 할당하는 것처럼 함수가 변수에 할당되었습니다. 함수가 어떤 방식으로 만들어졌는지에 관계없이 함수는 값이고, 따라서 변수에 할당할 수 있습니다. 위 예시에선 함수가 변수 sayHi에 저장된 값이 되었습니다.

alert( sayHi ); // 함수 코드가 보임

sayHi옆에 괄호가 없기 때문에 함수는 실행되지 않습니다. 어떤 언어에선 괄호 없이 함수 이름만 언급해도 함수가 실행됩니다. 하지만 자바스크립트는 괄호가 있어야만 함수가 호출됩니다.

자바스크립트에서 함수는 값입니다. 따라서 함수를 값처럼 취급할 수 있습니다. 위 코드에선 함수 소스 코드가 문자형으로 바뀌어 출력되었습니다.

함수는 sayHi()처럼 호출할 수 있다는 점 때문에 일반적인 값과는 조금 다릅니다. 특별한 종류의 값이죠.

하지만 그 본질은 값이기 때문에 값에 할 수 있는 일을 함수에도 할 수 있습니다.

변수를 복사해 다른 변수에 할당하는 것처럼 함수를 복사해 다른 변수에 할당할 수도 있습니다.

function sayHi() {   // (1) 함수 생성
  alert( "Hello" );
}

let func = sayHi;    // (2) 함수 복사 sayHi 다음에 괄호가 없다는 점에 유의

func(); // Hello     // (3) 복사한 함수를 실행(정상적으로 실행됩니다)!
sayHi(); // Hello    //     본래 함수도 정상적으로 실행됩니다.

함수 sayHi는 아래와 같이 함수 표현식을 사용해 정의할 수 있습니다.

let sayHi = function() {
  alert( "Hello" );
};

let func = sayHi;
// ...

함수 표현식의 끝에 세미콜론이 있는 이유는 값처럼 취급되기 때문이다.

2.1 콜백 함수

function ask(question, yes, no) {
  if (confirm(question)) yes()
  else no();
}

function showOk() {
  alert( "동의하셨습니다." );
}

function showCancel() {
  alert( "취소 버튼을 누르셨습니다." );
}

// 사용법: 함수 showOk와 showCancel가 ask 함수의 인수로 전달됨
ask("동의하십니까?", showOk, showCancel);

함수 ask의 인수, showOk와 showCancel은 콜백 함수 또는 콜백이라고 불립니다.

함수를 함수의 인수로 전달하고, 필요하다면 인수로 전달한 그 함수를 “나중에 호출(called back)“하는 것이 콜백 함수의 개념입니다. 위 예시에선 사용자가 “yes”라고 대답한 경우 showOk가 콜백이 되고, “no”라고 대답한 경우 showCancel가 콜백이 됩니다.

함수 표현식을 사용하면 코드 길이가 짧아집니다.

function ask(question, yes, no) {
  if (confirm(question)) yes()
  else no();
}

ask(
  "동의하십니까?",
  function() { alert("동의하셨습니다."); },
  function() { alert("취소 버튼을 누르셨습니다."); }
);

ask(…) 안에 함수가 선언된 게 보이시나요? 이렇게 이름 없이 선언한 함수는 익명 함수(anonymous function) 라고 부릅니다. 익명 함수는 (변수에 할당된 게 아니기 때문에) ask 바깥에선 접근할 수 없습니다. 위 예시는 의도를 가지고 이렇게 구현하였기 때문에 바깥에서 접근할 수 없어도 문제가 없습니다.

자바스크립트를 사용하다 보면 콜백을 활용한 코드를 아주 자연스레 만나게 됩니다. 이런 코드는 자바스크립트의 정신을 대변합니다.

2.1.1 함수는 “동작”을 나타내는 값입니다.

2.2 함수 표현식 vs 함수 선언문

이게 가능한 이유는 자바스크립트의 내부 알고리즘 때문입니다. 자바스크립트는 스크립트를 실행하기 전, 준비단계에서 전역에 선언된 함수 선언문을 찾고, 해당 함수를 생성합니다. 스크립트가 진짜 실행되기 전 “초기화 단계”에서 함수 선언 방식으로 정의한 함수가 생성되는 것이죠.

스크립트는 함수 선언문이 모두 처리된 이후에서야 실행됩니다. 따라서 스크립트 어디서든 함수 선언문으로 선언한 함수에 접근할 수 있는 것입니다.

// 함수 선언문
sayHi("John"); // Hello, John

function sayHi(name) {
  alert( `Hello, ${name}` );
}

// 함수 표현식
sayHi("John"); // error!

let sayHi = function(name) {  // (*) 마술은 일어나지 않습니다.
  alert( `Hello, ${name}` );
};

엄격 모드에서 함수 선언문이 코드 블록 내에 위치하면 해당 함수는 블록 내 어디서든 접근할 수 있습니다. 하지만 블록 밖에서는 함수에 접근하지 못합니다.

예시

let age = prompt("나이를 알려주세요.", 18);

// 조건에 따라 함수를 선언함
if (age < 18) {

  function welcome() {
    alert("안녕!");
  }

} else {

  function welcome() {
    alert("안녕하세요!");
  }

}

// 함수를 나중에 호출합니다.
welcome(); // Error: welcome is not defined

함수 선언문은 함수가 선언된 코드 블록 안에서만 유효하기 때문에 이런 에러가 발생합니다.

let age = 16; // 16을 저장했다 가정합시다.

if (age < 18) {
  welcome();               // \   (실행)
                           //  |
  function welcome() {     //  |
    alert("안녕!");        //  |  함수 선언문은 함수가 선언된 블록 내
  }                        //  |  어디에서든 유효합니다
                           //  |
  welcome();               // /   (실행)

} else {

  function welcome() {
    alert("안녕하세요!");
  }
}

// 여기는 중괄호 밖이기 때문에
// 중괄호 안에서 선언한 함수 선언문은 호출할 수 없습니다.

welcome(); // Error: welcome is not defined

그럼 if문 밖에서 welcome 함수를 호출할 방법은 없는 걸까요?

함수 표현식을 사용하면 가능합니다. if문 밖에 선언한 변수 welcome에 함수 표현식으로 만든 함수를 할당하면 되죠.

이제 코드가 의도한 대로 동작합니다.

let age = prompt("나이를 알려주세요.", 18);

let welcome;

if (age < 18) {

  welcome = function() {
    alert("안녕!");
  };

} else {

  welcome = function() {
    alert("안녕하세요!");
  };

}

welcome(); // 제대로 동작합니다.


//// 물음표 연산자로 간략히
let age = prompt("나이를 알려주세요.", 18);

let welcome = (age < 18) ?
  function() { alert("안녕!"); } :
  function() { alert("안녕하세요!"); };

welcome(); // 제대로 동작합니다.

2.2.1 함수 선언문과 함수 표현식 중 무엇을 선택해야 하나요?
제 경험에 따르면 함수 선언문을 이용해 함수를 선언하는 걸 먼저 고려하는 게 좋습니다. 함수 선언문으로 함수를 정의하면, 함수가 선언되기 전에 호출할 수 있어서 코드 구성을 좀 더 자유롭게 할 수 있습니다.

함수 선언문을 사용하면 가독성도 좋아집니다. 코드에서 let f = function(…) {…}보다 function f(…) {…} 을 찾는 게 더 쉽죠. 함수 선언 방식이 더 “눈길을 사로잡습니다”.

그러나 어떤 이유로 함수 선언 방식이 적합하지 않거나, (위 예제와 같이) 조건에 따라 함수를 선언해야 한다면 함수 표현식을 사용해야 합니다.

2.3 화살표 함수

화살표 함수(arrow function)를 사용하면 단순하고 간결한 문법으로 함수를 만들 수 있습니다. 화살표 함수라는 이름은 문법의 생김새를 차용해 지어졌습니다.

let func = (arg1, arg2, ...argN) => expression

화살표 함수를 이용해 작성한 위 코드는 아래 코드와 (거의) 동일하게 동작합니다.

let func = function(arg1, arg2, ...argN) {
  return expression;
};

다만, 화살표 함수를 이용한 방법이 좀 더 간결하죠.

예시 하나 더.

let sum = (a, b) => a + b;

/* 위 화살표 함수는 아래 함수의 축약 버전입니다.

let sum = function(a, b) {
  return a + b;
};
*/

alert( sum(1, 2) ); // 3

인수가 하나밖에 없다면 인수를 감싸는 괄호를 생략할 수 있습니다. 괄호를 생략하면 코드 길이는 더 짧아지죠.

// 아래 두 함수는 같습니다
// let double = function(n) { return n * 2 }
let double = n => n * 2;

alert( double(3) ); // 6

인수가 하나도 없다면, 괄호를 비워놓으면 됩니다. 다만, 괄호는 생략할 수 없습니다.

let sayHi = () => alert("안녕!");
sayHi();

2.3.1 여러 줄의 본문으로 구성된 화살표 함수

let sum = (a, b) => {  // 중괄호는 본문 여러 줄로 구성되어 있음을 알려줍니다.
  let result = a + b;
  return result; // 중괄호를 사용했다면, return 지시자로 결괏값을 반환해주어야 합니다.
};

alert( sum(1, 2) ); // 3

2.4 요약

함수를 선언해야 한다면 함수가 선언되기 이전에도 함수를 활용할 수 있기 때문에, 함수 선언문 방식을 따르는 게 좋습니다. 함수 선언 방식은 코드를 유연하게 구성할 수 있도록 해주고, 가독성도 좋습니다.

함수 표현식은 함수 선언문을 사용하는게 부적절할 때에 사용하는 것이 좋습니다. 이번 챕터에서 함수 선언문을 사용해야만 하는 경우를 몇 가지 알아보았는데, 튜토리얼 뒤쪽에서 좀 더 깊게 해당 사례를 살펴보도록 하겠습니다.

화살표 함수는 본문이 한 줄인 함수를 작성할 때 유용합니다. 만약 본문이 한 줄이 아니라면 다른 방법으로 화살표 함수를 작성해야 합니다.