본문 바로가기

Languages/JavaScript

[JavaScript] 객체의 얕은 복사, 깊은 복사, 참조 완벽 이해하기 (쉽게쉽게 이해해보자)

728x90

 

얕복과 깊복... 개발 용어로 설명 해놓은 것을 보니 정말 헷갈리는 부분이었지만,
나의 방식대로 비유를 통해서 이해하니 금방 이해가 되었다! 
이 게시글을 참고하면 어려웠던 복사의 개념을 이해할 수 있게 될 것이다.

 


 

 

우선 복사의 종류는 2가지이다.

1. 얕은 복사 
2. 깊은 복사

 

 

여기서 '얕은 복사'는 또 2가지 종류로 나뉜다.

(1) 참조에 의한 복사
(2) 값 복사


참조에 의한 복사  : 원본을 복사하지만, 원본으로 부터 완전히 독립되지 못하고 수정할 때마다 '동기화'가 진행된다. (즉, 복사한 파일을 수정하면 원본 파일까지 같이 수정된다고 이해하면 쉽다.)

 

값 복사  : 원본를 복사하면, 독립된 개체가 생성이 된다. (우리가 워드 파일(A)을 만들고나서, A파일을 복사해서 복사한 파일(B)의 내용을 수정했을 때 원본(A)은 그대로고, 복사한 파일(B)의 내용만 바뀌듯이!)

 

 

이렇게 설명하면 '값 복사가 깊은 복사가 아닌가?' 라는 생각을 할 수도 있다. 

하지만 값 복사는 '객체' 복사는 가능하지만, '객체 안의 객체'까지는 복사가 불가능하므로 얕은 복사이다.

이에 반해, 깊은 복사'객체 안의 객체'까지 완전히 따로 놀게끔 복사가 가능하다. 

그래서 '더 깊은 의미에서의 진정한 복사'라는 뜻에서 깊은 복사이다. 

깊은 복사는 예시를 봐야 이해가 더 빠르다. (아래에 서술하겠다)

이제 앞에서 쉽게 설명한 것을 개발 용어로 바꿔서 설명해보겠다.

 

 


 

 

 얕은 복사 中 참조에 의한 복사 

 

'참조에 의한 복사'는 변수엔 객체가 그대로 저장되는 것이 아니라, 객체에 대한 '참조 값’이 저장되는 것이다.

'참조값'객체가 저장되어있는 '메모리 주소’이다. 

따라서 객체가 할당된 변수를 복사할 땐 객체의 참조 값이 복사되고 객체 자체는 복사되지 않는다.

이것은 원시형 타입과 달리, 객체만의 특징이다.

즉, 객체는 다른 변수에 대입할 때 을 복사하는 게 아니라 참조(메모리의 주소)를 복사하는 것이다.

(그래서 따로따로 운영되지않으며, 항상 원본의 주소를 가리키고 있기에 동기화가 진행된다! ) 

예시를 통해 알아보자!

 

const user = {
  name : 'Tom',
  age : 30,
}
console.log(user.name); //Tom

 

user의 key값인 name을 찍어보면 당연히 Tom이 나온다.

 

참조에 의한 복사를 해보겠다

const user = {
  name : 'Tom',
  age : 30,
}


const user2 = user;

console.log(user); // {name: 'Tom', age: 30}
console.log(user2); // {name: 'Tom', age: 30}

 

복사가 되었다. 이제 복사한 객체인 user2의 name값을 'Mike'로 바꿔볼 것이다.

const user = {
  name : 'Tom',
  age : 30,
}


const user2 = user;

user2.name = 'Mike';

console.log(user);
console.log(user2);

 

user2의 value 값을 Mike로 바꿔줬더니 user와 user2의 value 값이 모두 바뀌어버렸다. (쉽게 말해, 동기화가 되었다)

 

여기서 일어난 일은 다음과 같다.

(1) 객체를 생성하면, 객체는 메모리 내 어딘가에 저장되고, 변수 user에는 객체를 '참조'할 수 있는 값이 저장된다.
(2) 그리고 객체가 할당된 변수(user)를 user2에다가 복사하면,  객체의 '참조값'이 복사된다. (객체가 복사되는 것이 아니다)
(3) 변수는 두개(user와 user2)이지만, 각 변수에는 '동일 객체'에 대한 '참조 값'이 저장된다. 

 

