구조 분해와 스프레드 — 코드를 짧게 쓰는 것 이상의 의미
{ ...original }로 객체를 복사했는데, 복사본의 중첩 객체를 바꾸니 원본까지 바뀌었습니다. 왜 이런 일이 생길까요?
스프레드 연산자는 1단계 깊이만 복사하는 얕은 복사 를 수행합니다. 구조 분해와 스프레드는 편리하지만, 동작 방식의 한계를 모르면 의도치 않은 참조 공유 버그를 만들게 됩니다.
배열 구조 분해
배열의 요소를 개별 변수로 추출합니다.
const colors = ['빨강', '파랑', '초록'];
const [first, second, third] = colors;
console.log(first); // '빨강'
console.log(second); // '파랑'
console.log(third); // '초록'
요소 건너뛰기
const [a, , c] = [1, 2, 3];
console.log(a, c); // 1 3
기본값
const [x = 10, y = 20] = [1];
console.log(x); // 1 — 값이 있으므로 기본값 무시
console.log(y); // 20 — 값이 없으므로 기본값 사용
기본값은 해당 위치의 값이 undefined일 때만 적용됩니다. null은 적용되지 않습니다.
const [a = 10] = [null];
console.log(a); // null — undefined가 아니므로 기본값 미적용
변수 교환
임시 변수 없이 두 변수의 값을 교환할 수 있습니다.
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
나머지 요소 (rest)
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
...rest는 항상 ** 마지막에** 위치해야 합니다.
객체 구조 분해
객체의 프로퍼티를 변수로 추출합니다.
const user = { name: 'Alice', age: 25, role: 'developer' };
const { name, age } = user;
console.log(name); // 'Alice'
console.log(age); // 25
변수명 변경
const { name: userName, age: userAge } = user;
console.log(userName); // 'Alice'
console.log(userAge); // 25
기본값 + 변수명 변경
const { name: userName = '익명', email = 'none' } = { name: 'Alice' };
console.log(userName); // 'Alice'
console.log(email); // 'none'
중첩 구조 분해
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)
const { name, ...rest } = { name: 'Alice', age: 25, role: 'developer' };
console.log(name); // 'Alice'
console.log(rest); // { age: 25, role: 'developer' }
특정 프로퍼티를 제거한 새 객체를 만들 때 유용합니다.
// password를 제외한 객체 생성
const user = { name: 'Alice', email: 'alice@example.com', password: '1234' };
const { password, ...safeUser } = user;
console.log(safeUser); // { name: 'Alice', email: 'alice@example.com' }
함수 매개변수에서의 구조 분해
// 객체 매개변수 구조 분해
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' }
// 배열 매개변수 구조 분해
function getFirst([first, ...rest]) {
return { first, rest };
}
console.log(getFirst([1, 2, 3])); // { first: 1, rest: [2, 3] }
실무에서 옵션 객체를 받는 함수에서 자주 사용합니다.
// React 컴포넌트에서의 props 구조 분해
function UserCard({ name, age, onDelete }) {
return (
// JSX에서 바로 사용
<div>{name} ({age}세)</div>
);
}
Spread Operator (전개 연산자)
...이 ** 할당의 오른쪽 **(또는 함수 호출)에서 사용되면 spread입니다.
배열 스프레드
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 — 다른 배열
객체 스프레드
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' }
함수 호출에서의 스프레드
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 Parameter | Spread Operator |
|---|---|---|
| 위치 | 함수 매개변수, 구조 분해 좌변 | 함수 호출, 배열/객체 리터럴 |
| 역할 | 여러 요소를 하나로 ** 모음** | 하나를 여러 요소로 ** 펼침** |
| 결과 | 배열 | 개별 요소 |
// 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
// 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단계 깊이만 복사 됩니다. 중첩 객체는 참조가 공유됩니다.
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
const deep1 = JSON.parse(JSON.stringify(original));
간단하지만 한계가 있습니다.
undefined,function,Symbol→ 누락Date→ 문자열로 변환Map,Set→ 빈 객체로 변환- 순환 참조 → TypeError
structuredClone (권장)
const deep2 = structuredClone(original);
deep2.address.city = '대전';
console.log(original.address.city); // '서울' — 원본 유지
structuredClone은 2022년부터 모든 주요 브라우저와 Node.js에서 지원합니다.
**지원하는 것 **: 중첩 객체, 배열, Date, Map, Set, ArrayBuffer, 순환 참조 등
** 지원하지 않는 것 **: 함수, DOM 노드, Symbol, Error 객체
// 순환 참조도 처리 가능
const obj = { name: 'test' };
obj.self = obj;
const cloned = structuredClone(obj); // 정상 동작
console.log(cloned.self === cloned); // true
// 함수는 복사 불가
structuredClone({ fn: () => {} }); // DataCloneError
실무 패턴
조건부 프로퍼티 추가
const user = {
name: 'Alice',
...(isAdmin && { role: 'admin' }),
...(email && { email })
};
조건이 falsy면 false가 스프레드되는데, ...false는 아무것도 추가하지 않습니다.
불변 상태 업데이트 (React)
// 배열에 요소 추가
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 같은 라이브러리를 고려할 수 있습니다.
배열 메서드와의 조합
// 배열에서 최댓값/최솟값
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]에서 a는 10이 아니라 null입니다. 기본값은 해당 위치의 값이 undefined일 때만 적용되며, null은 적용되지 않습니다. 이 동작은 함수 매개변수의 기본값에서도 동일합니다.
중첩 구조 분해의 가독성 저하
구조 분해가 3단계 이상 중첩되면 코드가 오히려 읽기 어려워집니다. 2단계까지만 사용하고, 그 이상은 단계별로 분리하는 것이 좋습니다.
정리
| 항목 | 설명 |
|---|---|
| 구조 분해 | 배열/객체에서 값을 추출하는 선언적 문법 |
... 좌변 (rest) | 여러 요소를 하나의 배열로 모음 |
... 우변 (spread) | 하나를 여러 요소로 펼침 |
| 얕은 복사 한계 | 1단계만 복사, 중첩 객체는 참조 공유 |
| 깊은 복사 | structuredClone 권장 (함수/DOM/Symbol 제외) |
| rest vs arguments | rest는 진짜 배열, arguments는 유사 배열 |