{ ...original }로 객체를 복사했는데, 복사본의 중첩 객체를 바꾸니 원본까지 바뀌었습니다. 왜 이런 일이 생길까요?

스프레드 연산자는 1단계 깊이만 복사하는 얕은 복사 를 수행합니다. 구조 분해와 스프레드는 편리하지만, 동작 방식의 한계를 모르면 의도치 않은 참조 공유 버그를 만들게 됩니다.

배열 구조 분해

배열의 요소를 개별 변수로 추출합니다.

JS
const colors = ['빨강', '파랑', '초록'];
const [first, second, third] = colors;

console.log(first);  // '빨강'
console.log(second); // '파랑'
console.log(third);  // '초록'

요소 건너뛰기

JS
const [a, , c] = [1, 2, 3];
console.log(a, c); // 1 3

기본값

JS
const [x = 10, y = 20] = [1];
console.log(x); // 1 — 값이 있으므로 기본값 무시
console.log(y); // 20 — 값이 없으므로 기본값 사용

기본값은 해당 위치의 값이 undefined일 때만 적용됩니다. null은 적용되지 않습니다.

JS
const [a = 10] = [null];
console.log(a); // null — undefined가 아니므로 기본값 미적용

변수 교환

임시 변수 없이 두 변수의 값을 교환할 수 있습니다.

JS
let a = 1;
let b = 2;

[a, b] = [b, a];
console.log(a, b); // 2 1

나머지 요소 (rest)

JS
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]

...rest는 항상 ** 마지막에** 위치해야 합니다.

객체 구조 분해

객체의 프로퍼티를 변수로 추출합니다.

JS
const user = { name: 'Alice', age: 25, role: 'developer' };
const { name, age } = user;

console.log(name); // 'Alice'
console.log(age);  // 25

변수명 변경

JS
const { name: userName, age: userAge } = user;
console.log(userName); // 'Alice'
console.log(userAge);  // 25

기본값 + 변수명 변경

JS
const { name: userName = '익명', email = 'none' } = { name: 'Alice' };
console.log(userName); // 'Alice'
console.log(email);    // 'none'

중첩 구조 분해

JS
const response = {
  data: {
    user: {
      name: 'Alice',
      address: {
        city: '서울'
      }
    }
  }
};

const {
  data: {
    user: {
      name,
      address: { city }
    }
  }
} = response;

console.log(name); // 'Alice'
console.log(city); // '서울'

중첩이 깊어지면 가독성이 떨어지므로, 2~3단계까지만 쓰는 것이 좋습니다.

나머지 프로퍼티 (rest)

JS
const { name, ...rest } = { name: 'Alice', age: 25, role: 'developer' };
console.log(name); // 'Alice'
console.log(rest); // { age: 25, role: 'developer' }

특정 프로퍼티를 제거한 새 객체를 만들 때 유용합니다.

JS
// password를 제외한 객체 생성
const user = { name: 'Alice', email: 'alice@example.com', password: '1234' };
const { password, ...safeUser } = user;
console.log(safeUser); // { name: 'Alice', email: 'alice@example.com' }

함수 매개변수에서의 구조 분해

JS
// 객체 매개변수 구조 분해
function createUser({ name, age, role = 'user' }) {
  return { name, age, role };
}

const user = createUser({ name: 'Alice', age: 25 });
console.log(user); // { name: 'Alice', age: 25, role: 'user' }
JS
// 배열 매개변수 구조 분해
function getFirst([first, ...rest]) {
  return { first, rest };
}

console.log(getFirst([1, 2, 3])); // { first: 1, rest: [2, 3] }

실무에서 옵션 객체를 받는 함수에서 자주 사용합니다.

JS
// React 컴포넌트에서의 props 구조 분해
function UserCard({ name, age, onDelete }) {
  return (
    // JSX에서 바로 사용
    <div>{name} ({age}세)</div>
  );
}

Spread Operator (전개 연산자)

...이 ** 할당의 오른쪽 **(또는 함수 호출)에서 사용되면 spread입니다.

배열 스프레드

JS
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// 배열 합치기
const merged = [...arr1, ...arr2];
console.log(merged); // [1, 2, 3, 4, 5, 6]

// 배열 복사 (얕은 복사)
const copy = [...arr1];
console.log(copy); // [1, 2, 3]
console.log(copy === arr1); // false — 다른 배열

객체 스프레드

JS
const defaults = { theme: 'light', fontSize: 14, lang: 'ko' };
const userSettings = { theme: 'dark', fontSize: 16 };

// 객체 병합 — 뒤에 오는 값이 우선
const settings = { ...defaults, ...userSettings };
console.log(settings);
// { theme: 'dark', fontSize: 16, lang: 'ko' }

함수 호출에서의 스프레드

JS
const numbers = [5, 2, 8, 1, 9];
console.log(Math.max(...numbers)); // 9

// apply를 대체
// ES5: Math.max.apply(null, numbers)
// ES6: Math.max(...numbers)

Rest Parameter vs Spread Operator

문법은 같은 ...이지만 역할이 다릅니다.

