본문 바로가기

구조 분해 할당과 스프레드 연산자

배열·객체에서 필요한 값을 구조 분해하고 스프레드로 복사·결합·함수 인수 전달을 간결하게 처리합니다.

8장의 앞 두 글에서는 let과 const로 변수 선언을 다듬고, 화살표 함수로 함수를 더 짧고 예측하기 쉽게 쓰는 방법을 익혔습니다. 이번에는 데이터를 다루는 방식을 크게 바꾼 두 문법, 구조 분해 할당(destructuring assignment)과 스프레드 문법(spread syntax)을 살펴봅니다.

구조 분해 할당은 배열이나 객체에서 필요한 값을 꺼내 변수에 담는 문법이고, 스프레드 문법은 배열이나 객체를 펼쳐 복사하거나 합치는 문법입니다. 둘 다 코드를 짧고 읽기 쉽게 만들어 주지만, 무엇이 새로 만들어지고 무엇이 원본과 공유되는지를 정확히 알아야 안전하게 쓸 수 있습니다. 이 글의 코드 블록은 서로 독립된 예제라서 같은 이름의 const를 선언하는 경우가 있으므로, 한 파일에 모두 붙이지 말고 따로 실행하세요.


구조 분해 할당

구조 분해 할당은 배열의 요소나 객체의 속성을 해체해 각각의 변수에 담는 표현식입니다. 복잡한 데이터에서 필요한 값만 깔끔하게 꺼낼 때 유용합니다. 객체와 배열에서 본 참조 공유를 떠올려 보면, 구조 분해는 값을 새 이름에 연결할 뿐 객체를 복사하지는 않는다는 점이 중요합니다.

배열 구조 분해

배열의 요소를 순서대로 꺼내 변수에 담습니다. 등호 왼쪽에 대괄호로 변수 이름을 나열하면, 오른쪽 배열의 0번, 1번, 2번 요소가 그 순서대로 각 변수에 들어갑니다. 이때 원래 배열 colors는 바뀌지 않습니다.

const colors = ["red", "green", "blue"];

// 배열 구조 분해 할당
const [firstColor, secondColor, thirdColor] = colors;

console.log(firstColor);  // 결과: "red"
console.log(secondColor); // 결과: "green"
console.log(thirdColor);  // 결과: "blue"

변수가 요소보다 적으면 남는 요소는 무시되고, 변수가 더 많으면 남는 변수에는 undefined가 들어갑니다.

필요한 요소만 꺼내고 싶다면, 건너뛸 자리를 쉼표로 비워 둡니다.

const numbers = [10, 20, 30, 40, 50];
const [a, , c, , e] = numbers; // 두 번째와 네 번째 요소 건너뛰기
console.log(a); // 결과: 10
console.log(c); // 결과: 30
console.log(e); // 결과: 50

꺼낼 값이 undefined일 때 쓸 기본값을 =로 정할 수 있습니다. 배열에 그 위치의 요소가 없을 때도 undefined로 보므로 기본값이 쓰입니다.

const fruits = ["apple"];
const [myFruit, otherFruit = "orange"] = fruits;
console.log(myFruit);    // 결과: "apple"
console.log(otherFruit); // 결과: "orange" (fruits 배열에 두 번째 요소가 없으므로 기본값 사용)
const [item1, item2 = "default"] = [];
console.log(item1); // 결과: undefined
console.log(item2); // 결과: "default"

나머지 요소(rest element) ...를 쓰면 남은 요소를 새 배열로 모읍니다. 스프레드와 모양은 같지만, 여기서는 값을 펼치는 것이 아니라 모으는 역할입니다.

const scores = [80, 90, 70, 60, 95];
const [math, english, ...restScores] = scores;
console.log(math);       // 결과: 80
console.log(english);    // 결과: 90
console.log(restScores); // 결과: [70, 60, 95] (새로운 배열)

임시 변수 없이 두 변수의 값을 바꿀 수도 있습니다. 오른쪽에 [y, x] 배열을 만든 뒤, 그 배열을 다시 x와 y로 구조 분해하는 것입니다.

let x = 1;
let y = 2;
[x, y] = [y, x]; // x와 y의 값을 교환
console.log(x); // 결과: 2
console.log(y); // 결과: 1

