JavaScript 객체 복사와 구조 분해 할당 사용하기
예제 기준 · JavaScript : 표준 객체·배열 문법과 브라우저 기본 API structuredClone() (별도 라이브러리 없음)
객체를 복사해 일부 값을 바꾸거나 필요한 property만 변수로 꺼내는 작업은 자주 사용합니다. 얕은 복사와 깊은 복사의 차이를 확인하고, 객체·배열 구조 분해로 값을 추출하는 방법을 살펴봅니다.
먼저 객체 대입은 복사가 아닙니다. 다음 코드에서 두 변수는 같은 객체의 참조(reference)를 가리키므로 copied.name을 바꾸면 original.name도 바뀝니다.
const original = {
name: 'Apple'
};
const copied = original;
copied.name = 'Banana';
console.log(original.name); // Banana
console.log(original === copied); // true
아래 코드 블록은 각각 독립적으로 사용합니다. 실행 예제의 주요 처리를 콘솔 출력으로 정리했으며 DOM 요소가 없어도 읽을 수 있는 코드입니다. 실행 화면은 결과를 pre#result의 textContent로, 안내를 #statusMessage로 표시합니다. 객체를 보여 주는 JSON.stringify()는 출력용이며 복사에 사용하지 않습니다.
spread syntax로 객체 복사하기
{ ...source }는 source의 열거 가능한 자체 property를 새 객체에 펼칩니다. 다음 데이터에서는 바깥 객체가 새로 만들어지므로 복사본의 name을 바꿔도 원본의 문자열은 그대로입니다.
const original = {
name: 'Apple',
details: {
quantity: 1
}
};
const shallow = { ...original };
shallow.name = 'Banana';
console.log(original === shallow); // false
console.log(original.name); // Apple
console.log(shallow.name); // Banana
console.log(original.details === shallow.details); // true
이 복사는 한 단계만 처리하는 얕은 복사입니다. details까지 새로 만들어지는 것은 아닙니다. 아래 실행 예제의 ‘spread 복사본 변경’ 버튼은 name 변경과 함께 details.quantity도 1 증가시킵니다.
Object.assign()으로 객체 복사하기
Object.assign({}, source)는 빈 객체를 대상(target)으로 삼아 source의 열거 가능한 자체 property를 복사합니다. 다음처럼 일반 데이터 property만 있는 객체에서는 spread와 같은 얕은 복사 결과를 얻습니다.
const original = {
name: 'Apple',
details: {
quantity: 1
}
};
const other = Object.assign({}, original);
other.name = 'Orange';
other.details.quantity += 1;
console.log(original === other); // false
console.log(original.name); // Apple
console.log(other.name); // Orange
console.log(original.details.quantity); // 2
console.log(original.details === other.details); // true
첫 번째 인수가 변경 대상이므로 원본을 첫 번째 인수로 넘기는 코드와 구분해야 합니다. 또한 Object.assign은 대상의 setter를 호출할 수 있어 객체 spread와 모든 상황에서 동일하지는 않습니다.
아래 예제는 초기화할 때 원본, spread 복사본, Object.assign 복사본을 함께 만듭니다. 초기화 후 어느 변경 버튼이든 한 번 누르면 세 객체가 공유하는 quantity가 모두 2로 표시됩니다. 이어서 다른 버튼을 누르면 3이 됩니다. name은 누른 버튼에 해당하는 복사본만 Banana 또는 Orange로 바뀝니다.
얕은 복사의 주의점
문자열·숫자 같은 primitive 값은 각 객체의 property에 값으로 들어갑니다. 반면 객체를 값으로 가진 property에는 참조가 복사됩니다. 바깥 객체가 달라도 같은 중첩 객체를 가리킬 수 있습니다.
const original = {
name: 'Apple',
details: {
quantity: 1
}
};
const shallow = { ...original };
shallow.name = 'Banana';
shallow.details.quantity += 1;
console.log(original.name); // Apple
console.log(shallow.name); // Banana
console.log(original.details.quantity); // 2
console.log(shallow.details.quantity); // 2
console.log(original.details === shallow.details); // true
shallow.details.quantity 변경은 공유 객체 내부를 수정합니다. 따라서 원본에서도 변경이 보입니다. 다음 깊은 복사 비교 예제의 ‘spread 복사본 변경’ 버튼이 이 처리를 수행합니다.
structuredClone()으로 깊은 복사하기
structuredClone(source)는 지원하는 데이터의 중첩 객체와 배열까지 복사합니다. 다음 코드는 실행 예제의 초기 데이터와 ‘structuredClone 복사본 변경’ 버튼 동작을 옮긴 것입니다.
const original = {
name: 'Apple',
details: {
quantity: 1
}
};
const other = structuredClone(original);
other.name = 'Orange';
other.details.quantity += 1;
console.log(original.name); // Apple
console.log(other.name); // Orange
console.log(original.details.quantity); // 1
console.log(other.details.quantity); // 2
console.log(original.details === other.details); // false
실행 예제는 초기화 시 얕은 복사본과 깊은 복사본을 함께 생성합니다. 초기화 직후 spread 버튼만 누르면 원본과 spread의 quantity는 2, 깊은 복사본은 1입니다. 초기화 직후 structuredClone 버튼만 누르면 깊은 복사본만 2가 됩니다. API가 없는 브라우저에서는 버튼을 비활성화하고 미지원 안내를 표시합니다.
다음은 중첩 배열을 추가한 보충 코드입니다. 위 실행 화면의 데이터에는 배열이 없지만, 배열 안의 객체도 독립적으로 복사되는지 같은 방식으로 비교할 수 있습니다.
const original = {
items: [
{
name: 'Apple',
quantity: 1
}
]
};
const copied = structuredClone(original);
copied.items[0].quantity += 1;
console.log(original.items === copied.items); // false
console.log(original.items[0] === copied.items[0]); // false
console.log(original.items[0].quantity); // 1
console.log(copied.items[0].quantity); // 2
structuredClone이 모든 값을 복사할 수 있는 것은 아닙니다. 함수나 DOM 노드처럼 지원하지 않는 값이 포함되면 DataCloneError가 발생할 수 있습니다.
JSON.parse(JSON.stringify(source))는 범용 깊은 복사 방법으로 사용하지 않습니다. Date는 문자열로 변환되고, 객체의 undefined property는 빠지며, 배열의 undefined 요소는 null로 바뀝니다. JSON 직렬화와 데이터 복사는 목적을 구분해야 합니다.
객체 구조 분해 할당
객체 구조 분해는 property 이름을 기준으로 값을 가져옵니다. 다음 코드에서는 name과 quantity 변수를 한 번에 선언하며 color는 꺼내지 않습니다.
const fruit = {
name: 'Apple',
quantity: 3,
color: 'Red'
};
const { name, quantity } = fruit;
console.log(name); // Apple
console.log(quantity); // 3
위 선언은 각각 fruit.name과 fruit.quantity를 읽어 변수에 넣는 것과 같은 목적입니다. property를 나열한 순서보다 이름이 중요합니다. 구조 분해 자체가 객체를 깊은 복사하지는 않으므로, 꺼낸 값이 객체라면 참조를 공유합니다.
배열 구조 분해 할당
배열에서는 위치를 기준으로 값을 가져옵니다. 빈 자리를 남기면 해당 위치를 건너뜁니다. 실행 예제는 두 번째 값 Banana를 건너뛰어 first와 third를 표시합니다.
const fruits = ['Apple', 'Banana', 'Orange'];
const [first, , third] = fruits;
console.log(first); // Apple
console.log(third); // Orange
‘값 추출’ 버튼을 누르면 앞 절의 객체와 이 배열을 함께 출력합니다. 원본 객체와 배열은 변경하지 않습니다.
나머지 요소가 필요하면 마지막 위치에 rest 요소를 둡니다. 다음은 같은 과일 배열을 사용한 보충 코드이며 실행 화면에는 rest 결과를 표시하지 않습니다.
const fruits = ['Apple', 'Banana', 'Orange'];
const [first, ...remaining] = fruits;
console.log(first); // Apple
console.log(remaining); // ['Banana', 'Orange']
이름 변경과 기본값 사용하기
name: fruitName은 name property를 fruitName 변수로 받습니다. 뒤에 기본값을 지정하면 property가 없거나 값이 undefined일 때 사용합니다.
const fruit = {};
const { name: fruitName = 'Apple', quantity = 1 } = fruit;
console.log(fruitName); // Apple
console.log(quantity); // 1
null과 빈 문자열은 그대로 유지됩니다. 다음 함수 예제의 세 버튼도 이 차이를 비교합니다.
| name 값 | fruitName 결과 |
|---|---|
| 누락 또는 undefined | 'Apple' |
| null | null |
| '' | '' |
배열 기본값도 같은 기준을 사용합니다. 다음은 실행 화면에 없는 배열 보충 코드입니다.
const fruits = [undefined, null, ''];
const [first = 'Apple', second = 'Banana', third = 'Orange'] = fruits;
console.log(first); // Apple
console.log(second); // null
console.log(third); // 빈 문자열
함수 parameter에서 구조 분해 사용하기
객체를 받는 함수의 parameter에서도 필요한 property만 꺼낼 수 있습니다. 아래 함수는 실행 예제와 동일하며, 출력할 문자열을 반환합니다. 이름 변경과 기본값을 한곳에서 확인할 수 있습니다.
function formatFruit({ name: fruitName = 'Apple', quantity = 1 } = {}) {
return 'fruitName: ' + JSON.stringify(fruitName) + '\nquantity: ' + quantity;
}
console.log(formatFruit({}));
// fruitName: "Apple"
// quantity: 1
console.log(formatFruit({ name: null }));
// fruitName: null
// quantity: 1
console.log(formatFruit({ name: '' }));
// fruitName: ""
// quantity: 1
‘누락 속성’, ‘null 속성’, ‘빈 문자열 속성’ 버튼은 각각 위 세 객체를 전달하고 입력과 결과를 함께 표시합니다. JSON.stringify를 사용하므로 null과 빈 문자열을 출력에서도 구분할 수 있습니다.
parameter 전체의 = {}는 formatFruit()나 formatFruit(undefined)처럼 인수를 생략하거나 undefined를 전달했을 때 적용됩니다. 객체 내부의 name 기본값과는 적용 위치가 다릅니다. formatFruit(null)은 객체 구조 분해를 할 수 없어 TypeError가 발생합니다. 이는 formatFruit({ name: null })과 다릅니다.
중첩 구조가 깊어지면 한 줄에서 여러 단계의 이름 변경과 기본값을 처리하기보다 중간 변수를 두고 단계별로 읽는 편이 수정하기 쉽습니다.
정리
| 방법 | 목적 | 중첩 객체 |
|---|---|---|
| 객체 대입 | 같은 객체 참조 사용 | 바깥 객체부터 공유 |
| { ...source } | 새 객체에 얕은 복사 | 참조 공유 |
| Object.assign({}, source) | 빈 대상 객체에 얕은 복사 | 참조 공유 |
| structuredClone(source) | 지원하는 데이터의 깊은 복사 | 원본과 독립적으로 복사 |
객체 구조 분해는 이름으로, 배열 구조 분해는 위치로 필요한 값을 꺼냅니다. 기본값은 undefined일 때만 적용됩니다. 값을 추출하려는지, 바깥 객체만 새로 만들려는지, 중첩 데이터까지 독립적으로 수정하려는지에 따라 선택하면 됩니다.
