스프레드 문법
스프레드 문법 (...)
스프레드(...) 문법은 배열이나 문자열 같은 이터러블(iterable)을, 0개 이상의 인자(함수 호출의 경우)나 요소(배열 리터럴의 경우)가 기대되는 위치에서 확장할 수 있게 합니다. 객체 리터럴에서는 스프레드 문법이 객체의 속성을 열거하고 그 키-값 쌍을 생성 중인 객체에 추가합니다.
스프레드 문법은 나머지(rest) 문법과 정확히 똑같이 생겼습니다. 어떤 면에서 스프레드 문법은 나머지 문법의 반대입니다. 스프레드 문법은 배열을 그 요소들로 "확장"하는 반면, 나머지 문법은 여러 요소를 모아 단일 요소로 "응축"합니다. 나머지 매개변수와 나머지 속성을 참고하세요.
호환성: Baseline — 널리 사용 가능(Widely available). 이 기능은 잘 확립되어 있으며 많은 기기와 브라우저 버전에서 작동합니다. 2015년 10월부터 브라우저에서 사용할 수 있습니다.
본문
Try it
function sum(x, y, z) {
return x + y + z;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers));
// Expected output: 6
console.log(sum.apply(null, numbers));
// Expected output: 6
구문 (Syntax)
myFunction(a, ...iterableObj, b)
[1, ...iterableObj, '4', 'five', 6]
{ ...obj, key: 'value' }
설명 (Description)
스프레드 문법은 객체나 배열의 모든 요소를 새 배열이나 객체에 포함시켜야 하거나, 함수 호출의 인자 목록에 하나씩 적용해야 할 때 사용할 수 있습니다. 스프레드 문법을 받아들이는 세 가지 구별되는 위치가 있습니다.
- 함수 인자 목록 (
myFunction(a, ...iterableObj, b)) - 배열 리터럴 (
[1, ...iterableObj, '4', 'five', 6]) - 객체 리터럴 (
{ ...obj, key: 'value' })
문법은 같아 보이지만, 각각 약간 다른 의미론을 지닙니다.
Array와 String 같은 이터러블 값만 배열 리터럴과 인자 목록에서 스프레드될 수 있습니다. 많은 객체는 이터러블이 아닙니다. Symbol.iterator 메서드가 없는 모든 일반 객체가 그렇습니다.
const obj = { key1: "value1" };
const array = [...obj]; // TypeError: obj is not iterable
반면에 객체 리터럴에서 스프레드는 값의 자체 속성(own properties)을 열거합니다. 일반적인 배열의 경우 모든 인덱스가 열거 가능한 자체 속성이라 배열은 객체로 스프레드될 수 있습니다.
const array = [1, 2, 3];
const obj = { ...array }; // { 0: 1, 1: 2, 2: 3 }
모든 원시 값은 객체로 스프레드될 수 있습니다. 문자열만 열거 가능한 자체 속성을 가지며, 그 외의 것을 스프레드해도 새 객체에 속성이 생성되지 않습니다.
const obj = { ...true, ..."test", ...10 };
// { '0': 't', '1': 'e', '2': 's', '3': 't' }
함수 호출에 스프레드 문법을 사용할 때는 JavaScript 엔진의 인자 길이 제한을 초과할 가능성에 유의하세요. 자세한 내용은 Function.prototype.apply()를 참고하세요.
예제 (Examples)
함수 호출에서의 스프레드 (Spread in function calls)
apply()대체: 배열의 요소를 함수의 인자로 사용하려는 경우Function.prototype.apply()를 사용하는 것이 흔합니다.
스프레드 문법을 사용하면 위 코드를 다음과 같이 쓸 수 있습니다.function myFunction(x, y, z) {} const args = [0, 1, 2]; myFunction.apply(null, args);
인자 목록의 어떤 인자에서든 스프레드 문법을 사용할 수 있고, 스프레드 문법을 여러 번 사용할 수 있습니다.function myFunction(x, y, z) {} const args = [0, 1, 2]; myFunction(...args);function myFunction(v, w, x, y, z) {} const args = [0, 1]; myFunction(-1, ...args, 2, ...[3]);new연산자에 적용:new로 생성자를 호출할 때 배열과apply()를 직접 사용할 수 없습니다.apply()는 대상 함수를 생성(construct)하는 대신 호출하기 때문이며, 이는 무엇보다new.target이undefined가 된다는 것을 뜻합니다. 그러나 배열은 스프레드 문법 덕분에new와 함께 쉽게 사용할 수 있습니다.const dateFields = [1970, 0, 1]; // 1 Jan 1970 const d = new Date(...dateFields);
배열 리터럴에서의 스프레드 (Spread in array literals)
- 더 강력한 배열 리터럴: 스프레드 문법이 없다면, 배열 리터럴 문법으로는 기존 배열을 한 부분으로 사용해 새 배열을 만드는 것이 더 이상 충분하지 않습니다. 대신
push(),splice(),concat()등을 조합한 명령형 코드를 사용해야 합니다. 스프레드 문법을 사용하면 훨씬 간결해집니다.
인자 목록을 위한 스프레드처럼,const parts = ["shoulders", "knees"]; const lyrics = ["head", ...parts, "and", "toes"]; // ["head", "shoulders", "knees", "and", "toes"]...는 배열 리터럴 어디에서나 사용할 수 있고 한 번 이상 사용될 수 있습니다. - 배열 복사 (Copying an array): 스프레드 문법을 사용해 배열의 얕은 복사본을 만들 수 있습니다. 각 배열 요소는 복사되지 않고 정체성을 유지합니다.
배열을 복사할 때 스프레드 문법은 사실상 한 단계 깊이만 들어갑니다. 따라서 다차원 배열을 복사하는 데는 부적합할 수 있습니다.const arr = [1, 2, 3]; const arr2 = [...arr]; // like arr.slice() arr2.push(4); // arr2 becomes [1, 2, 3, 4] // arr remains unaffectedObject.assign()도 마찬가지입니다 — JavaScript에서 깊은 복제를 하는 기본 연산은 없습니다. 웹 API 메서드structuredClone()은 특정 지원 타입의 값을 깊게 복사할 수 있게 해줍니다. 자세한 내용은 얕은 복사(shallow copy)를 참고하세요.const a = [[1], [2], [3]]; const b = [...a]; b.shift().shift(); // 1 // Oh no! Now array 'a' is affected as well: console.log(a); // [[], [2], [3]] - 더 나은 배열 결합 방법:
Array.prototype.concat()은 배열을 기존 배열의 끝에 결합할 때 흔히 사용됩니다. 스프레드 문법이 없다면 다음과 같이 합니다.
스프레드 문법을 사용하면 다음과 같이 됩니다.let arr1 = [0, 1, 2]; const arr2 = [3, 4, 5]; // Append all items from arr2 onto arr1 arr1 = arr1.concat(arr2);let arr1 = [0, 1, 2]; const arr2 = [3, 4, 5]; arr1 = [...arr1, ...arr2]; // arr1 is now [0, 1, 2, 3, 4, 5]Array.prototype.unshift()는 기존 배열의 시작에 값 배열을 삽입하는 데 자주 사용됩니다. 스프레드 문법이 없다면 다음과 같이 합니다.
스프레드 문법을 사용하면 다음과 같이 됩니다.const arr1 = [0, 1, 2]; const arr2 = [3, 4, 5]; // Prepend all items from arr2 onto arr1 Array.prototype.unshift.apply(arr1, arr2); console.log(arr1); // [3, 4, 5, 0, 1, 2]let arr1 = [0, 1, 2]; const arr2 = [3, 4, 5]; arr1 = [...arr2, ...arr1]; console.log(arr1); // [3, 4, 5, 0, 1, 2]참고:
unshift()와 달리 이 방식은 원래arr1배열을 제자리에서 수정하는 대신 새arr1을 만듭니다. - 배열에 조건부로 값 추가하기: 조건 연산자를 사용해 배열 리터럴에서 요소를 조건에 따라 있게 하거나 없게 할 수 있습니다.
조건이const isSummer = false; const fruits = ["apple", "banana", ...(isSummer ? ["watermelon"] : [])]; // ['apple', 'banana']false일 때는 빈 배열을 스프레드하므로 최종 배열에 아무것도 추가되지 않습니다. 이는 다음 경우와 다릅니다.
이 경우const fruits = ["apple", "banana", isSummer ? "watermelon" : undefined]; // ['apple', 'banana', undefined]isSummer가false일 때 추가undefined요소가 추가되며, 이 요소는Array.prototype.map()같은 메서드가 방문하게 됩니다.
객체 리터럴에서의 스프레드 (Spread in object literals)
- 객체 복사와 병합 (Copying and merging objects): 스프레드 문법을 사용해 여러 객체를 하나의 새 객체로 병합할 수 있습니다.
단일 스프레드는 배열 복사와 유사하게 원본 객체의 얕은 복사본을 만듭니다(열거 불가능한 속성과 프로토타입 복사는 제외).const obj1 = { foo: "bar", x: 42 }; const obj2 = { bar: "baz", y: 13 }; const mergedObj = { ...obj1, ...obj2 }; // { foo: "bar", x: 42, bar: "baz", y: 13 }const clonedObj = { ...obj1 }; // { foo: "bar", x: 42 } - 속성 덮어쓰기 (Overriding properties): 한 객체가 다른 객체로 스프레드되거나 여러 객체가 하나로 스프레드될 때 같은 이름의 속성이 발견되면, 그 속성은 원래 설정된 위치를 유지하면서 마지막에 할당된 값을 취합니다.
const obj1 = { foo: "bar", x: 42 }; const obj2 = { foo: "baz", y: 13 }; const mergedObj = { x: 41, ...obj1, ...obj2, y: 9 }; // { x: 42, foo: "baz", y: 9 } - 객체에 조건부로 속성 추가하기: 조건 연산자를 사용해 객체 리터럴에서 요소를 조건에 따라 있게 하거나 없게 할 수 있습니다.
조건이const isSummer = false; const fruits = { apple: 10, banana: 5, ...(isSummer ? { watermelon: 30 } : {}), }; // { apple: 10, banana: 5 }false인 경우는 빈 객체이므로 최종 객체로 아무것도 스프레드되지 않습니다. 이는 다음 경우와 다릅니다.
이 경우const fruits = { apple: 10, banana: 5, watermelon: isSummer ? 30 : undefined, }; // { apple: 10, banana: 5, watermelon: undefined }watermelon속성은 항상 존재하며Object.keys()같은 메서드가 방문하게 됩니다. 원시 값도 객체로 스프레드될 수 있고, 모든 거짓(falsy) 값은 열거 가능한 속성이 없다는 관찰로부터 논리 AND 연산자를 간단히 사용할 수 있습니다.
이 경우const isSummer = false; const fruits = { apple: 10, banana: 5, ...(isSummer && { watermelon: 30 }), };isSummer가 어떤 거짓 값이어도fruits객체에 속성이 생성되지 않습니다. Object.assign()과의 비교:Object.assign()은 객체를 변형(mutate)하는 데 사용될 수 있는 반면, 스프레드 문법은 그럴 수 없습니다.
또한const obj1 = { foo: "bar", x: 42 }; Object.assign(obj1, { x: 1337 }); console.log(obj1); // { foo: "bar", x: 1337 }Object.assign()은 대상 객체의 세터(setter)를 트리거하는 반면, 스프레드 문법은 그렇지 않습니다.
단일 스프레드로const objectAssign = Object.assign( { set foo(val) { console.log(val); }, }, { foo: 1 }, ); // Logs "1"; objectAssign.foo is still the original setter const spread = { set foo(val) { console.log(val); }, ...{ foo: 1 }, }; // Nothing is logged; spread.foo is 1Object.assign()함수를 순진하게 다시 구현할 수는 없습니다.
위 예제에서 스프레드 문법은 기대처럼 작동하지 않습니다. 나머지 매개변수 때문에 인자의 배열을 객체 리터럴로 스프레드하기 때문입니다. 다음은 세터를 트리거하지 않고 어떤 객체도 변형하지 않는다는 점을 제외하면const obj1 = { foo: "bar", x: 42 }; const obj2 = { foo: "baz", y: 13 }; const merge = (...objects) => ({ ...objects }); const mergedObj1 = merge(obj1, obj2); // { 0: { foo: 'bar', x: 42 }, 1: { foo: 'baz', y: 13 } } const mergedObj2 = merge({}, obj1, obj2); // { 0: {}, 1: { foo: 'bar', x: 42 }, 2: { foo: 'baz', y: 13 } }Object.assign()과 유사하게 동작하는 스프레드 문법을 사용한merge구현입니다.const obj1 = { foo: "bar", x: 42 }; const obj2 = { foo: "baz", y: 13 }; const merge = (...objects) => objects.reduce((acc, cur) => ({ ...acc, ...cur })); const mergedObj = merge(obj1, obj2); // { foo: 'baz', x: 42, y: 13 }
명세 (Specifications)
ECMAScript® 2027 언어 명세 — prod-SpreadElement, prod-ArgumentList, prod-PropertyDefinition