객체 구조 분해

객체의 속성을 꺼내 변수에 담습니다. 배열은 순서로 꺼내지만, 객체는 속성 이름으로 꺼냅니다. 단축형에서는 속성 이름이 그대로 변수 이름이 됩니다.

const person = {
    name: "Alice",
    age: 30,
    city: "New York"
};

// 객체 구조 분해 할당
const { name, age } = person;

console.log(name); // 결과: "Alice"
console.log(age);  // 결과: 30
// console.log(city); // ReferenceError: city is not defined (city는 할당하지 않음)

패턴에 적지 않은 city는 변수로 만들어지지 않으므로, 마지막 주석 줄처럼 읽으려 하면 ReferenceError가 납니다. 배열과 달리 순서는 상관없어서 { age, name }으로 적어도 결과가 같습니다. 이어지는 세 예는 위의 person 객체를 이어서 쓰므로, 따로 실행할 때도 person 선언은 함께 붙여 넣습니다.

속성 이름과 다른 이름으로 변수를 만들고 싶다면 속성이름: 변수이름 형태로 적습니다. 콜론 왼쪽이 꺼낼 속성, 오른쪽이 새로 만들 변수이므로, 다음 줄은 userName과 userAge 두 변수만 만듭니다.

const { name: userName, age: userAge } = person;
console.log(userName); // 결과: "Alice"
console.log(userAge);  // 결과: 30

배열처럼 속성 값이 undefined이거나 속성이 없을 때 쓸 기본값을 정할 수 있습니다. 이름 바꾸기와 함께 쓸 때는 { country: nation = "Korea" }처럼 새 변수 이름 뒤에 기본값을 적습니다.

const { country = "Korea", age } = person; // person 객체에 country 프로퍼티가 없으므로 기본값 사용
console.log(country); // 결과: "Korea"
console.log(age);     // 결과: 30

기본값은 꺼내는 쪽의 변수에만 쓰일 뿐이어서, person 객체에 country 속성이 새로 생기지는 않습니다.

나머지 속성(rest property) ...는 꺼내지 않은 나머지 속성을 새 객체로 모읍니다. 나머지 속성은 패턴의 맨 끝에만 올 수 있습니다.

const { name, ...restInfo } = person;
console.log(name);     // 결과: "Alice"
console.log(restInfo); // 결과: { age: 30, city: "New York" } (새로운 객체)

중첩된 객체도 한 번에 구조 분해할 수 있습니다. 패턴의 모양을 객체의 모양에 맞추고, 필요하면 안쪽에서도 다른 이름을 붙입니다.

const userProfile = {
    id: 1,
    userInfo: {
        fullName: "Bob Johnson",
        email: "[email protected]"
    },
    preferences: {
        theme: "dark"
    }
};
const {
    userInfo: { fullName, email }, // userInfo 객체를 분해하여 fullName, email 추출
    preferences: { theme: userTheme } // preferences 객체를 분해하여 theme를 userTheme으로 추출
} = userProfile;
console.log(fullName);   // 결과: "Bob Johnson"
console.log(email);      // 결과: "[email protected]"
console.log(userTheme);  // 결과: "dark"

userInfo와 preferences는 안쪽으로 들어가기 위한 경로일 뿐이라 그 자체로는 변수가 만들어지지 않습니다. 패턴의 중간에 있는 객체가 없으면, 예를 들어 preferences가 undefined라면 그 안을 읽을 수 없어 TypeError가 납니다.

함수의 매개변수 자리에서 바로 구조 분해할 수도 있습니다. 객체를 인수로 받는 함수에서 특히 많이 씁니다.

// 객체 매개변수 구조 분해
function displayUser({ name, age }) {
    console.log(`이름: ${name}, 나이: ${age}`);
}
const myUser = { name: "철수", age: 25 };
displayUser(myUser); // 결과: 이름: 철수, 나이: 25
// 배열 매개변수 구조 분해
function printCoordinates([x, y]) {
    console.log(`X: ${x}, Y: ${y}`);
}
const point = [10, 20];
printCoordinates(point); // 결과: X: 10, Y: 20

기본값이 적용되는 조건

