나머지 매개변수와 스프레드 — 가변 인수를 타입 안전하게 처리하기
TypeScript 나머지 매개변수(rest parameters)의 문법과 타입 규칙, 튜플 타입을 활용한 정밀한 rest 타입 정의, 스프레드 인수와의 연계, 실전 패턴을 정리합니다.
지난 글에서 선택적/기본값 매개변수를 살펴봤다. 이번에는 나머지 매개변수(Rest Parameters) 와 스프레드 인수(Spread Arguments) 를 다룬다. 개수가 정해지지 않은 인수를 처리하면서도 타입 안전성을 유지하는 방법을 살펴본다.
나머지 매개변수 기본 문법
... 문법으로 나머지 인수를 배열로 받는다. TypeScript는 타입을 배열 또는 튜플로 지정해야 한다.
// 기본 rest — 배열 타입
function sum(...nums: number[]): number {
return nums.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15
sum(); // 0 — 빈 배열 허용
// 혼합 타입 — 앞 매개변수와 조합
function log(level: "info" | "warn" | "error", ...messages: string[]): void {
console.log(`[${level}]`, ...messages);
}
log("info", "서버 시작됨");
log("warn", "메모리 부족", "GC 강제 실행");
log("error", "연결 실패", "재시도 중...", "3회 남음");
나머지 매개변수는 마지막 위치에만 올 수 있고, 함수당 하나만 사용할 수 있다.
// ❌ 컴파일 에러 — rest는 반드시 마지막 위치
function bad(...a: string[], b: number): void {} // TS1014
// ✅ 올바른 순서
function ok(a: string, b: number, ...rest: boolean[]): void {}
튜플 타입 Rest
배열 타입 대신 튜플 타입을 사용하면 각 위치의 타입을 정확히 지정할 수 있다.
// 정확히 3개의 인수, 각 타입이 다름
function createRecord(...args: [string, number, boolean]): Record<string, unknown> {
const [name, age, active] = args;
return { name, age, active };
}
createRecord("Alice", 30, true); // OK
createRecord("Bob", 25, false); // OK
createRecord("Charlie"); // ❌ 인수 부족
createRecord("Dave", 40, true, 0); // ❌ 인수 과다
// 선택적 요소가 있는 튜플 rest
function formatDate(...args: [number, number?, number?]): string {
const [year, month = 1, day = 1] = args;
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
}
formatDate(2026); // "2026-01-01"
formatDate(2026, 6); // "2026-06-01"
formatDate(2026, 6, 15); // "2026-06-15"
튜플 rest는 각 인수의 위치와 타입이 고정될 때 유용하다. 오버로드보다 간결하게 같은 효과를 낼 수 있다.
나머지 매개변수와 타입 추론
제네릭과 나머지 매개변수를 조합하면 인수 타입을 캡처할 수 있다.
// 나머지 인수 타입을 튜플로 캡처
function call<T extends unknown[], R>(
fn: (...args: T) => R,
...args: T
): R {
return fn(...args);
}
call((a: number, b: number) => a + b, 1, 2); // 3 — 타입 안전
call((s: string) => s.length, "hello"); // 5
// 함수 파라미터 타입 추출
type Parameters<T extends (...args: unknown[]) => unknown> =
T extends (...args: infer P) => unknown ? P : never;
type AddParams = Parameters<(a: number, b: number) => number>;
// [a: number, b: number]
T extends unknown[]는 타입 매개변수 T가 배열(즉, 여러 인수를 나타내는 튜플)이어야 한다는 제약이다.
스프레드 인수
나머지 매개변수와 반대 방향으로, 배열을 펼쳐서 함수 인수로 전달하는 것이 스프레드 인수(Spread Arguments) 다.
function add(a: number, b: number, c: number): number {
return a + b + c;
}
const nums = [1, 2, 3] as const; // readonly [1, 2, 3] — 튜플로 추론
add(...nums); // OK — 정확히 3개의 number
const arr: number[] = [1, 2, 3]; // number[]
add(...arr); // ❌ TS2556: 가변 길이 배열이므로 개수 보장 불가
가변 길이 배열(number[])을 스프레드하면 TypeScript가 개수를 보장할 수 없어 에러가 발생한다. as const로 튜플로 만들거나, 함수를 rest 매개변수로 바꿔 해결한다.
// 해결책 1: as const
const fixed = [1, 2, 3] as const;
add(...fixed); // OK
// 해결책 2: 함수를 rest로 변경
function addAll(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
addAll(...arr); // OK
실전 패턴: 고차 함수에서의 Rest
나머지 매개변수는 함수를 래핑하거나 데코레이팅할 때 특히 유용하다.
// 함수 호출 로깅 래퍼
function withLogging<T extends unknown[], R>(
fn: (...args: T) => R,
label: string,
): (...args: T) => R {
return (...args: T) => {
console.log(`[${label}] 호출:`, args);
const result = fn(...args);
console.log(`[${label}] 반환:`, result);
return result;
};
}
const loggedAdd = withLogging((a: number, b: number) => a + b, "add");
loggedAdd(1, 2); // 로그 출력 후 3 반환
// 재시도 래퍼
async function withRetry<T extends unknown[], R>(
fn: (...args: T) => Promise<R>,
retries: number,
...args: T
): Promise<R> {
for (let i = 0; i < retries; i++) {
try {
return await fn(...args);
} catch (err) {
if (i === retries - 1) throw err;
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
}
}
throw new Error("도달 불가");
}
T extends unknown[]로 원래 함수의 매개변수 타입을 그대로 보존하면서 래핑할 수 있다.
나머지 매개변수 vs arguments 객체
ES5 이전의 arguments 객체와 나머지 매개변수는 다르다.
// ❌ arguments 객체 — any 타입, 화살표 함수에서 사용 불가
function oldSum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i]; // any
}
return total;
}
// ✅ 나머지 매개변수 — 타입 안전, 모든 함수에서 사용 가능
function newSum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
// 화살표 함수에서도 동작
const arrowSum = (...nums: number[]): number =>
nums.reduce((a, b) => a + b, 0);
arguments는 TypeScript에서도 사용할 수 있지만 any 타입으로 처리되므로 나머지 매개변수를 사용하는 것이 바람직하다.
순서 제약과 타입 안전성
나머지 매개변수는 마지막에 위치해야 하지만, 앞에 여러 필수 매개변수를 둘 수 있다.
// 첫 번째 인수 필수, 나머지 선택
function tag(first: string, ...rest: string[]): string {
return [first, ...rest].join(", ");
}
tag("필수"); // "필수"
tag("첫째", "둘째"); // "첫째, 둘째"
tag("a", "b", "c", "d"); // "a, b, c, d"
// 타입 가드와 함께
function processAll<T>(
validator: (item: T) => boolean,
...items: T[]
): T[] {
return items.filter(validator);
}
processAll((n: number) => n > 0, -1, 2, -3, 4, 5); // [2, 4, 5]
processAll((s: string) => s.length > 2, "hi", "hello", "ok", "world"); // ["hello", "world"]
나머지 매개변수를 제네릭과 함께 사용하면 첫 번째 함수 인수의 타입에서 나머지 인수 타입을 추론할 수 있어 매우 유연한 API를 설계할 수 있다.
지난 글: 선택적 매개변수와 기본값 — 유연한 함수 시그니처 설계
다음 글: this 매개변수 — 메서드와 this 타입 처리
읽어주셔서 감사합니다. 😊