나머지 매개변수와 전개: JavaScript & TypeScript에서 ...의 두 얼굴
최근 코드베이스의 라우터를 리팩토링하다가 이 작은 헬퍼를 마주쳤습니다:
export function docsPath(...segments: string[]): string {
const path = segments.filter(Boolean).join("/");
return path ? `${DOCS_BASE}/${path}` : DOCS_BASE;
}제 눈을 멈추게 한 부분은 ...segments였습니다. 객체를 복사하거나({ ...props }) 배열을 복사할 때([...items]) ... 연산자를 수백 번 봐왔지만, 정작 함수의 매개변수 목록 안에서 이게 무슨 일을 하는지는 한 번도 제대로 생각해본 적이 없었습니다. 알고 보니 ...는 놓이는 위치에 따라 두 가지 다른 역할을 합니다 — 그리고 이 차이를 이해하면 일상적으로 마주치는 많은 코드가 한순간에 이해됩니다.
나머지 매개변수가 해결하는 문제
URL 세그먼트들을 하나의 경로로 합치는 함수를 만든다고 해봅시다. 호출하는 쪽에서 세그먼트를 몇 개나 넘길까요? 알 수 없습니다 — 어떤 때는 하나, 어떤 때는 셋, 어떤 때는 아예 없습니다:
docsPath("guides"); // "/docs/guides"
docsPath("guides", "my-slug"); // "/docs/guides/my-slug"
docsPath("customers", "acme", "x"); // "/docs/customers/acme/x"
docsPath(); // "/docs"나머지 매개변수가 등장하기 전에는, 다루기 번거롭고 진짜 배열도 아닌 마법 같은 arguments 객체에 의존해야 했습니다:
function docsPath() {
// `arguments`는 배열과 비슷하지만, .filter나 .join을 직접 호출할 수 없습니다
const segments = Array.prototype.slice.call(arguments);
// ...
}나머지 매개변수는 이 모든 것을 깔끔한 문법 하나로 대체합니다.
...segments가 실제로 의미하는 것
...가 함수의 매개변수 목록에 나타나면, 그것은 나머지 매개변수(rest parameter) 입니다. 이렇게 말하는 셈입니다:
"호출자가 넘기는 인자가 몇 개든 전부
segments라는 하나의 진짜 배열로 모아라."
function docsPath(...segments: string[]) {
// segments는 진짜 Array<string>입니다
}
docsPath("a", "b", "c"); // segments === ["a", "b", "c"]
docsPath(); // segments === []segments가 진짜 배열이기 때문에, 모든 배열 메서드를 공짜로 쓸 수 있습니다 — 헬퍼가 쓰는 게 바로 그것이죠:
const path = segments.filter(Boolean).join("/");기억해둘 만한 몇 가지 규칙:
함수는 나머지 매개변수를 단 하나만 가질 수 있습니다.
매개변수 목록의 맨 마지막에 와야 합니다 ("나머지"를 쓸어 담으니까요).
일반 매개변수와 조합할 수 있습니다:
function logEvent(level: string, ...messages: string[]) {
console.log(`[${level}]`, ...messages);
}
logEvent("info", "user", "logged", "in");
// level === "info"
// messages === ["user", "logged", "in"]헬퍼를 단계별로 따라가기
DOCS_BASE = "/docs"라고 가정하고 docsPath를 추적해봅시다:
docsPath("guides", "my-slug");
// 1. segments = ["guides", "my-slug"] ← 나머지 매개변수가 인자를 모음
// 2. .filter(Boolean) → ["guides", "my-slug"] ("", undefined, null 제거)
// 3. .join("/") → "guides/my-slug"
// 4. path가 truthy → "/docs/guides/my-slug"
docsPath();
// 1. segments = []
// 2. .filter(Boolean) → []
// 3. .join("/") → ""
// 4. path가 falsy → "/docs" (base만 반환).filter(Boolean) 단계는 멋진 방어 장치입니다. 호출자가 조건부이거나 비어 있을 수 있는 세그먼트를 넘겨도, 보기 흉한 이중 슬래시를 만들지 않게 해줍니다:
const slug = maybeUndefined(); // undefined나 ""일 수 있음
docsPath("guides", slug); // 여전히 "/docs/guides", 절대 "/docs/guides/"가 아님(Boolean을 콜백으로 쓰면 모든 falsy 값을 제거합니다: "", undefined, null, 0, NaN, false.)
또 다른 얼굴: 전개
여기서 반전이 있습니다. 똑같은 ... 토큰이 함수 호출이나 배열/객체 리터럴에 나타나면 정반대 일을 합니다. 거기서는 전개 연산자(spread operator) 가 됩니다 — 기존의 이터러블을 받아 개별 요소들로 펼칩니다.
const parts = ["customers", "acme"];
docsPath(...parts);
// docsPath("customers", "acme")와 동일그러니까:
Rest (매개변수 목록에서): 여러 값을 배열 안으로 모읍니다.
Spread (호출 / 리터럴에서): 배열을 여러 값으로 흩뿌립니다.
둘은 거울상입니다. 가장 분명하게 보는 방법은 둘을 함께 쓰는 것입니다:
function docsPath(...segments: string[]) { // rest: 모으기
return "/docs/" + segments.join("/");
}
const parts = ["a", "b", "c"];
docsPath(...parts); // spread: 흩뿌리기전개는 다른 여러 곳에서도 등장합니다:
// 배열 복사 / 병합
const merged = [...arr1, ...arr2];
// 객체 복사 / 병합
const updated = { ...user, name: "New Name" };
// 개별 인자가 기대되는 곳에 배열 넘기기
Math.max(...[3, 1, 4, 1, 5]); // 5빠른 멘탈 모델
딱 하나만 기억해야 한다면, ...가 어디에 있는지를 기억하세요:
위치이름하는 일함수 매개변수 목록Rest인자들을 배열 안으로 모음함수 호출Spread배열을 인자들로 펼침배열 리터럴 [...]Spread요소들을 새 배열에 펼쳐 넣음객체 리터럴 {...}Spread속성들을 새 객체에 펼쳐 넣음
같은 문법, 반대 방향 — 모으기 vs 흩뿌리기.
TypeScript 노트
TypeScript에서는 나머지 매개변수를 배열로 타입 지정하며, 모인 각 인자는 요소 타입과 일치해야 합니다:
function docsPath(...segments: string[]): string { /* ... */ }
docsPath("a", "b"); // ✅
docsPath("a", 42); // ❌ 'number' 타입의 인자는 'string' 타입에 할당할 수 없습니다심지어 튜플 타입으로 이질적인 나머지 매개변수를 타입 지정할 수도 있는데, 이것이 타입이 지정된 이벤트 이미터나 console.log 스타일 시그니처가 만들어지는 방식입니다:
function emit(event: string, ...args: [id: number, ok: boolean]) {}
emit("done", 1, true); // ✅
emit("done", 1); // ❌ boolean이 빠짐정리
매개변수 목록 안의
...이름은 나머지 매개변수입니다 — "임의 개수의 인자"를filter,map,join을 쓸 수 있는 진짜 배열로 바꿔줍니다.호출이나 리터럴 안의
...값은 전개 연산자입니다 — 배열/객체를 개별 조각들로 펼칩니다.둘은 서로 역(逆)입니다: rest는 모으고, spread는 흩뿌립니다.
나머지 매개변수는 옛
arguments객체를 대체하는 현대적이고 타입 안전한 방법이며,docsPath같은 작은 유틸리티를 깔끔하고 유연하게 만들어줍니다.
아주 작은 문법이지만, ...의 두 얼굴을 한번 보고 나면 관용적인 JavaScript와 TypeScript 코드의 놀랄 만큼 많은 부분이 더 이상 마법처럼 보이지 않게 됩니다.