기본값은 값이 없거나 undefined일 때만 적용됩니다. null은 값이 있는 것으로 보므로 기본값으로 바뀌지 않습니다.

const { theme: first = "light" } = { theme: undefined };
const { theme: second = "light" } = { theme: null };
console.log(first, second);

출력은 light null입니다. null까지 기본값으로 바꿔야 한다면 구조 분해의 =만으로는 부족하고, const theme = value ?? "light"처럼 따로 처리해야 합니다. 또 구조 분해할 대상 자체가 null이나 undefined이면 속성을 읽을 수 없어 TypeError가 납니다.


스프레드 문법

스프레드 문법 ...은 배열이나 객체를 펼쳐 그 안의 값들을 다른 곳에 넣습니다. 배열 리터럴과 함수 호출에서 쓰는 스프레드는 이터레이터와 제너레이터에서 본 이터러블을 순회해 얻은 값을 하나씩 펼칩니다. 객체 리터럴에서 쓰는 스프레드는 대상 객체의 열거 가능한 자체 속성을 새 객체에 복사합니다. 모양은 같아도 규칙이 달라서, 일반 객체는 배열 스프레드로 펼칠 수 없습니다.

배열 스프레드

여러 배열을 하나로 합칠 수 있고, 사이사이에 다른 값을 끼워 넣을 수도 있습니다. 다음 코드에서 대괄호 안의 ...arr1은 그 자리에 1, 2를 적은 것과 같아서, 펼친 요소가 적은 순서대로 새 배열에 들어갑니다.

const arr1 = [1, 2];
const arr2 = [3, 4];
const combinedArr = [...arr1, ...arr2];
console.log(combinedArr); // 결과: [1, 2, 3, 4]
const newArr = [0, ...arr1, 5, ...arr2];
console.log(newArr); // 결과: [0, 1, 2, 5, 3, 4]

원본을 바꾸지 않고 배열의 사본을 만들 수 있습니다. 다만 이 사본은 얕은 복사입니다.

const original = [1, 2, 3];
const copy = [...original];
console.log(copy);              // [1, 2, 3]
console.log(original === copy); // false (다른 배열 객체)
copy.push(4);
console.log(original); // [1, 2, 3] (원본은 그대로)

얕은 복사이므로 [[1], [2]]처럼 배열 안에 배열이나 객체가 들어 있다면, 바깥 배열만 새로 만들어지고 안쪽 배열은 원본과 사본이 함께 가리킵니다.

요소를 추가하거나 끼워 넣은 새 배열을 만들 때도 씁니다. 다음 세 예는 모두 baseArray를 그대로 두고, 새로 만든 배열을 다른 변수에 담습니다.

const baseArray = [1, 2, 3];
const newArrayAtStart = [0, ...baseArray]; // 맨 앞에 추가
console.log(newArrayAtStart); // [0, 1, 2, 3]
const newArrayAtEnd = [...baseArray, 4]; // 맨 뒤에 추가
console.log(newArrayAtEnd); // [1, 2, 3, 4]
const newArrayAtMiddle = [...baseArray.slice(0, 2), 99, ...baseArray.slice(2)]; // 중간에 삽입
console.log(newArrayAtMiddle); // [1, 2, 99, 3]

중간에 끼워 넣는 코드는 slice()로 앞부분과 뒷부분을 나눈 뒤 사이에 값을 넣는 방식이라 조금 길어집니다. ES2023에 추가된 baseArray.toSpliced(2, 0, 99)를 쓰면 원본을 바꾸지 않고 같은 결과인 새 배열을 만들 수 있습니다.

문자열이나 Set 같은 이터러블을 배열로 바꿀 때도 스프레드를 씁니다.

const myString = "hello";
const charArray = [...myString];
console.log(charArray); // 결과: ["h", "e", "l", "l", "o"]
const mySet = new Set([1, 2, 3]);
const setToArray = [...mySet];
console.log(setToArray); // 결과: [1, 2, 3]

length와 숫자 키만 가진 유사 배열 객체(array-like object)는 이터러블이 아니라서 [...obj]로 펼치면 TypeError가 납니다. 이런 객체는 Array.from()으로 배열로 바꿉니다.

객체 스프레드

