본문 바로가기

선택적 매개변수와 기본 매개변수

인수를 생략했을 때와 undefined, null, 빈 문자열을 넘겼을 때 선택적 매개변수와 기본 매개변수에 무엇이 들어가는지 계산하고, 매개변수의 순서 규칙과 선택 인수가 많을 때의 옵션 객체를 다룹니다.

앞 글에서 매개변수 뒤에 ?를 붙이거나 기본값을 적으면 인수를 생략할 수 있다는 것을 보았습니다. 그런데 "생략한다"는 말에는 여러 경우가 섞여 있습니다. 인수를 아예 넘기지 않는 것, undefined를 넘기는 것, null이나 빈 문자열을 넘기는 것은 자바스크립트에서 서로 다르게 동작합니다.

이 글은 이 경우들을 하나씩 계산해 보고, 매개변수를 어떤 순서로 두어야 하는지와 선택할 인수가 많을 때 옵션 객체로 바꾸는 방법을 봅니다. 예제는 ts-first 프로젝트에 parameters.ts를 만들고 include를 그 파일로 바꾸어 확인하며, strictNullChecks가 켜진 기본 설정을 기준으로 합니다.

optional은 본문에서 undefined 가능성을 남깁니다. default는 undefined만 대체하고 null·빈 문자열·0·false는 보존합니다. 기본값 앞에 필수 인수가 있으면 순서를 지켜 호출합니다.

TYPE / RUNTIME / CONTRACT

기본값은 undefined일 때만 적용됩니다

생략·undefined·null·falsy는 서로 다른 입력입니다. 아래는 strictNullChecks를 켠 타입 검사와 JavaScript 기본값 동작을 함께 비교한 것입니다.

OPTIONAL

생략해도 초기값이 생기지는 않습니다

email?: string의 값은 string | undefined입니다. summarizeUser의 if (email)은 빈 문자열도 제외합니다.

DEFAULT

생략 또는 명시적 undefined

logWithPrefix(message)와 두 번째 인수에 undefined를 전달한 호출은 모두 [INFO]를 사용합니다.

NULL / FALSY

허용 타입과 기본값 동작은 별개

string 매개변수에 null을 전달하려면 허용 타입이 필요합니다. null과 빈 문자열은 기본값으로 바뀌지 않습니다. 0·false도 각각 타입이 허용할 때 그대로 유지됩니다.

ORDER

뒤의 필수 인수는 계속 필요합니다

createGreeting(undefined, "Have a great day!")는 Hello, Unknown!으로 시작합니다. 선택 매개변수 뒤 필수 매개변수는 오류지만 기본값 뒤 필수 매개변수는 가능합니다.

선택 인수가 많다면 options 객체를 검토할 수 있습니다. 기본값 자체가 입력 검증을 수행하지는 않습니다.

선택적 매개변수

선택적 매개변수는 인수가 있어도 되고 없어도 되는 자리입니다. 인수를 넘기지 않으면 그 매개변수에는 undefined가 들어가므로, 본문에서 email?: string의 타입은 string | undefined입니다.

function summarizeUser(name: string, email?: string, phone?: string): string {
  let summary = `이름: ${name}`;
  if (email) {
    summary += `, 이메일: ${email}`;
  }
  if (phone) {
    summary += `, 전화번호: ${phone}`;
  }
  return summary;
}

console.log(summarizeUser("홍길동"));
console.log(summarizeUser("김철수", "[email protected]"));
console.log(summarizeUser("이영희", "[email protected]", "010-0000-0000"));
이름: 홍길동
이름: 김철수, 이메일: [email protected]
이름: 이영희, 이메일: [email protected], 전화번호: 010-0000-0000

summarizeUser는 이름 하나만 반드시 받고 나머지 두 개는 선택이므로, 인수 없이 summarizeUser()를 부르면 Expected 1-3 arguments, but got 0.이라는 TS2554가 나옵니다. 본문의 if (email)은 값이 참으로 취급되는지를 보는 검사여서, undefined뿐 아니라 빈 문자열 ""도 걸러 냅니다. 빈 문자열을 "주어졌지만 비어 있는 값"으로 따로 다루려면 if (email !== undefined)처럼 생략된 경우만 골라내는 검사를 씁니다.

인수는 자리의 순서대로 매개변수에 들어갑니다. 그래서 이메일 없이 전화번호만 넘기려면, summarizeUser("윤지", undefined, "010-0000-0000")처럼 이메일의 자리를 undefined로 채워야 합니다. 두 번째 인수에 전화번호를 바로 쓰면 그 값은 email에 들어가 이메일로 출력됩니다.

선택적 매개변수는 필수 매개변수보다 뒤에 두어야 합니다. function invalidOrder(first?: string, second: string)처럼 앞에 두면 A required parameter cannot follow an optional parameter.라는 TS1016이 나옵니다. 앞의 인수만 생략하는 호출은 자리를 맞출 방법이 없기 때문입니다.

