Modern Javascipt (깊은복사, 얕은복사)

신윤철·2022년 4월 17일
1

JS

목록 보기
4/4
post-thumbnail

자료형

Javascript에서 자료형(Data Type)은 기본형과 참조형 두가지로 나눌 수 있습니다.
앞서 작성한 데이터 타입에서 자세히 다뤘으니 간단하게 짚고 넘어가겠습니다.

기본형(Primitive Data Type)의 특징과 종류

  • 한번에 하나의 값 만 가질 수 있습니다.
  • 하나의 고정된 메모리 공간을 이용합니다.
  • 종류에는 Number, String, Boolean, Undefined, Null 등이 있습니다.

참조형(Non-Primitive DataType)의 특징과 종류

  • 한번에 여러 값을 갖을 수 있습니다.
  • 여러개의 고정되지 않은 동적인 메모리 공간을 이용합니다.
  • 종류에는 Object, Array, Function 등이 있습니다.

얕은 복사

얕은 복사는 참조형에서만 발생하며 참조형의 프로퍼티들이 담긴 주소를 복사합니다.

'값을 담은 주소'를 복사해서 '값'이 변해도 '값을 담은 주소'는 그대로이므로 원본데이터와 복사한데이터의 값이 같이 변합니다.

그림으로 설명하면 obj2 = obj1에서 obj2obj1의 프로퍼티들이 담긴 주소 @2000~을 가리키는 주소 @1000을 복사합니다.

let obj1 = {
  a : 10,
  b : 'abc'
};
obj2 = obj1

console.log(obj2 === obj1) 		// true
console.log(obj2.a === obj1.a) 	//true

obj2.a = 20;
console.log(obj1.a) 			// 20

때문에 복사한obj2의 프로퍼티를 변경하면 obj1의 프로퍼티도 변하고,
obj2obj1을 비교하면 같다고 나옵니다.

이렇게 복사한 값이 원본 데이터를 변경하면 매우 큰 문제가 발생할 수 있으므로 상황에 따라 깊은 복사를 사용해야 합니다.


깊은 복사

깊은 복사는 기본형과 참조형에서 약간 다르게 복사해야합니다.

기본형에서는 값에 직접 연결된 주소를 복사하므로 그 자체가 깊은 복사입니다.
참조형에서는 '값을 담은 주소'를 원본데이터와 다른 메모리 주소로 복사해야합니다.

Javascript에서는 값을 서로 다른 새로운 값으로 재할당하면 새로운 메모리 주소로 배치합니다. (같은 값은 같은 주소를 사용함)

기본형의 깊은 복사

기본적으로 기본형 데이터타입의 복사는 깊은 복사를 합니다.

그림을 살펴보면 a를 복사한 b는 값 100을 가리키는 주소 @1001을 복사합니다.
즉 값(100)의 주소(@1001)를 복사합니다.

let a = 100;
b = a;

console.log(a === b); 	// true
b = 200;

console.log(a);			// 100
console.log(a === b); 	// false

그리고 기본형은 값의 주소를 복사할 경우 식별자에 새로운 값을 할당할 시 새로운 값의 주소를 메모리에 연결합니다.

참조형의 깊은 복사

이제 중요한 참조형의 깊은 복사를 알아보겠습니다.

이론적으로 얕은 복사에서 원본 참조형과 복사된 참조형이 같이 변경되는 이유는 같은 주소값을 바라보기 때문입니다.

즉, 간단하게 생각하면 같은 값을 담은 주소값(아래 그림에선 @1000)를 참조하지 않으면 됩니다.

그림에선 obj1obj2가 서로 다른 주소값을 가리킵니다.

이때 obj2.a의 값을 변경하면 아래 그림처럼 obj1.a의 값은 변하지 않게 됩니다.

이러한 생각을 갖고 참조형의 여러 깊은 복사를 살펴보겠습니다.

직접 재귀 함수를 통해 깊은 복사 함수 구현

위의 이론을 토대로 직접 깊은 복사를 구현해보겠습니다.

const copyObjectDeep = (obj) => {
  if(obj === null || typeof obj !== "object"){
    return obj;
  }
  
  let copyResult = {}
  for (let prop in obj){
    copyResult[prop] = copyObjectDeep(obj[prop]);
  }
  return copyResult;
}