ES2018부터는 객체 리터럴에서도 스프레드를 쓸 수 있습니다. 여러 객체의 속성을 모아 새 객체를 만듭니다.

const user = { name: "John", age: 30 };
const address = { city: "Seoul", zip: "12345" };
const details = { job: "Developer" };
const fullProfile = { ...user, ...address, ...details };
console.log(fullProfile);
// 결과: { name: 'John', age: 30, city: 'Seoul', zip: '12345', job: 'Developer' }

속성은 스프레드를 적은 순서대로 새 객체에 복사되므로, 출력에서도 user, address, details의 속성이 그 순서로 나옵니다.

같은 이름의 속성이 있으면 뒤에 오는 객체의 값이 앞의 값을 덮어씁니다.

const objA = { a: 1, b: 2 };
const objB = { b: 3, c: 4 };
const mergedObj = { ...objA, ...objB };
console.log(mergedObj); // 결과: { a: 1, b: 3, c: 4 } (b가 덮어써짐)

b는 objA에서 먼저 2로 복사된 뒤 objB의 3으로 덮어써지며, 자리는 처음 복사된 위치에 그대로 남습니다.

배열처럼 원본을 바꾸지 않고 객체의 얕은 사본을 만들 수 있습니다.

const originalObj = { x: 1, y: 2 };
const copyObj = { ...originalObj };
console.log(copyObj);          // 결과: { x: 1, y: 2 }
console.log(originalObj === copyObj); // 결과: false
copyObj.x = 10;
console.log(originalObj.x); // 결과: 1 (원본 불변)

기존 객체를 바탕으로 일부 속성만 바꾸거나 새 속성을 더한 객체를 만들 때 특히 많이 씁니다. 스프레드 뒤에 적은 속성이 덮어쓰므로, 바꿀 속성은 스프레드 다음에 적습니다.

const product = { id: 1, name: "Laptop", price: 1200 };
const updatedProduct = { ...product, price: 1100, category: "Electronics" };
console.log(updatedProduct);
// 결과: { id: 1, name: 'Laptop', price: 1100, category: 'Electronics' }

더 깊이: 객체 스프레드와 Object.assign은 무엇이 다를까

객체 스프레드가 생기기 전에는 Object.assign({}, a, b)로 같은 일을 했고, 결과도 대개 같습니다. 둘 다 원본의 자체 속성 가운데 열거할 수 있는 것만 옮기고, 프로토타입은 복사하지 않습니다. 차이는 속성을 새 객체에 넣는 방식에 있습니다. Object.assign()은 대상 객체에 값을 대입하므로 대상에 setter가 있으면 그 setter를 실행하지만, 스프레드는 새 객체에 속성을 바로 정의하므로 setter를 부르지 않습니다.

예를 들어 foo에 setter가 있는 객체를 대상으로 Object.assign(대상, { foo: 1 })을 실행하면 setter가 1을 받아 실행되고, foo는 여전히 setter인 채로 남습니다. 같은 객체 리터럴 안에서 setter 뒤에 ...{ foo: 1 }을 적으면 setter는 실행되지 않고 foo는 값이 1인 일반 속성으로 바뀝니다. 그래서 이미 있는 객체에 값을 써 넣으면서 setter의 검사나 기록을 거쳐야 한다면 Object.assign()을, 새 객체를 만들 뿐이라면 스프레드를 씁니다.

원본에 getter가 있는 경우도 기억해 둡니다. 스프레드는 원본의 속성 값을 읽어 옮기므로, getter는 복사하는 순간 실행되고 그 결과 값만 일반 속성으로 들어갑니다. 차이점은 MDN의 스프레드 문법 설명에 정리되어 있습니다.

함수 호출의 스프레드

함수를 호출할 때 배열의 요소를 각각의 인수로 펼쳐 넘길 수 있습니다. 매개변수 자리에서 인수를 배열로 모으는 나머지 매개변수와는 반대 방향입니다.

function sum(a, b, c) {
    return a + b + c;
}

const numbersToSum = [1, 2, 3];
const result = sum(...numbersToSum); // 배열 요소를 개별 인자로 펼쳐서 전달
console.log(result); // 결과: 6

더 깊이: 아주 긴 배열을 인수로 펼치면

