스프레드 문법

스프레드 문법 (...)

스프레드(...) 문법은 배열이나 문자열 같은 이터러블(iterable)을, 0개 이상의 인자(함수 호출의 경우)나 요소(배열 리터럴의 경우)가 기대되는 위치에서 확장할 수 있게 합니다. 객체 리터럴에서는 스프레드 문법이 객체의 속성을 열거하고 그 키-값 쌍을 생성 중인 객체에 추가합니다.

스프레드 문법은 나머지(rest) 문법과 정확히 똑같이 생겼습니다. 어떤 면에서 스프레드 문법은 나머지 문법의 반대입니다. 스프레드 문법은 배열을 그 요소들로 "확장"하는 반면, 나머지 문법은 여러 요소를 모아 단일 요소로 "응축"합니다. 나머지 매개변수와 나머지 속성을 참고하세요.

호환성: Baseline — 널리 사용 가능(Widely available). 이 기능은 잘 확립되어 있으며 많은 기기와 브라우저 버전에서 작동합니다. 2015년 10월부터 브라우저에서 사용할 수 있습니다.

출처: Spread syntax (...)

본문

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' })

문법은 같아 보이지만, 각각 약간 다른 의미론을 지닙니다.

ArrayString 같은 이터러블 값만 배열 리터럴과 인자 목록에서 스프레드될 수 있습니다. 많은 객체는 이터러블이 아닙니다. 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.targetundefined가 된다는 것을 뜻합니다. 그러나 배열은 스프레드 문법 덕분에 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 unaffected
    
    배열을 복사할 때 스프레드 문법은 사실상 한 단계 깊이만 들어갑니다. 따라서 다차원 배열을 복사하는 데는 부적합할 수 있습니다. Object.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]
    
    이 경우 isSummerfalse일 때 추가 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 1
    
    단일 스프레드로 Object.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

더 알아보기