[JS] Javascript에서의 값의 복사 (얕은 복사, 깊은 복사)

2023. 1. 15. 08:15
반응형

일반적인 값의 복사

일반적으로 변수에서 다른 변수로의 값을 복사하기 위해서는 다음과 같이 사용한다.

//값의 복사
let a = "철수";
let b = a;
console.log(`${a}, ${b}`);	//철수 철수
b = "훈이";
console.log(`${a}, ${b}`); //철수 훈이
일반적인 값의 경우에는 해당 방법으로 복사한 값을 변경해도 원본의 값은 변하지 않는다. 그렇지만 객체나 배열의 경우 이야기가 달라진다.
//1. 객체 & 배열의 복사
let profile = {
	name : "짱구",
	age : 5
}
let copProfile = profile; //같은 매모리 주소 참조
copProfile.name = "맹구";
console.log(profile);	//맹구 5
console.log(copProfile);	//맹구 5

let arr = [1, 2, 3];
let arr2 = arr;
arr2.push(4);
console.log(arr, arr2)	//[1,2,3,4] [1,2,3,4]

다음처럼 객체나 배열의 경우 이 방식으로 복사할 시 메모리 주소를 복사하는 것이다. 즉, 원본과 복사본이 같은 메모리 주소를 바라보는 것이다.

따라서 값을 변경할 시 해당 메모리 주소에 저장되어 있는 값이 변경되며, 같은 메모리를 참조하므로, 복사본이 변경되면 원본이 변경되는 문제점이 있다. 이를 얕은 복사라고 한다.

 

얕은 복사와 깊은 복사

로직을 짜다 보면 얕은 복사를 해서는 안 되는 경우가 종종 발생한다. 특히 원본을 유지하면서 사본을 변경해야 하는 경우에는 반드시 깊은 복사를 해야 한다.

1. 얕은 복사(Shallow Copy)
복사된 객체는 원본 객체와 같은 메모리 주소를 참조한다.
값이 변경되면 원본 및 복사 객체의 인스턴스 변수 값은 같이 변경된다.

2. 깊은 복사(Deep Copy)
객체를 복사 할 때, 해당 객체와 인스턴스 변수 값까지 복사하는 방식.
전부를 복사하여 새로운 메모리 주소에 복사하는 형식으로 따라서 각각 다른 메모리 주소를 참조하게 된다.
 

스프레드 연산자를 이용한 깊은 복사 시도

객체의 깊은 복사를 위해서는 새로운 메모리를 참조하는 객체 생성 -> 값을 대입 하는 과정이 필요하다.

값을 대입하는 과정에서 프로퍼티가 많다면 스프레드 연산자가 유용하게 사용된다.

//2. 객체 & 배열의 복사 (스프레드 연산을 이용한 복사)
let profile = {
	name : "짱구",
	age : 5
}

let copProfile2 = {
	...profile
} //다른 메모리 주소 참조

// 스프레드 연산 이용2
copProfile2.name = "유리";
console.log(profile);	//짱구
console.log(copProfile2);	//유리

let copProfile3 = {
	...profile,
	school: "떡잎유치원"
}
console.log(profile);	//짱구 5
console.log(copProfile3);	//짱구 5 떡잎유치원

// 배열의 복사
let arr3 = [...arr];
arr3.push(5);
console.log(arr, arr3);

위의 코드처럼 {}을 이용하여 다른 주소를 참조하는 객체 생성 후 안에 스프레드 연산자로 복사하는 방법이 있다.

해당 방법을 사용하면 원본은 유지하면서 복사본을 사용할 수 있다.

//객체의 프로퍼티를 삭제하여 복사하고 싶은 경우
const child = {
	name: "철수",
	age : 6,
	hobby: "수영",
	school: "떡잎유치원"
}

//이러면 원본또한 지워짐.
//const child2 = child;
//delete child2.school;
//console.log(child);	//철수 6 수영

//REST 파라미터 이용
const {age , ...child3} = child;
console.log(child3, child)	//철수 수영 떡잎유치원, 철수 6 수영 떡잎유치원

객체 안의 프로퍼티를 삭제하여 복사하고 싶을 때에는 앞 포스팅에서 언급한 REST 파라미터가 유용하게 사용된다.

 

그렇다면 이처럼 스프레드 연산자를 통하여 깊은 복사에 성공한 것일까?

 

스프레드 연산자를 이용한 깊은 복사 시도의 한계

//스프레드 연산을 통한 복사의 문제점 
let member = {
    name : "짱구",
    age : 5,
    hobby: {
        hobby1: "운동",
        hobby2: "게임"
    }
}

let member2 = {
    ...member
}

member2.hobby.hobby2 = "독서";
//원본이 변경됨. hobby는 객체. 같은 메모리 주소를 바라봄 (얕은 복사)
console.log(member);  //hobby2 : 독서  
console.log(member2);	//hobby2 : 독서

//# Fix. 그러나 Object가 복잡해질수록 한계
let member3 = {
    name: member.name,
    age: member.age,
    hobby: {
        ...member.hobby
    }
}
console.log(member3);

그러나 스프레드 연산자로 복사할 때 에도 프로퍼티의 값이 또다른 객체일 경우 메모리 주소를 공유하개 된다.

위에서 hobby는 member라는 객체 안의 객체이다. 즉 스프레드 연산으로 복사를 하더라도 hobby는 같은 메모리 주소를 참조하게 되는 문제점이 있다.

 

따라서 복사할 대상에 hobby는 새로운 주소를 참조하도록 객체를 선언하고 안에 스프레드 연산으로 이어주는 방법이 있다. 그러나 Object가 복잡할 수록 이런 방식은 한계점이 있다.

 

JSON 변환을 이용한 깊은 복사

//3. 객체 & 배열의 복사 (깊은 복사)
//객체 -> JSON String -> 객체
let memberString = JSON.stringify(member);
let member4 = JSON.parse(memberString);
member4.hobby.hobby2 = "코딩";
console.log(member, member4);

복잡한 객체를 복사할 때에는 JSON 변환을 사용하면 유용하다.

- JSON.stringify()는 객체를 JSON 문자열로 변환해준다.

- JSON.parse()는 JSON 문자열을 객체로 변환해준다.

JSON은 기본적으로 String 문자열로, JS에서는 문자열을 복사해도 다른 주소를 참조한다. 따라서 문자열로 변환 후 복사, 그것을 다시 객체로 변환하는 방식으로 깊은 복사를 구현할 수 있다.

 

라이브러리 이용

import lodash from 'lodash'
const member5 = lodash.cloneDeep(member)
member5.hobby.hobby2 = "볼링";
console.log(member, member5);

깊은 복사를 지원하는 라이브러리들이 많다. 

lodash도 그 중 하나이며 cloneDeep() 메서드는 깊은 복사를 지원해준다.

반응형

BELATED ARTICLES

more