Math.max(...numbers)처럼 배열을 펼쳐 넘기는 코드는 짧고 읽기 쉽지만, 배열의 길이만큼 인수가 생긴다는 점을 잊기 쉽습니다. 함수 하나에 넘길 수 있는 인수의 개수에는 엔진마다 한도가 있고, MDN에 따르면 Safari의 JavaScriptCore 엔진은 그 한도가 65536으로 고정되어 있습니다. 명세는 이 한도를 넘었을 때의 동작을 정하지 않으므로, 대부분의 엔진은 예외를 던지지만 인수 일부만 넘기는 엔진이 있어도 명세를 어긴 것은 아닙니다.

그래서 수만 개를 넘을 수 있는 배열에는 펼치기를 쓰지 않습니다. 최댓값이나 합계를 구한다면 reduce()나 반복문으로 하나씩 처리하고, 꼭 펼쳐야 한다면 배열을 일정한 크기로 잘라 여러 번 호출한 뒤 결과를 합칩니다. 개발할 때의 작은 데이터로는 문제가 드러나지 않다가, 실제 데이터가 커진 뒤에야 오류가 날 수 있으므로 미리 점검해 둘 만합니다. 자세한 내용은 MDN의 apply() 설명에 있습니다.


새로 만들어지는 것과 공유되는 것

앞의 예제들은 원시값만 담은 배열과 객체라서, 새로 만든 쪽을 바꿔도 원본이 그대로였습니다. 그런데 속성 값으로 다른 객체가 들어 있다면 이야기가 달라집니다.

구조 분해와 spread의 복사 경계 — 상태 추적

원본/구조 분해/rest/spread가 같은 settings를 가리켜 내부 theme 변경을 공유한다. 별개의 연산을 직렬이라고 주장하지 않고, 준비된 참조 전체에서 변경 전후 관찰값을 비교하는 trace가 유용하다.

구조 분해와 spread의 복사 경계

REFERENCE GRAPH

구조 분해와 spread의 복사 경계

구조 분해는 값을 이름에 연결한다. rest와 spread는 새 바깥 객체·배열을 만들지만, 그 안의 객체 참조는 공유한다.

새 바깥 구조와 공유된 내부 객체원본 source의 settings 프로퍼티, 구조 분해로 선택한 settings 이름, name을 제외한 rest 새 객체, spread 새 객체가 하나의 내부 settings 객체를 참조합니다. 네 연산은 같은 원본에 별도로 적용한 비교이며 실행 순서를 나타내지 않습니다. rest와 spread는 새 바깥 객체를 만들고, 구조 분해는 컨테이너를 복사하지 않습니다. 모바일도 하나의 내부 객체로 향하는 네 참조와 별개 바깥 영역을 유지합니다. 배열 spread도 내부 객체를 공유하지만 그림은 객체 settings 사례에 한정합니다. 별개 연산을 같은 원본에 적용한 관계 비교같은 settings 객체theme: "dark"내부 변경을 공유원본 sourcesettings 프로퍼티구조 분해 이름const {settings} = source새 객체 복사 없음rest 바깥 객체const {name, ...rest} = source새 바깥 객체spread 바깥 객체const copy = {...source}새 바깥 객체각 화살표는 내부 객체 참조 · 연산 실행 순서가 아님
새 바깥 구조와 공유된 내부 객체원본 source의 settings 프로퍼티, 구조 분해로 선택한 settings 이름, name을 제외한 rest 새 객체, spread 새 객체가 하나의 내부 settings 객체를 참조합니다. 네 연산은 같은 원본에 별도로 적용한 비교이며 실행 순서를 나타내지 않습니다. rest와 spread는 새 바깥 객체를 만들고, 구조 분해는 컨테이너를 복사하지 않습니다. 모바일도 하나의 내부 객체로 향하는 네 참조와 별개 바깥 영역을 유지합니다. 배열 spread도 내부 객체를 공유하지만 그림은 객체 settings 사례에 한정합니다.같은 원본으로 별개 연산같은 settings내부 객체 하나변경을 공유원본 sourcesettings 프로퍼티구조 분해 이름새 객체 없음rest 바깥 객체새 바깥 객체spread 바깥 객체새 바깥 객체네 참조 모두 같은 내부 객체새 바깥 구조 ≠ 깊은 복사