기본 매개변수

기본 매개변수는 인수가 undefined일 때 대신 쓸 값을 정합니다. 인수를 넘기지 않았을 때와 undefined를 직접 넘겼을 때 모두 기본값이 쓰이고, 그 밖의 값은 그대로 들어갑니다.

function logWithPrefix(message: string, prefix: string = "[INFO]"): void {
  console.log(`${prefix} ${message}`);
}

function power(base: number, exponent: number = 2): number {
  return base ** exponent;
}

logWithPrefix("애플리케이션이 시작되었습니다.");
logWithPrefix("데이터베이스 연결 실패!", "[ERROR]");
console.log(power(3), power(3, undefined), power(3, 0));
[INFO] 애플리케이션이 시작되었습니다.
[ERROR] 데이터베이스 연결 실패!
9 9 1

power(3)과 power(3, undefined)는 지수의 자리가 undefined이므로 기본값 2를 써서 9가 됩니다. power(3, 0)의 0은 조건에서 거짓으로 취급되는 falsy 값이지만 undefined는 아니므로 기본값으로 바뀌지 않고, 3의 0제곱인 1이 됩니다. null과 빈 문자열, false도 마찬가지로 기본값을 부르지 않습니다.

기본 매개변수는 호출하는 쪽에서 보면 선택적 매개변수와 같습니다. 편집기에서 power에 마우스를 올리면 function power(base: number, exponent?: number): number로 보이므로, 호출하는 쪽에서 본 exponent의 타입은 number | undefined이고 본문 안에서는 기본값이 채워져 undefined가 빠진 number입니다. 그래서 undefined는 넘길 수 있지만 power(3, null)은 null이 이 타입에 맞지 않아 TS2345로 진단되며, null을 넘겨 기본값을 고르게 할 수는 없습니다.

기본값을 쓰면 본문에 if (prefix === undefined) prefix = "[INFO]"; 같은 코드를 적지 않아도 되고, 함수의 선언만 보고도 어떤 인수를 생략할 수 있으며 그때 무슨 값이 쓰이는지 알 수 있습니다. 다만 기본값은 들어온 값을 검증하지 않으므로, 빈 문자열처럼 업무에서 쓸 수 없는 값은 따로 걸러야 합니다.

더 깊이: 기본값이 계산되는 시점과 범위

기본값의 식은 함수를 선언할 때 한 번 계산해 두는 것이 아니라, 기본값이 필요한 호출마다 새로 계산됩니다. 그래서 function append(value: number, list: number[] = [])를 인수 하나로 두 번 부르면, 호출마다 새 빈 배열이 만들어져 앞 호출에서 넣은 값이 남지 않습니다. MDN의 기본 매개변수 설명은 이 점이 Python과 다르다고 짚습니다. 기본값 자리에 함수 호출을 적으면 그 함수도 기본값이 쓰이는 호출마다 실행되므로, 비용이 큰 계산을 기본값에 두면 호출할 때마다 그 비용을 치릅니다.

기본값의 식은 함수 본문과 다른, 본문을 감싸는 별도의 범위(scope)에서 계산됩니다. 그래서 왼쪽에 먼저 선언한 매개변수는 뒤의 기본값에서 쓸 수 있지만, 함수 본문에서 선언한 함수나 변수는 var로 선언했더라도 기본값에서 쓸 수 없고 실행 중에 ReferenceError가 납니다. 또 함수의 length 속성은 기본값이 있는 첫 매개변수와 그 뒤의 매개변수를 세지 않습니다. 인수의 개수를 length로 판단하는 라이브러리에 함수를 넘길 때는 이 점 때문에 동작이 달라질 수 있습니다.

기본값 뒤의 필수 매개변수

선택적 매개변수와 달리 기본 매개변수는 필수 매개변수 앞에도 둘 수 있습니다. 다만 그 뒤의 필수 인수는 여전히 넘겨야 하므로, 앞의 기본값을 쓰려면 그 자리에 undefined를 직접 넘깁니다.

function createGreeting(
  name: string = "Unknown",
  message: string,
  salutation: string = "Hello",
): string {
  return `${salutation}, ${name}! ${message}`;
}

console.log(createGreeting(undefined, "Have a great day!"));
console.log(createGreeting("Alice", "Nice to meet you!", "Hi"));
Hello, Unknown! Have a great day!
Hi, Alice! Nice to meet you!

createGreeting은 적어도 두 개의 인수가 필요하므로, createGreeting("World")는 Expected 2-3 arguments, but got 1.로 진단됩니다. 이렇게 기본값과 필수 인수가 섞인 순서는 호출하는 쪽에서 읽기 어려우므로, 보통은 필수 매개변수를 모두 앞에 두고 선택적 매개변수와 기본 매개변수를 뒤에 둡니다.

선택 인수가 많을 때

