JavaScript/JavaScript 문법

[JavaScript]얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)

DevStory 2021. 6. 22.

얕은 복사(Shallow Copy)깊은 복사(Deep Copy)에 대해 포스팅합니다.


자바스크립트의 데이터 유형

얕은 복사와 깊은 복사에 대해 설명하기 전에 자바스크립트의 데이터 유형에 대해 간단하게 알아봅시다.

 

자바스크립트에서 기본 데이터 타입(원시 타입)으로는 string, number, null, undefined, symbol이 존재합니다.

참조형 데이터 타입으로는 ArrayObject가 존재합니다.

 

아래 코드에서는 기본 데이터 타입에 대해 복사 후 값을 변경합니다.

var a = 10;
var b = a;

b = 5;

console.log('a : ' + a);
console.log('b : ' + b);

var b = a; 의 코드에서 a의 값을 b에 복사합니다.

그리고 b의 값을 5로 변경 후 a와 b의 값을 확인해보면, a의 값은 변경되지 않았고 b의 값은 변경되었습니다.

 

이번에는 반대로 기존 변수의 값을 변경해보았습니다.

var a = 10;
var b = a;

a = 5;

console.log('a : ' + a);
console.log('b : ' + b);

위 2개의 코드를 통해서 기본 데이터 타입은 복사 후 값을 변경해도 다른 변수에는 영향을 주지 않음을 확인하였습니다.

 

그렇다면 참조형 데이터 타입에 대해 복사 후 값을 변경합니다.

var arrA = ['a', 'b', 'c', 'd'];
var arrB = arrA;
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

console.log('값 변경 후');

arrA[3] = 'e';
arrB[4] = 'f';
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

기존 배열인 arrA과 복사된 배열 arrB의 값을 변경하였는데, 둘 다 동일한 결과를 보여주고 있습니다.

기본 데이터 타입 복사의 관점에서 생각을 하면, 아래와 같은 결과가 나와야 합니다.

arrA : ["a", "b", "c", "e"]

arrB : ["a", "b", "c", "d", "f"]

 

이러한 경우를 얕은 복사(Shallow Copy)라고 하며, 참조 주소를 공유하고 있습니다.

깊은 복사(Deep Copy)는 참조 주소를 공유하지 않고 참조 공간도 복사됩니다.


배열을 복사하는 방법

배열을 복사하는 방법으로는 slice(), concat(), spread 연산자, Array.from()가 존재합니다.

단, 1 레벨(1차원 배열)에 대해서는 깊은 복사가 허용되나 2 레벨(2차원 배열)이상 부터는 깊은 복사가 되지 않습니다.

 

EX 1) slice() 1차원 배열

var arrA = ['a', 'b', 'c', 'd'];
var arrB = arrA.slice();
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

console.log('값 변경 후');

arrA[3] = 'e';
arrB[4] = 'f';
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

EX 2) slice() 2차원 배열

var arrA = ['a', 'b', ['c', 'd']];
var arrB = arrA.slice();
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

console.log('값 변경 후');

arrA[0] = 'z';
arrB[0] = 'x';

arrA[2][0] = 'e';
arrB[2][1] = 'f';
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

EX 3) concat() 1차원 배열

결과는 slice()와 동일합니다.

var arrA = ['a', 'b', 'c', 'd'];
var arrB = [].concat(arrA);
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

console.log('값 변경 후');

arrA[3] = 'e';
arrB[4] = 'f';
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

EX 4) concat() 2차원 배열

결과는 slice()와 동일합니다.

var arrA = ['a', 'b', ['c', 'd']];
var arrB = [].concat(arrA);
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

console.log('값 변경 후');

arrA[0] = 'z';
arrB[0] = 'x';

arrA[2][0] = 'e';
arrB[2][1] = 'f';
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

EX 5) spread 연산자 1차원 배열

결과는 slice()와 동일합니다.

var arrA = ['a', 'b', 'c', 'd'];
var arrB = [...arrA];
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

console.log('값 변경 후');

arrA[3] = 'e';
arrB[4] = 'f';
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

EX 6) spread 연산자 2차원 배열

결과는 slice()와 동일합니다.

var arrA = ['a', 'b', ['c', 'd']];
var arrB = [...arrA];
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

console.log('값 변경 후');

arrA[0] = 'z';
arrB[0] = 'x';

arrA[2][0] = 'e';
arrB[2][1] = 'f';
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

EX 7) Array.from() 1차원 배열

결과는 slice()와 동일합니다.

var arrA = ['a', 'b', 'c', 'd'];
var arrB = Array.from(arrA);
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

console.log('값 변경 후');

arrA[3] = 'e';
arrB[4] = 'f';
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

EX 8) Array.from() 2차원 배열

결과는 slice()와 동일합니다.

var arrA = ['a', 'b', ['c', 'd']];
var arrB = Array.from(arrA);
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

console.log('값 변경 후');

arrA[0] = 'z';
arrB[0] = 'x';

arrA[2][0] = 'e';
arrB[2][1] = 'f';
console.log('arrA : ', arrA);
console.log('arrB : ', arrB);

객체를 복사하는 방법

객체를 복사하는 방법으로는 Object.assign(), spread 연산자가 존재합니다.

배열과 마찬가지로 1 레벨은 깊은 복사가 허용되나 2 레벨 부터는 깊은 복사가 되지 않습니다.

 

EX 1) Object.assign() 1레벨