const {name} = source

새 container 만들지 않음

nested identity name 값이 object면 같은 nested reference

const {skip, ...rest} = source

새 container 새 object outer

nested identity nested object는 원본과 공유

const copy = {...source}

새 container 새 object outer

nested identity enumerable own property를 얕게 복사하며 내부 객체 참조는 공유

const copy = [...source]

새 container 새 array outer

nested identity element object identity는 공유

  • 연산별 참조 공유 비교
  • rest·spread의 새 바깥 구조

새 바깥 구조 · 같은 내부 객체

rest와 spread의 결과는 원본과 다른 객체·배열이다. 내부 객체를 변경하면 공유된 참조를 통해 원본에서도 그 변경이 보일 수 있다.

destructuring은 container 복사 문법이 아니며, rest와 spread도 기본적으로 outer만 새로 만든다. nested graph까지 독립시키지는 않는다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 설명새 container: 만들지 않음
원도표 설명nested identity: name 값이 object이면 같은 nested reference
원도표 설명새 container: 새 object outer
원도표 설명nested identity: nested object는 원본과 공유
원도표 설명nested identity: enumerable own property를 얕게 복사하며 내부 객체 참조는 공유
원도표 설명새 container: 새 array outer
원도표 설명nested identity: element object identity는 공유
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
공유 입력outer: 서로 다른 결과 가능 / settings: 같은 참조source와 독립 복사 결과의 settings는 같은 내부 객체다rest/spread는 내부 객체의 깊은 복사가 아니다
내부 변경settings.theme: dark / 독립 outer: 그대로한 경로에서 settings.theme을 바꾼다공유 참조를 통한 변경이다
공유 결과관찰: 모든 공유 settings dark / 생성 동작: 서로 독립적인 사례다른 경로도 theme:dark를 읽는다여러 복사 문법을 반드시 순차 실행해야 한다는 뜻은 아니다

구조 분해와 spread의 복사 경계 — 상태 추적

원본/구조 분해/rest/spread가 같은 settings를 가리켜 내부 theme 변경을 공유한다. 별개의 연산을 직렬이라고 주장하지 않고, 준비된 참조 전체에서 변경 전후 관찰값을 비교하는 trace가 유용하다.

구조 분해와 spread의 복사 경계

REFERENCE GRAPH

구조 분해와 spread의 복사 경계

구조 분해는 값을 이름에 연결한다. rest와 spread는 새 바깥 객체·배열을 만들지만, 그 안의 객체 참조는 공유한다.

새 바깥 구조와 공유된 내부 객체원본 source의 settings 프로퍼티, 구조 분해로 선택한 settings 이름, name을 제외한 rest 새 객체, spread 새 객체가 하나의 내부 settings 객체를 참조합니다. 네 연산은 같은 원본에 별도로 적용한 비교이며 실행 순서를 나타내지 않습니다. rest와 spread는 새 바깥 객체를 만들고, 구조 분해는 컨테이너를 복사하지 않습니다. 모바일도 하나의 내부 객체로 향하는 네 참조와 별개 바깥 영역을 유지합니다. 배열 spread도 내부 객체를 공유하지만 그림은 객체 settings 사례에 한정합니다. 별개 연산을 같은 원본에 적용한 관계 비교같은 settings 객체theme: "dark"내부 변경을 공유원본 sourcesettings 프로퍼티구조 분해 이름const {settings} = source새 객체 복사 없음rest 바깥 객체const {name, ...rest} = source새 바깥 객체spread 바깥 객체const copy = {...source}새 바깥 객체각 화살표는 내부 객체 참조 · 연산 실행 순서가 아님
새 바깥 구조와 공유된 내부 객체원본 source의 settings 프로퍼티, 구조 분해로 선택한 settings 이름, name을 제외한 rest 새 객체, spread 새 객체가 하나의 내부 settings 객체를 참조합니다. 네 연산은 같은 원본에 별도로 적용한 비교이며 실행 순서를 나타내지 않습니다. rest와 spread는 새 바깥 객체를 만들고, 구조 분해는 컨테이너를 복사하지 않습니다. 모바일도 하나의 내부 객체로 향하는 네 참조와 별개 바깥 영역을 유지합니다. 배열 spread도 내부 객체를 공유하지만 그림은 객체 settings 사례에 한정합니다.같은 원본으로 별개 연산같은 settings내부 객체 하나변경을 공유원본 sourcesettings 프로퍼티구조 분해 이름새 객체 없음rest 바깥 객체새 바깥 객체spread 바깥 객체새 바깥 객체네 참조 모두 같은 내부 객체새 바깥 구조 ≠ 깊은 복사