선택할 인수가 여러 개면, 중간 것만 생략하려 할 때마다 undefined로 자리를 채워야 합니다. 이럴 때는 선택 인수를 하나의 객체로 묶어 받는 옵션 객체(options object)가 읽기 쉽습니다.

type SummaryOptions = {
  email?: string;
  phone?: string;
};

function summarizeContact(name: string, options: SummaryOptions = {}): string {
  const parts = [`이름: ${name}`];
  if (options.email !== undefined) parts.push(`이메일: ${options.email}`);
  if (options.phone !== undefined) parts.push(`전화번호: ${options.phone}`);
  return parts.join(", ");
}

console.log(summarizeContact("윤지", { phone: "010-0000-0000" }));
console.log(summarizeContact("민호"));
이름: 윤지, 전화번호: 010-0000-0000
이름: 민호

전화번호만 넘길 때도 이름을 붙여 { phone: ... }으로 적으므로, 자리를 undefined로 채울 필요가 없고 어느 값이 무엇인지 호출하는 곳에서 바로 보입니다. 옵션 객체 자체에 기본값 {}를 주었으므로 두 번째 호출처럼 옵션을 통째로 생략할 수도 있습니다. 이 함수는 !== undefined로 생략된 속성만 골라내므로, 빈 문자열을 넘기면 그대로 출력한다는 점이 summarizeUser와 다릅니다.

정리

  • 선택적 매개변수는 인수를 생략하면 undefined가 되므로 본문에서 그 타입에 undefined가 포함되고, 필수 매개변수보다 뒤에 두어야 합니다.
  • 기본 매개변수는 인수가 생략되었거나 undefined일 때만 기본값을 쓰며, null과 빈 문자열, 0, false는 그대로 들어갑니다.
  • 기본 매개변수는 호출하는 쪽에서 선택적 매개변수로 보이지만, 본문 안에서는 기본값이 채워져 undefined가 빠진 타입입니다.
  • 기본 매개변수는 필수 매개변수 앞에 둘 수 있지만, 그 기본값을 쓰려면 자리에 undefined를 넘겨야 하므로 보통은 뒤에 둡니다.
  • 선택할 인수가 많으면 옵션 객체로 묶어, 생략할 값의 자리를 맞추는 대신 이름으로 넘기게 합니다.

연습 문제

1. 기본값이 쓰이는 호출

위의 power에서 power(2, 10), power(2, undefined), power(5, 0)의 결과는 각각 무엇인가요? power(2, null)은 어떻게 되나요?

해설 보기

power(2, 10)은 2의 10제곱인 1024입니다. power(2, undefined)는 지수의 자리가 undefined이므로 기본값 2를 써서 4이고, power(5, 0)은 0이 그대로 들어가 1입니다. power(2, null)은 null이 호출하는 쪽에서 본 매개변수의 타입에 맞지 않아 Argument of type 'null' is not assignable to parameter of type 'number | undefined'.로 진단됩니다.

2. 자리를 맞춘 호출

summarizeUser에 이름 윤지와 전화번호 010-0000-0000만 넘기는 호출을 쓰고 결과를 적어 보세요. createGreeting에는 이름의 기본값을 쓰면서 메시지는 좋은 하루, 인사말은 안녕을 넘겨 보세요. 이름에 빈 문자열을 넘기면 결과는 어떻게 되나요?

해설 보기

summarizeUser("윤지", undefined, "010-0000-0000")은 이름: 윤지, 전화번호: 010-0000-0000을 돌려줍니다. createGreeting(undefined, "좋은 하루", "안녕")은 안녕, Unknown! 좋은 하루입니다. 이름에 빈 문자열을 넘긴 createGreeting("", "좋은 하루", "안녕")은 빈 문자열이 undefined가 아니므로 그대로 쓰여 안녕, ! 좋은 하루가 되며, 빈 이름을 막으려면 본문에서 따로 검사해야 합니다.

3. 빈 문자열을 넘겼을 때

summarizeUser("민지", "")와 summarizeContact("민지", { email: "" })는 각각 무엇을 돌려주나요? 두 결과가 다른 이유는 무엇인가요?

해설 보기

summarizeUser("민지", "")는 이름: 민지를 돌려주고, summarizeContact("민지", { email: "" })는 이름: 민지, 이메일: 을 돌려줍니다. summarizeUser의 if (email)은 빈 문자열을 거짓으로 보아 이메일을 빼지만, summarizeContact의 !== undefined는 생략된 경우만 빼고 빈 문자열은 넘겨진 값으로 다루기 때문입니다. 어떤 값을 "없음"으로 볼지는 함수마다 정해야 하며, 검사의 모양이 그 결정을 그대로 드러냅니다.

다음 글 함수 오버로딩에서는 인수의 타입과 개수에 따라 돌려주는 타입이 달라지는 함수를, 여러 개의 시그니처로 나타내는 방법을 다룹니다.