구분Rest ParameterSpread Operator
위치함수 매개변수, 구조 분해 좌변함수 호출, 배열/객체 리터럴
역할여러 요소를 하나로 ** 모음**하나를 여러 요소로 ** 펼침**
결과배열개별 요소
JS
// Rest — 여러 인자를 배열로 모음
function sum(...numbers) {
  return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3)); // 6

// Spread — 배열을 개별 인자로 펼침
const args = [1, 2, 3];
console.log(sum(...args)); // 6

rest vs arguments

JS
// arguments — 유사 배열 (Array 메서드 사용 불가)
function oldWay() {
  console.log(Array.isArray(arguments)); // false
  // arguments.map(...) — TypeError
}

// rest — 진짜 배열
function newWay(...args) {
  console.log(Array.isArray(args)); // true
  return args.map((x) => x * 2); // 정상 동작
}

화살표 함수에서는 arguments를 사용할 수 없으므로, rest parameter가 유일한 방법입니다.

얕은 복사의 한계

스프레드 연산자로 객체나 배열을 복사하면 1단계 깊이만 복사 됩니다. 중첩 객체는 참조가 공유됩니다.

JS
const original = {
  name: 'Alice',
  address: {
    city: '서울',
    zip: '12345'
  }
};

const copy = { ...original };

// 1단계 — 독립적
copy.name = 'Bob';
console.log(original.name); // 'Alice' — 영향 없음

// 2단계 — 참조 공유!
copy.address.city = '부산';
console.log(original.address.city); // '부산' — 원본도 변경됨!

깊은 복사 방법들

JSON.parse + JSON.stringify

JS
const deep1 = JSON.parse(JSON.stringify(original));

간단하지만 한계가 있습니다.

  • undefined, function, Symbol → 누락
  • Date → 문자열로 변환
  • Map, Set → 빈 객체로 변환
  • 순환 참조 → TypeError

structuredClone (권장)

JS
const deep2 = structuredClone(original);

deep2.address.city = '대전';
console.log(original.address.city); // '서울' — 원본 유지

structuredClone은 2022년부터 모든 주요 브라우저와 Node.js에서 지원합니다.

**지원하는 것 **: 중첩 객체, 배열, Date, Map, Set, ArrayBuffer, 순환 참조 등 ** 지원하지 않는 것 **: 함수, DOM 노드, Symbol, Error 객체

JS
// 순환 참조도 처리 가능
const obj = { name: 'test' };
obj.self = obj;

const cloned = structuredClone(obj); // 정상 동작
console.log(cloned.self === cloned); // true

// 함수는 복사 불가
structuredClone({ fn: () => {} }); // DataCloneError

실무 패턴

조건부 프로퍼티 추가

JS
const user = {
  name: 'Alice',
  ...(isAdmin && { role: 'admin' }),
  ...(email && { email })
};

조건이 falsy면 false가 스프레드되는데, ...false는 아무것도 추가하지 않습니다.

불변 상태 업데이트 (React)

JS
// 배열에 요소 추가
const newItems = [...items, newItem];

// 배열에서 요소 제거
const filtered = items.filter((item) => item.id !== targetId);

// 객체 프로퍼티 업데이트
const updatedUser = { ...user, name: '새이름' };

// 중첩 객체 업데이트
const updatedState = {
  ...state,
  user: {
    ...state.user,
    address: {
      ...state.user.address,
      city: '부산'
    }
  }
};

중첩이 깊어지면 코드가 복잡해집니다. 이런 경우 Immer 같은 라이브러리를 고려할 수 있습니다.

배열 메서드와의 조합

JS
// 배열에서 최댓값/최솟값
const max = Math.max(...[5, 2, 8, 1]);

// 유니크 배열
const unique = [...new Set([1, 2, 2, 3, 3])];
console.log(unique); // [1, 2, 3]

// 문자열을 배열로
const chars = [..."안녕하세요"];
console.log(chars); // ['안', '녕', '하', '세', '요']

주의할 점

스프레드 복사는 얕은 복사

{ ...obj }[...arr]은 1단계 깊이만 복사합니다. 중첩 객체는 참조가 공유되므로, 복사본의 중첩 프로퍼티를 수정하면 원본도 변경됩니다. 깊은 복사가 필요하면 structuredClone을 사용합니다.

구조 분해의 기본값은 undefined일 때만 적용

const [a = 10] = [null]에서 a10이 아니라 null입니다. 기본값은 해당 위치의 값이 undefined일 때만 적용되며, null은 적용되지 않습니다. 이 동작은 함수 매개변수의 기본값에서도 동일합니다.

중첩 구조 분해의 가독성 저하

구조 분해가 3단계 이상 중첩되면 코드가 오히려 읽기 어려워집니다. 2단계까지만 사용하고, 그 이상은 단계별로 분리하는 것이 좋습니다.

정리

항목설명
구조 분해배열/객체에서 값을 추출하는 선언적 문법
... 좌변 (rest)여러 요소를 하나의 배열로 모음
... 우변 (spread)하나를 여러 요소로 펼침
얕은 복사 한계1단계만 복사, 중첩 객체는 참조 공유
깊은 복사structuredClone 권장 (함수/DOM/Symbol 제외)
rest vs argumentsrest는 진짜 배열, arguments는 유사 배열
댓글 로딩 중...