const {name} = source

새 container 만들지 않음

nested identity name 값이 object면 같은 nested reference

const {skip, ...rest} = source

새 container 새 object outer

nested identity nested object는 원본과 공유

const copy = {...source}

새 container 새 object outer

nested identity enumerable own property를 얕게 복사하며 내부 객체 참조는 공유

const copy = [...source]

새 container 새 array outer

nested identity element object identity는 공유

  • 연산별 참조 공유 비교
  • rest·spread의 새 바깥 구조

새 바깥 구조 · 같은 내부 객체

rest와 spread의 결과는 원본과 다른 객체·배열이다. 내부 객체를 변경하면 공유된 참조를 통해 원본에서도 그 변경이 보일 수 있다.

destructuring은 container 복사 문법이 아니며, rest와 spread도 기본적으로 outer만 새로 만든다. nested graph까지 독립시키지는 않는다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 설명새 container: 만들지 않음
원도표 설명nested identity: name 값이 object이면 같은 nested reference
원도표 설명새 container: 새 object outer
원도표 설명nested identity: nested object는 원본과 공유
원도표 설명nested identity: enumerable own property를 얕게 복사하며 내부 객체 참조는 공유
원도표 설명새 container: 새 array outer
원도표 설명nested identity: element object identity는 공유
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
공유 입력outer: 서로 다른 결과 가능 / settings: 같은 참조source와 독립 복사 결과의 settings는 같은 내부 객체다rest/spread는 내부 객체의 깊은 복사가 아니다
내부 변경settings.theme: dark / 독립 outer: 그대로한 경로에서 settings.theme을 바꾼다공유 참조를 통한 변경이다
공유 결과관찰: 모든 공유 settings dark / 생성 동작: 서로 독립적인 사례다른 경로도 theme:dark를 읽는다여러 복사 문법을 반드시 순차 실행해야 한다는 뜻은 아니다

위 그림은 settings 속성에 { theme: "dark" } 객체를 가진 원본 source에 네 가지 연산을 따로 적용한 결과를 비교합니다. 원본의 settings 속성, const { settings } = source로 구조 분해한 이름, name을 빼고 모은 나머지 객체 rest의 settings, { ...source }로 만든 스프레드 객체의 settings가 모두 하나의 같은 내부 객체를 가리킵니다. 네 연산은 차례로 실행되는 단계가 아니라 같은 원본에 각각 적용한 비교입니다.

나머지 객체와 스프레드 객체는 원본과 다른 새 바깥 객체입니다. 하지만 그 안의 객체는 복사되지 않고 참조만 복사되므로, 한쪽에서 settings.theme을 바꾸면 원본에서도 바뀐 값이 보입니다. 구조 분해는 아예 새 객체를 만들지 않고 같은 내부 객체에 이름만 하나 더 붙입니다. 그림 아래의 설명처럼 배열 스프레드도 바깥 배열만 새로 만들 뿐, 요소로 든 객체는 원본과 공유합니다.

다음 코드로 바깥 객체와 안쪽 객체를 따로 확인해 봅니다.

const originalSettings = { name: "원본", settings: { theme: "dark" } };
const { settings } = originalSettings;
const copiedSettings = { ...originalSettings, name: "복사본" };
copiedSettings.settings.theme = "light";
console.log(originalSettings === copiedSettings);
console.log(settings === copiedSettings.settings);
console.log(originalSettings.name, originalSettings.settings.theme);

