자바스크립트 중급 #4 디스트럭처링과 spread/rest 깊이
기초 #5 객체와 배열 에서 디스트럭처링과 spread를 가볍게 봤습니다. 이번 글은 그 도구들을 실전에서 자주 만나는 패턴들로 깊이 들어갑니다.
객체 디스트럭처링 — 기본 + 응용 #
const user = { id: 'u1', name: '커티스', age: 30 };
// 기본
const { name, age } = user;
// 이름 바꿔서
const { name: userName } = user;
// 기본값
const { email = '없음' } = user;
// 이름 바꾸기 + 기본값
const { phone: userPhone = '미입력' } = user;특히 마지막 패턴이 처음에는 헷갈려요 — phone 키를 userPhone으로 받되, 없으면 '미입력'. 옵션 객체를 받는 함수에서 자주 등장합니다.
중첩 객체 풀기 #
const response = {
data: {
user: {
id: 'u1',
profile: {
name: '커티스',
age: 30,
},
},
},
};
const {
data: {
user: {
profile: { name, age },
},
},
} = response;
console.log(name, age); // 커티스 30
깊이 들어간 곳의 값을 한 줄로 끌어옵니다. 중간 변수(data, user)는 만들지 않고 필요한 leaf 값만 가져옵니다.
다만 너무 깊게 쓰면 읽기 힘듭니다. 중첩이 두 단계 이상 깊으면 두세 단계로 나눠서 풀거나 옵셔널 체이닝(#5)을 쓰는 게 보통 더 명확합니다.
배열 디스트럭처링 — 패턴들 #
const arr = [1, 2, 3, 4, 5];
const [a, b] = arr; // a=1, b=2
const [, , c] = arr; // c=3 (앞 둘 건너뛰기)
const [first, ...rest] = arr; // first=1, rest=[2,3,4,5]
const [x = 10] = []; // x=10 (기본값)
배열은 순서가 기준이라 — 디스트럭처링도 순서대로 받아옵니다.
Swap — 두 변수 값 바꾸기 #
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
임시 변수 없이 한 줄에 끝내는 관용구. 옛날에는 let temp = a; a = b; b = temp; 였습니다.
매개변수 디스트럭처링 — 가장 자주 쓰는 패턴 #
함수의 인자에서 디스트럭처링을 쓰는 건 모던 자바스크립트의 가장 흔한 관용구입니다.
function createUser({ name, age, email = '미입력' }) {
console.log(name, age, email);
}
createUser({ name: '커티스', age: 30 });
// 커티스 30 미입력
이 패턴의 장점:
- 호출하는 쪽에서 키 이름이 보임 — 인자 순서를 외울 필요 없음
- 선택 인자를 키와 기본값으로 표현
- 새 인자 추가에 호환성 유지 — 기존 호출이 깨지지 않음
리액트 컴포넌트의 props가 정확히 이 패턴입니다.
빈 객체로 호출 가능하게 #
function init({ debug = false, retries = 3 } = {}) {
console.log(debug, retries);
}
init(); // false 3 — 인자 없이도 OK
init({ debug: true }); // true 3
매개변수 자체에 = {}를 두면 인자를 안 줘도 빈 객체로 시작해서 디스트럭처링이 안전해져요. 옵션이 모두 선택일 때 핵심 패턴입니다.
일부는 받고 나머지는 통째로 #
function update(user, { id, ...rest }) {
console.log(id); // 따로
console.log(rest); // 나머지 객체
Object.assign(user, rest);
}
update(targetUser, { id: 'u1', name: '커티스', age: 30 });
// id: u1
// rest: { name: '커티스', age: 30 }
id는 따로 추출해서 검사/로그/특별 처리하고, 나머지는 통째로 객체로 받아 spread/assign으로 흘려보내는 패턴. 라이브러리 함수에 자주 사용됩니다.
Spread 응용 — 객체 #
1) 깊은 병합은 안 됨 (얕은 복사) #
const a = { user: { name: '커티스', age: 30 } };
const b = { ...a, theme: 'dark' };
b.user.age = 31;
console.log(a.user.age); // 31 ← a도 같이 바뀜
{...a}는 1단계 속성만 새로 만듭니다. 안의 객체는 여전히 같은 참조(기초 #2)를 공유합니다. 깊은 복사가 필요하면:
const a = { user: { name: '커티스', age: 30 } };
const b = structuredClone(a);
b.user.age = 31;
console.log(a.user.age); // 30
structuredClone은 ES2022에 표준화된 빌트인. 대부분의 자료구조(객체, 배열, Date, Map, Set 등)를 깊게 복사해 줍니다. 옛날에는 JSON.parse(JSON.stringify(...)) 트릭을 썼는데, 그건 함수/Date/undefined를 못 다뤘습니다.
2) 조건부 속성 — &&와 spread
#
const includeEmail = true;
const profile = {
name: '커티스',
age: 30,
...(includeEmail && { email: 'me@example.com' }),
};includeEmail이 true 면 { email: ... }가 spread, false 면 빈 객체가 spread (아무것도 안 일어남).
옵션 객체를 동적으로 만들 때 자주 사용하는 패턴입니다.
3) 함수 인자에 spread #
const args = [1, 2, 3];
Math.max(...args); // 3
function logAll(a, b, c) {
console.log(a, b, c);
}
logAll(...args); // 1 2 3
apply의 모던 대체. 옛날에는 Math.max.apply(null, args) 였습니다.
Spread vs Rest — 같은 ... 다른 의미
#
같은 ... 문법이 위치에 따라 의미가 달라요.
| 위치 | 이름 | 의미 |
|---|---|---|
| 함수 호출 인자 | spread | 배열을 인자들로 펼침 |
| 배열/객체 리터럴 안 | spread | 안의 원소/속성을 펼쳐 넣음 |
| 함수 매개변수 | rest | 인자들을 배열로 모음 |
| 디스트럭처링 좌변 | rest | 나머지를 모음 |
const arr = [1, 2, 3];
Math.max(...arr); // spread — 펼침
const copy = [...arr]; // spread — 펼침
function f(...args) {} // rest — 모음
const [head, ...tail] = arr; // rest — 모음
좌변에서 모으면 rest, 우변에서 펼치면 spread. 이게 가장 짧은 기억법입니다.
자주 만나는 실전 패턴 묶음 #
1) immutable update — 한 필드만 바꿔서 새 객체 #
const user = { id: 'u1', name: '커티스', age: 30 };
const updated = { ...user, age: 31 };
// 원본은 그대로, age만 바뀐 새 객체
리액트 setState 같은 경우에 필수입니다.
2) 배열 — 인덱스로 한 원소만 갱신 #
const items = ['a', 'b', 'c', 'd'];
const i = 2;
const newValue = 'C';
const updated = [
...items.slice(0, i),
newValue,
...items.slice(i + 1),
];
// ['a', 'b', 'C', 'd']
// 또는 ES2023 의 toSpliced
const updated2 = items.toSpliced(i, 1, newValue);toSpliced가 더 짧습니다. 모던 자바스크립트가 점점 immutable 메서드를 늘려가는 흐름입니다.
3) 키 합치기 — 두 객체 병합 #
const defaults = { theme: 'light', lang: 'ko' };
const userPrefs = { theme: 'dark' };
const merged = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'ko' }
뒤에 적은 객체가 같은 키를 덮어씁니다(우선). Object.assign({}, defaults, userPrefs)와 같은 효과.
4) JSX/리액트 props 전달 #
function Wrapper(props) {
return <Child {...props} />;
}리액트 코드에서 정말 자주 봅니다. 부모가 받은 props를 자식에게 통째로 흘려보내는 패턴.
함정 — 객체 spread 시 prototype 정보 손실 #
{...obj}는 obj의 직접 속성만 가져옵니다. prototype 체인은 따라가지 않습니다.
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`안녕, ${this.name}`);
}
}
const u = new User('커티스');
const copy = { ...u };
u.greet(); // 안녕, 커티스
copy.greet(); // ✗ TypeError — copy는 그냥 객체, greet 없음
클래스 인스턴스를 spread로 복사하면 일반 객체가 됩니다. 메서드는 prototype에 있어서 사라집니다. 클래스 인스턴스를 진짜 복사하려면 Object.assign도 같은 한계가 있고, structuredClone은 클래스 인스턴스를 복사하지 못합니다(plain 객체로 변환). 클래스 인스턴스를 복사하려면 클래스에 clone() 메서드를 직접 만드는 쪽이 일반적입니다.
마무리 #
이번 글에서 정리한 내용:
- 객체 디스트럭처링 — 이름 바꾸기, 기본값, 둘 합치기, 중첩
- 배열 디스트럭처링 — swap 관용구, rest로 머리/꼬리 분리
- 매개변수 디스트럭처링 — 옵션 객체 패턴이 가장 흔함
= {}로 인자 없이 호출 안전하게- spread는 얕은 복사 — 깊은 복사는
structuredClone - 조건부 속성 —
...(cond && { ... }) - spread vs rest — 좌변 rest / 우변 spread
- immutable update 패턴들
- 클래스 인스턴스는 spread로 복사 못 함
다음 글(#5 옵셔널 체이닝과 nullish 병합)에서는 깊은 객체 안전하게 접근하는 ?., 그리고 falsy와 nullish의 차이를 풀어주는 ?? 연산자를 다룹니다.