var objectA = {
    name: 'Java'
}

var objectB = Object.assign({},objectA);

console.log("objectA : ", objectA);
console.log("objectB : ", objectB);

console.log("값 변경 후");

objectA.name = 'C Programming';
objectB.name = 'React';

console.log("objectA : ", objectA);
console.log("objectB : ", objectB);

EX 2) Object.assign() 2레벨

var objectA = {
    name: 'Java',
    score: {
        A : 100,
        B : 200,
        C : 300
    }
}

var objectB = Object.assign({},objectA);

console.log("objectA : ", objectA);
console.log("objectB : ", objectB);

console.log("값 변경 후");

objectA.score.A = 500;
objectB.score.C = 1000;

objectA.name = 'C Programming';
objectB.name = 'React';

console.log("objectA : ", objectA);
console.log("objectB : ", objectB);

EX 3) spread 연산자 1레벨

결과는 Object.assign()과 동일합니다.

var objectA = {
    name: 'Java'
}

var objectB = {...objectA};

console.log("objectA : ", objectA);
console.log("objectB : ", objectB);

console.log("값 변경 후");

objectA.name = 'C Programming';
objectB.name = 'React';

console.log("objectA : ", objectA);
console.log("objectB : ", objectB);

EX 4) spread 연산자 2레벨

결과는 Object.assign()과 동일합니다.

var objectA = {
    name: 'Java',
    score: {
        A : 100,
        B : 200,
        C : 300
    }
}

var objectB = {...objectA};

console.log("objectA : ", objectA);
console.log("objectB : ", objectB);

console.log("값 변경 후");

objectA.score.A = 500;
objectB.score.C = 1000;

objectA.name = 'C Programming';
objectB.name = 'React';

console.log("objectA : ", objectA);
console.log("objectB : ", objectB);

Deep Copy를 하는 방법

Deep Copy를 하는 방법은 3가지입니다.

1. 모든 깊이에 있는 객체까지 복사하는 재귀 함수를 구현

2. JSON 사용

3. lodash의 cloneDeep() 사용

 

1번의 경우에는 직접 코드를 구현해야 한다는 단점이 있으며,

2번의 경우에는 모든 데이터가 복사되지 않거나 타입이 변경되는 문제가 있습니다.

3번의 경우에는 1번의 단점을 커버해주지만 lodash 패키지가 설치되어야 합니다.

 

EX 1) 재귀 함수 구현

const a = {
  string: 'string',
  number: 123,
  bool: false,
  nul: null,
  date: new Date(),  // stringified
  undef: undefined,  // lost
  inf: Infinity,  // forced to 'null'
  re: /.*/,  // lost
}
console.log(a);
console.log(typeof a.date);  // Date object
const clone = cloneFunction(a);
console.log(clone);
console.log(typeof clone.date);  // result of .toISOString()

function cloneFunction(item) {
    if (!item) { return item; } // null, undefined values check

    var types = [ Number, String, Boolean ], 
        result;

    // normalizing primitives if someone did new String('aaa'), or new Number('444');
    types.forEach(function(type) {
        if (item instanceof type) {
            result = type( item );
        }
    });

    if (typeof result == "undefined") {
        if (Object.prototype.toString.call( item ) === "[object Array]") {
            result = [];
            item.forEach(function(child, index, array) { 
                result[index] = cloneFunction( child );
            });
        } else if (typeof item == "object") {
            // testing that this is DOM
            if (item.nodeType && typeof item.cloneNode == "function") {
                result = item.cloneNode( true );    
            } else if (!item.prototype) { // check that this is a literal
                if (item instanceof Date) {
                    result = new Date(item);
                } else {
                    // it is an object literal
                    result = {};
                    for (var i in item) {
                        result[i] = cloneFunction( item[i] );
                    }
                }
            } else {
                // depending what you would like here,
                // just keep the reference, or create new object
                if (false && item.constructor) {
                    // would not advice to do that, reason? Read below
                    result = new item.constructor();
                } else {
                    result = item;
                }
            }
        } else {
            result = item;
        }
    }

    return result;
}

EX 2) JSON 사용

const a = {
  string: 'string',
  number: 123,
  bool: false,
  nul: null,
  date: new Date(),  // stringified
  undef: undefined,  // lost
  inf: Infinity,  // forced to 'null'
  re: /.*/,  // lost
}
console.log(a);
console.log(typeof a.date);  // Date object
const clone = JSON.parse(JSON.stringify(a));
console.log(clone);
console.log(typeof clone.date);  // result of .toISOString()

원본 객체와 복제된 객체의 date 프로퍼티의 데이터 타입이 일치하지 않는 문제가 발생합니다.

EX 3) lodash의 cloneDeep() 사용

const a = {
  string: 'string',
  number: 123,
  bool: false,
  nul: null,
  date: new Date(),  // stringified
  undef: undefined,  // lost
  inf: Infinity,  // forced to 'null'
  re: /.*/,  // lost
}
console.log(a);
console.log(typeof a.date); 
const clone = _.cloneDeep(a);
console.log(clone);
console.log(typeof clone.date);  

참고

Deep Copy에서 JSON의 문제점

https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript

 

재귀 함수 구현

https://stackoverflow.com/questions/4459928/how-to-deep-clone-in-javascript


배열, 객체 복사 예제

https://dev.to/anuradha9712/shallow-vs-deep-copy-in-javascript-4kc6

반응형

댓글