JS - 깊은복사와 얕은복사 그리고 spread operator에 대해서

euneun·2021년 8월 23일
6

JS & TS

목록 보기
1/3

서론

깊은복사얕은복사에 대해서는 자바스크립트를 공부하고, 사용해보면서 몇번 접하게 된적이 있었지만
제대로 짚고 넘어가지않아 매번 검색을 하고 까먹고를 반복한것같아서 이참에 제대로 정리해보려고 한다.


원시형과 참조형

원시형

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); 

당연히도 위와 같은 결과가 나온다.
원시값은 변경불가능한 값이며 주소가 복사되지 않고, 값이 복사되기 때문에 copiedAa는 별개의 주소값을 가지게 되므로 서로 영향받지 않게된다.

그렇다면 객체를 복사해서 사용한다면 어떤일이 벌어질까?

let person = {
  name: 'euneun',
  age: '2',
  language: {first: 'java', second: 'javscript'}
}

위와 같은 객체를 copiedPerson이라는 변수에 할당하여
copiedPersonage속성을 1로 바꿔보자.

결과는 위와 같다.
copiedPersonage속성만 바꿨을뿐인데 원본인 personage까지 모두 1로 바뀌게됨을 확인할 수 있다.

이렇게 객체와 같은 참조형변수들은 변수에 할당하게되면 주소값이 할당되기 때문에,
해당 객체의 값을 변경하게되면 참조하고있는 모든 곳에서 같은 주소를 바라보고 있기때문에 함께 변경되게 된다.


얕은복사

객체를 복사해서 사용한 예시가 얕은 복사의 예시라고 볼 수 있다.

자바스크립트의 참조형은 얕은 복사가 일어나며, 이는 참조 값(메모리 주소)를 전달하여 결국 한 데이터를 공유하게 되는 현상이다.
이러한 현상은 js로 코딩을 할 때 우리의 의도와는 다르게 흘러갈 수 있기 때문에, 주의해야하고 깊은복사가 필요할때가 생기게 된다..!!

깊은복사

원시형를 복사해서 사용한 예시가 깊은 복사의 예시라고 볼 수 있다.

자바스크립트의 원시 타입은 깊은 복사가 되며, 이는 독립적인 주소 값을 할당하여 생성하는 것이다.

그렇다면 객체와 같은 참조형에서 깊은복사를 구현하고 싶을때는 어떻게 해야할까?


참조형(객체)에서 깊은복사 실현하기

1. spread operator 사용하기

아까 사용하였던 person 객체를 한번 더 사용해준다.
대신 이번에는 copiedPerson에 전개연산자를 이용하여 복사해준다.

let copiedPerson={...person};

후에 copiedPerson age 를 할당해주면 원본( person )과 copiedPerson age 가 연결되어있지 않고 서로 다른 값을 가짐을 확인 할 수 있다.

하지만 전개연산자를 활용하여도 두단계 이상의 depth부터는 깊은복사가 이루어지지 않는다..!

무슨 소리냐 하면은,
위에서 age property가 아닌 한단계 더 들어간 language property의 first property를 바꾸려는 시도를 한다면?


copiedPerson language first만 c++로 바꾸려고 하였을 뿐인데, 원본인 person까지 c++로 변경되었음을 확인 할 수 있다.
두단계 이상의 depth부터는 여전히 참조 값을 전달하는 얕은복사를 진행하고 있었다...ㅠㅠ

따라서 한단계까지의 깊은복사만을 이용하려면 전개연산자를 써도 좋을 것 같다.

2. Object.assign() 이용하기


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++로 변경되었음을 확인 할 수 있다.

그런데 그렇다고해서 아예 personcopiedPerson의 주소값이 같아진 것은 아니다!
1단계까지의 깊은복사는 성공했기때문에, 둘의 값을 비교하면 false이다.

따라서 Object.assign() 또한 한단계까지의 깊은복사만을 이용하려면 사용할 수 있을 것 같다.

3. 라이브러리 이용하기 (lodash의 clone deep)

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

4.JSON.stringify() 이용하기

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 라이브러리를 사용하는 편이 좋을 것 같다!


참조

profile
제대로 짚고 넘어가자!🧐

1개의 댓글

comment-user-thumbnail
2022년 5월 25일

감사합니다. 덕분에 잘 이해하고갑니다!

답글 달기