깊은복사와 얕은복사에 대해서는 자바스크립트를 공부하고, 사용해보면서 몇번 접하게 된적이 있었지만
제대로 짚고 넘어가지않아 매번 검색을 하고 까먹고를 반복한것같아서 이참에 제대로 정리해보려고 한다.
MDN에서는 원시값이란 ❓
JavaScript에서 원시 값(primitive, 또는 원시 자료형)이란 객체가 아니면서 메서드도 가지지 않는 데이터입니다.
원시 값에는 7종류, string, number (en-US), bigint (en-US), boolean, undefined, symbol, 그리고 null이 존재합니다.
또한 아래와 같이 중요한 특성을 덧붙인다.
모든 원시 값은 불변하여 변형할 수 없습니다.
즉 요약하면, 원시값에는
String
number
boolean
undefined
symbol
null
이 있고, 원시값은 변형할 수 없다!
변형할 수 없다는 것이지 재할당 할 수 없다는 뜻은 아니다.
let a = 1; // 주소 : 1000, 값 : 1
a = 2; // 주소 : 1004, 값 : 2
재할당은 새 메모리 공간을 확보하고 값을 저장한 후 변수가 이전에 참조하던 메모리 주소를 변경함으로써 이루어진다.
이렇게 해서 원시값은 불변성을 유지하고, 변수는 가리키는 메모리 주소를 바꿔가며 변수 값을 변경한다.
let a = 2;
let copiedA = a;
a = 3;
console.log(a);
console.log(copiedA);
당연히도 위와 같은 결과가 나온다.
원시값은 변경불가능한 값이며 주소가 복사되지 않고, 값이 복사되기 때문에 copiedA
와 a
는 별개의 주소값을 가지게 되므로 서로 영향받지 않게된다.
let person = {
name: 'euneun',
age: '2',
language: {first: 'java', second: 'javscript'}
}
위와 같은 객체를 copiedPerson
이라는 변수에 할당하여
copiedPerson
의 age
속성을 1로 바꿔보자.
결과는 위와 같다.
copiedPerson
의 age
속성만 바꿨을뿐인데 원본인 person
의 age
까지 모두 1로 바뀌게됨을 확인할 수 있다.
이렇게 객체와 같은 참조형변수들은 변수에 할당하게되면 주소값이 할당되기 때문에,
해당 객체의 값을 변경하게되면 참조하고있는 모든 곳에서 같은 주소를 바라보고 있기때문에 함께 변경되게 된다.
객체를 복사해서 사용한 예시가 얕은 복사의 예시라고 볼 수 있다.
자바스크립트의 참조형은 얕은 복사가 일어나며, 이는 참조 값(메모리 주소)를 전달하여 결국 한 데이터를 공유하게 되는 현상이다.
이러한 현상은 js로 코딩을 할 때 우리의 의도와는 다르게 흘러갈 수 있기 때문에, 주의해야하고 깊은복사가 필요할때가 생기게 된다..!!
원시형를 복사해서 사용한 예시가 깊은 복사의 예시라고 볼 수 있다.
자바스크립트의 원시 타입은 깊은 복사가 되며, 이는 독립적인 주소 값을 할당하여 생성하는 것이다.
그렇다면 객체와 같은 참조형에서 깊은복사를 구현하고 싶을때는 어떻게 해야할까?
아까 사용하였던 person 객체를 한번 더 사용해준다.
대신 이번에는 copiedPerson에 전개연산자를 이용하여 복사해준다.
let copiedPerson={...person};
후에 copiedPerson
의 age
를 할당해주면 원본( person
)과 copiedPerson
의 age
가 연결되어있지 않고 서로 다른 값을 가짐을 확인 할 수 있다.
하지만 전개연산자를 활용하여도 두단계 이상의 depth부터는 깊은복사가 이루어지지 않는다..!
무슨 소리냐 하면은,
위에서 age
property가 아닌 한단계 더 들어간 language
property의 first
property를 바꾸려는 시도를 한다면?
copiedPerson
의 language
의 first
만 c++로 바꾸려고 하였을 뿐인데, 원본인 person
까지 c++로 변경되었음을 확인 할 수 있다.
두단계 이상의 depth부터는 여전히 참조 값을 전달하는 얕은복사를 진행하고 있었다...ㅠㅠ
따라서 한단계까지의 깊은복사만을 이용하려면 전개연산자를 써도 좋을 것 같다.
let person = {
name: 'euneun',
age: '2',
language: {first: 'java', second: 'javscript'}
}
let copiedPerson=Object.assign({},person); // 빈 Object에 originObj를 병합하여 반환.
copiedPerson.age=1;
실행결과는 아래와 같다.
역시, 원본(person
)과 copiedPerson
의 age
가 연결되어있지 않고 서로 다른 값을 가짐을 확인 할 수 있다.
하지만 이 또한 전개연산자와 같이 두단계 이상의 depth부터는 얕은복사가 진행된다..ㅠㅠ
한단계 더 들어간 language
의 first
속성을 바꾸려는 시도를 한다면?
역시 원본인 person
까지 c++로 변경되었음을 확인 할 수 있다.
그런데 그렇다고해서 아예 person
과 copiedPerson
의 주소값이 같아진 것은 아니다!
1단계까지의 깊은복사는 성공했기때문에, 둘의 값을 비교하면 false
이다.
따라서 Object.assign()
또한 한단계까지의 깊은복사만을 이용하려면 사용할 수 있을 것 같다.
import cloneDeep from lodash/cloneDeep;
const original = {
...
}
const copied = cloneDeep(original);
copied === original // false
copied.nested === original.nested // false
copied.nested.doubleNested === original.nested.doubleNested // false
JSON.stringfy()
는 객체를 문자열로 변환시켜주는 함수이다.
객체를 json 문자열로 변환하는과정에서 원본 객체와의 참조가 모두 끊어진다.
문자열로 변환후 JSON.parse()
를 이용해 다시 객체로 만들어주면 깊은 복사가 된다.
let person = {
name: 'euneun',
age: '2',
language: {first: 'java', second: 'javscript'}
}
let copiedPerson = JSON.parse(JSON.stringify(person));
copiedPerson.language.first = "c++"
person.language.first === copiedPerson.language.first
실행결과는 false
가 나온다.
하지만 이 방법은 메서드(함수)나 JSON으로 변경할 수 없는 프로퍼티들은 무시하기도하고, 성능면에서 리소스를 많이 잡아먹기때문에 지양하는편이 좋다고 한다...
결론적으로,
Object.assign()
과전개연산자
는 객체의 한단계까지의 깊은복사만 가능하고,
그 이상의 depth부터는 여전히 얕은복사가 일어나므로
완벽한 깊은복사를 사용하기 위해서는 lodash 라이브러리를 사용하는 편이 좋을 것 같다!
참조
감사합니다. 덕분에 잘 이해하고갑니다!