*이것도 뭔 소린지 모르겠다면? 진짜 진짜 쉽게 이해를 돕기 위한 또다른 비유를 들어보았다. (이거 보고도 이해 안되면 그냥 포기하셈)

- 객체사물함이고, 변수는 그 사물함을 열 수 있는 열쇠이다. 그리고 그 열쇠에는 서랍장을 열 수 있는 비밀번호(참조값)가 담겨있다고 생각해보자.
- 사물함을 생성하면, 열쇠에는 그 사물함을 열 수 있는 비밀번호가 저장된다. (=객체를 생성하면, 변수에는 객체를 참조할 수 있는 참조값이 저장된다)  
- ex. 물품 보관소에서 1번 사물함을 쓰기로 지정하였다. 그리고 열쇠를 하나 받았는데, 그 열쇠에 비밀번호 1234가 자동으로  저장되었다.
- 기존 열쇠를 잃어버릴 수도 있어서 그 사물함을 열 수 있는 다른 열쇠를 하나 더 복제하기로 하였다. 
- 사물함 할당된 기존 열쇠다른 열쇠에 복사했더니, 그 사물함비밀번호 1234가 복사되었다.
(=객체가 할당된 변수(user)user2에다가 복사하면,  객체의 '참조값'이 복사된다.)
- 이는 사물함이 복사된게 아니라, 비밀번호가 복사된 것이다. (=객체가 복사되는 것이 아니다)
- 사물함(객체)은 하나, 사물함을 열 수 있는 열쇠(변수)는 두 개인데, 그중 하나(다른열쇠
)를 사용해 서랍장을 열어 내용물을 바꾼 후, 기존 열쇠로 서랍장을 열면 바뀐 내용물을 볼 수 있는 것이다.

 

 

 

 이렇게 참조에 의해 복사된 두 객체를 비교하면 어떻게 결과가 나올까? 

 

let a = {};
let b = a; // 참조에 의한 복사

console.log( a == b ); // true, 두 변수는 같은 객체를 참조함
console.log( a === b ); // true

 

객체 비교 시 동등 연산자 ==와 일치 연산자 ===는 동일하게 동작하며, 비교 시 피연산자인 두 객체가 동일한 객체인 경우에는 참을 반환한다.

두 변수가 같은 객체를 참조하는 예시를 살펴보면, 일치·동등 비교 모두에서 참이 반환된다. ( '동일한 객체'이다!  같은 1번 사물함이야!!!  )

 

 

 참고) '독립된' 두 객체를 비교했을 때는 결과가 어떻게 나오는지? 

let a = {};
let b = {}; // 독립된 두 객체

alert( a == b ); // false

두 객체 모두 비어있다는 점에서 같아 보이지만, 독립된 객체이기 때문에 일치·동등 비교하면 거짓이 반환된다.

 

 


 

 

 얕은 복사 中 값 복사 

 

그렇다면 객체를 진짜로 독립되게끔 (따로따로 놀게끔) 복사하려면 어떤 방법을 써야할까?

기존에 있던 객체와 똑같으면서 독립적인 객체를 만들고 싶은것이다!

방법은 있는데 자바스크립트는 객체 복제 내장 메서드를 지원하지 않기 때문에 조금 어렵다.
사실상 객체를 복제해야 할 일은 거의 없다.
위에 설명한 참조에 의한 복사로 해결 가능한 일이 대다수이다.

그럼에도 불구하고 정말 복제가 필요한 상황이라면  2가지 방법이 있다.

 

(1) 새로운 객체를 만든 다음 기존 객체의 프로퍼티들을 순회해 원시 수준까지 프로퍼티를 복사하기
(2) Object.assign 사용하기

 

 

 

 값복사 하는 첫번째 방법 

 

1번 방법은 다음과 같다.

let user = {
  name: "Tom",
  age: 30
};

let user2 = {}; // 새로운 빈 객체