const obj = {
  a: 10, 
  b: {
  	c: null,
    d: [1, 2]
  },
};
const obj2 = copyObjctDeep(obj);
console.log(obj === obj2) 	// false

obj2.a = 1000;
console.log(obj, obj2); 	// {a: 10, b: {…}} {a: 1000, b: {…}}

obj2.b.c = 200;
console.log(obj.b, obj2.b);	// {c: null, d: Array(2)} {c: 200, d: {…}}

앞선 이론처럼 재귀함수를 통해 원본 객체 obj의 모든 프로퍼티를 복사하여 새로운 객체 result에 넣었습니다.

즉, 완전히 다른 주소를 참조하는 깊은 복사를 하였습니다.

때문에 복사된 obj2의 프로퍼티를 변경해도 원본 객체에 영향을 주지 않습니다.

JSON를 활용한 깊은 복사

원리는 객체를 JSON.stringify() 메서드로 문자열로 전환했다가
다시 JSON.parse() 메서드를 통해 문자열을 객체로 변환하는 것입니다.

이렇게 하면 기존과 값만 같을 뿐 새로운 주소를 갖게되므로
이론상 깊은 복사를 합니다.

const copyObjectUsingJSON = (obj) => {
  return JSON.parse(JSON.stringify(obj));
}

const obj = {
  a: 10, 
  b: {
  	c: null,
    func1 : function () { console.log("함수"); },
  },
  func2 : function () { console.log("함수2"); },
};

const obj2 = copyObjectUsingJSON(obj);
console.log(obj === obj2) 	// false

obj2.a = 1000;
console.log(obj, obj2); 	// {a: 10, b: {…}} {a: 1000, b: {…}, func2: f}

obj2.b.c = 200;
console.log(obj.b, obj2.b);	// {c: null, func1: ƒ} {c: 200}

JSON을 사용해 깊은 복사를 사용하는 것은 몇가지 주의사항이 있습니다.

  1. function, __proto__, getter/setter 같이 JSON으로 변경 불가능한 프로퍼티는 모두 무시합니다.

  2. 다른 방법에 비해 처리 속도가 느립니다.

spread 연산자를 사용한 깊은 복사

spread 연산자 ...obj를 사용해도 참조형의 깊은 복사를 할 수 있습니다.

const obj = {
  a: 10, 
  b: {
  	c: null,
    func1 : function () { console.log("함수"); },
  },
};

const obj2 = {...obj};
console.log(obj === obj2) 	// false

obj2.a = 1000;
console.log(obj, obj2);		// {a: 10, b: {…}} {a: 1000, b: {…}} 

obj2.b.c = 200;
console.log(obj.b, obj2.b); // {c: 200, func1: ƒ} {c: 200, func1: ƒ}

spread 연산자를 사용하면 매우 간단하게 깊은 복사를 할 수 있지만

예제코드에서도 확인할 수 있듯이 큰 문제점이 있습니다.
spread 연산자를 통한 깊은 복사는 중첩된 객체에서는 얕은 복사를 한다는 점 입니다.

때문에 상황에 맞게 잘 사용해야 합니다.

lodash 모듈의 cloneDeep을 사용한 깊은 복사

직접 구현하기엔 귀찮고,
JSON을 사용하기엔 function같은 자료형을 복사하지 못하고,
spread 연산자를 사용하자니 중첩 객체를 깊은 복사하지 못합니다.

이런 우리를 위해 나온 좋은 모듈이 있습니다.
lodash 모듈의 cloneDeep() 메서드를 활용하면 됩니다.

// 먼저 lodash 모듈을 설치해줍니다.
& npm i lodash
const lodash = require("lodash");

const obj = {
  a: 10, 
  b: {
  	c: null,
    func1 : function () { console.log("함수"); },
  },
};

const obj2 = lodash.cloneDeep(obj);
console.log(obj === obj2) 	// false

obj2.a = 1000;
console.log(obj, obj2);		// {a: 10, b: {…}} {a: 1000, b: {…}} 

obj2.b.c = 200;
console.log(obj.b, obj2.b); // {c: null, func1: ƒ} {c: 200, func1: ƒ}

지금까지 알아본 방법 중 가장 간단하고 단점이 없는 방법인 것 같습니다.

깊은 복사를 하려면 lodash를 사용해야할 듯 합니다.!

profile
기본을 탄탄하게🌳

0개의 댓글