출력은 false, true, 원본 light입니다. 스프레드는 새 바깥 객체를 만들었으므로 originalSettings === copiedSettings는 false이고, name을 바꿔도 원본의 name은 "원본" 그대로입니다. 하지만 settings 속성은 같은 내부 객체의 참조라서, 구조 분해한 settings와 copiedSettings.settings가 같은 객체이고, 복사본에서 theme을 바꾸자 원본의 theme도 light가 되었습니다. ===는 메모리 주소가 아니라 같은 객체인지를 비교한다는 점도 함께 기억해 두세요.


정리

  • 배열 구조 분해는 순서로, 객체 구조 분해는 속성 이름으로 값을 꺼내며, 객체에서는 속성: 변수로 다른 이름을 붙일 수 있습니다.
  • 기본값은 값이 없거나 undefined일 때만 쓰이고, null은 그대로 남으며, 대상이 null이나 undefined면 TypeError가 납니다.
  • 나머지 요소와 나머지 속성 ...는 꺼내고 남은 값을 새 배열이나 새 객체로 모읍니다.
  • 배열과 함수 호출의 스프레드는 이터러블을 순회해 펼치고, 객체 스프레드는 열거 가능한 자체 속성을 복사하며 뒤의 속성이 앞을 덮어씁니다.
  • 스프레드와 나머지 패턴은 바깥 배열이나 객체만 새로 만드는 얕은 복사라서, 안쪽 객체는 원본과 공유합니다.
  • 구조 분해는 객체를 복사하지 않고 같은 값에 이름을 붙일 뿐입니다.

연습 문제

1. 안쪽 객체까지 복사하기

마지막 예제의 복사 선언을 const copiedSettings = { ...originalSettings, settings: { ...originalSettings.settings }, name: "복사본" };으로 바꾸고 처음부터 실행합니다. 세 출력은 어떻게 달라질까요? 이 방법이 모든 깊이의 복사를 보장할까요?

해설 보기

false, false, 원본 dark입니다. settings 객체도 스프레드로 새로 만들었으므로, 구조 분해한 원본의 settings와 복사본의 settings는 다른 객체이고, 복사본의 theme을 바꿔도 원본은 dark로 남습니다.

하지만 이 방법은 직접 펼친 단계까지만 새로 만듭니다. settings 안에 또 다른 객체가 있었다면 그 객체는 여전히 공유되므로, 모든 깊이를 복사한다고 생각하면 안 됩니다.

2. 매개변수의 기본값

다음 세 호출은 각각 어떻게 될까요?

function connect({ host = "localhost", port = 3000 } = {}) {
    console.log(`${host}:${port}`);
}

connect();
connect({ port: 8080 });
connect(null);
해설 보기

첫 호출은 localhost:3000, 두 번째는 localhost:8080을 출력하고, 세 번째는 TypeError가 납니다.

인수 없이 호출하면 매개변수가 undefined라서 = {} 기본값이 쓰이고, 빈 객체에서 두 속성 모두 기본값을 씁니다. 두 번째 호출은 port만 넘겼으므로 host만 기본값을 씁니다. null은 undefined가 아니라서 = {}가 적용되지 않고, null을 구조 분해하려다 오류가 납니다.

3. 스프레드의 순서

const defaults = { theme: "dark", size: "m" };와 const options = { theme: "light" };가 있습니다. { ...defaults, ...options }와 { ...options, ...defaults }의 결과는 각각 무엇일까요? 사용자의 설정이 기본값보다 우선해야 한다면 어느 쪽을 써야 할까요?

해설 보기

{ ...defaults, ...options }는 { theme: "light", size: "m" }이고, { ...options, ...defaults }는 { theme: "dark", size: "m" }입니다. 같은 이름의 속성은 뒤에 펼친 객체의 값이 남습니다.

사용자 설정이 우선해야 하므로 기본값을 먼저, 사용자 설정을 나중에 펼치는 { ...defaults, ...options }를 씁니다. 사용자가 정하지 않은 size는 기본값에서 채워집니다.

기본값이 적용되는 조건과 구조 분해의 대상 규칙은 MDN의 구조 분해 할당 문서에서, 펼치는 위치에 따른 규칙은 MDN의 스프레드 문법 문서에서 확인할 수 있습니다.

다음 글 Symbol, Map, Set에서는 겹치지 않는 고유한 값을 만드는 Symbol과, 객체와 배열만으로 다루기 불편했던 데이터를 담는 Map과 Set을 살펴봅니다.