// 빈 객체에 user 프로퍼티 전부를 복사해 넣는다.
for (let key in user) {
  user2[key] = user[key];
}

console.log(user2); // {name: 'Tom', age: 30}  // user2는 독립적인 복제본이 되었다.

 

user2.name = "Mike"; // user2의 데이터를 변경해본다.

console.log( user2.name ); // Mike

console.log( user.name ); // John (기존 객체에는 여전히 John이 있다)

 

이제 user2는 완전히 독립적인 복제본이 되었다.

 

 

 

 

 값복사 하는 두번째 방법 

 

2번 방법은 다음과 같다.

const user = {
  name : 'Tom',
  age : 30,
}

const user2 = Object.assign({},user); 

// user에 있는 모든 프로퍼티가 빈 배열에 복사되고 변수에 할당된다.


user2.name = 'Mike';

console.log(user); // {name: 'Tom', age: 30}
console.log(user2); // {name: 'Mike', age: 30}

 user2는 완전히 독립적인 복제본이 되었다.

 

 

 *참고) Object.assign의 동작 방식은 다음과 같다. 

Object.assign(dest, [src1, src2, src3...])

(1) 첫 번째 인수 dest는 목표로 하는 객체이다.
(2) 이어지는 인수 src1, ..., srcN는 복사하고자 하는 객체이다. (...은 필요에 따라 얼마든지 많은 객체를 인수로 사용할 수 있다는 것을 나타낸다.)
(3) 객체 src1, ..., srcN의 프로퍼티를 dest에 복사한다.
(4) 마지막으로 dest를 반환한다. 

 

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// permissions1과 permissions2의 프로퍼티를 user로 복사한다.
Object.assign(user, permissions1, permissions2);

// now user = { name: "John", canView: true, canEdit: true }

 

 

 

 

 이렇게 값복사된 두 객체를 비교하면 어떻게 결과가 나올까? 

const user = {
  name : 'Tom',
  age : 30,
}

const user2 = Object.assign({},user);


user2.name = 'Mike';

console.log( user ==  user2 ); // false
console.log( user ===  user2 );  // false

 

 

아까 참조에 의한 복사와는 다르게 일치·동등 비교 모두에서 거짓이 반환된다.

이제 두 객체는 완전히 독립된 다른 객체가 되었다! >_< (뿌듯)

 

 


 

 

 깊은 복사 

 

이제 마지막으로 깊은 복사를 알아보자!

지금까진 user의 모든 프로퍼티가 원시값인 경우만 가정했다.
그런데 프로퍼티는 다른 객체에 대한 참조 값일 수도 있다. (객체 안에 객체가 있는 경우!)

 

const user = {
  name : 'Tom',
  age : 30,
  addr : {
    city : 'seoul'
  }
}

const user2 = Object.assign({},user); // 얕은 복사 중 값복사를 해본다.



user2.name = 'Mike'; // user2의 name 값을 'Mike'로 바꾼다.
user2.addr.city = 'Busan'; // user2의 addr 객체의 city값을 'Busan'으로 바꾼다.

console.log(user); 
console.log(user2);

 

 

name은 잘 바꼈으나, city값은 둘다 Busan으로 바껴버렸다. (겉의 객체는 복사가 되었지만 안의 객체는 참조가 되어버린다)

즉, '객체 안의 객체의 value값'은 '참조 값이 복사'되는 문제가 발생한 것이다.

안에 있는 객체까지 완벽하게 복사하는 것이  (중첩 객체까지 처리하는 것이) 깊은 복사이다.

 

이것 또한 방법은 여러가지이다.

(1) '깊은 복사’를 가능하게 해주는 _.cloneDeep(obj)를 사용하기 (자바스크립트 라이브러리 lodash의 메서드) 
(2) 남들이 만들어놓은 라이브러리를 사용하기
(3) json 메서드를 사용하기
(4) 재귀를 사용하기

 

2번 방법 사용해서 깊은 복사를 해보았다.

 

 

깊은 복사는 해야할 일이 거의 드물기 때문에 필요 시에 상황에 맞는 방법을 사용하면 될 것이다.

 

 

 

 

 

참고 자료

https://ko.javascript.info/object-copy

728x90