본문 바로가기
Javascript

자바스크립의 값과 참조값의 얕은 복사와 깊은 복사

by memeseo 2023. 6. 10.

undo & redo 구현중에 인스턴스 상태를 저장하는 코드를 작성해야 해서 얕은 복사와 깊은 복사를 좀 더 심도 있게 공부하고자 오랜만에 블로그를 켰다 ,,


먼저, 자바스크립트의 값은 원시값참조값 두 가지 데이터 타입으로 나뉜다.

 

원시값

  • Number
  • String
  • Null
  • Undefined

 

참조값

  • Object
  • Symbol

 

원시값은 기본 자료형(단순 데이터)을 의미한다. 변수에 원시값을 저장하면 변수의 메모리 공간에 실제 데이터 값이 저장되고, 할당된 변수를 조작하려고 하면 저장된 실제 값이 조작된다.

let a = 'b';
a = 'c'
console.log(a) //'c'

 

원시값을 복사해서 다른 값을 입력할 경우, 원시값은 또 다른 독립적인 메모리 공간에 할당하기 때문에 기존 원시값을 저장한 변수에는 영향이 끼치지 않는다. 이 처럼 실제 값을 복사하는 것을 깊은 복사라고 한다. 밑에 예시는 원시값을 깊은 복사한 것이다.

const a = 'b';
let b = a;

b = 'c';

console.log(a) // 'b'
console.log(b) // 'c'

 

하지만 참조값은 변수가 객체의 주소를 가리키기 때문에 복사된 값은 같은 주소를 가리킨다. 그래서 복사를 하고 객체를 수정하면 두 변수는 똑같은 메모리 주소를 가리키고 있기 때문에 기존 객체에 영향을 끼친다. 밑에 예시는 참조값을 얕은 복사한 것이다.

 

const animal = {
	dog : 'cute',
    rabbit : 'lovely'
}

let thief = animal;
thief.dog = 'so cute';

console.log(animal.dog) // 'so cute' > 원본까지 바뀌어짐
console.log(thief.dog) // 'so cute'

 

이러한 객체의 특징 때문에 객체를 복사하는 방법은 크게 두 가지로 나뉜다.

 

얕은 복사 Shllow Copy

얕은 복사란 객체를 복사할 때 위의 예시처럼 원래 값과 복사된 값이 같은 주소를 가리키고 있는 것을 말한다. 객체안에 객체가 있을 경우 한개의 객체라도 원본 객체를 참조하고 있다면 이를 얕은 복사라고 한다.

 

얕은 복사 방법

1. Object.assign(생성할 객체, 복사할 객체)

Object.assign은 첫 번째 요소로 들어온 객체에 다음 인자로 들어온 객체를 복사해준다.

 

const original = {
	a : 1,
    b : {
    	c : 2
    }
};

const copy = Object.assign({}, original);

copy.b.c = 3;

console.log(original === copy) // false
console.log(original.b.c === copy.b.c) // true

 

2. Array.prototype.slice()

 

start부터 end 인덱스까지 기존 배열에서 추출하여 새로운 배열을 리턴하는 메소드이다. 만약 start와 end를 설정하지 않는다면, 기존 배열을 전체 얕은 복사 한다.

 

const original = ['a', 'b', 'c'];
const copy = original.slice();

console.log(JSON.stringify(original) == JSON.stringify(copy)); // true

copy.push('d');
console.log(original) // ['a', 'b', 'c']
console.log(copy) // ['a', 'b', 'c', 'd']

 

기존 배열에 영향을 끼치지 않아서 깊은 복사로 보일 수 있지만, 원시값을 저장한 1차원 배열이기 때문에 그렇다. 원시값은 기본적으로 깊은 복사를 하며, slice()메소드는 얕은 복사를 수행한다.

 

const original = [
	[1, 1, 1, 1],
    [0, 0, 0, 0]
];

let copy = original.slice();
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true

copy[0][0] = 11;
copy[1].push(00);

console.log(JSON.stringify(original) === JSON.stringify(copy)); // true

console.log(original);
// [[11, 1, 1, 1], [0, 0, 0, 0, 00]]
console.log(copy);
// [[11, 1, 1, 1], [0, 0, 0, 0, 00]]

 

만약 1차원 배열이 아닌, 중첩 구조를 갖는 2차원 배열이라면 얕은 복사를 수행하게 된다.

 

const original = [
    {
        a : 1,
        b : 2
    },
    true
];


let copy = original.slice();
console.log(JSON.stringify(original) === JSON.stringify(copy)) // true

copy[0].a = 0;
copy[1] = false;

console.log(JSON.stringify(original) === JSON.stringify(copy)) // false
console.log(original) 
// [{a : 0, b : 2}, true]
console.log(copy)
// [{a : 0, b : 2}, false]

 

위 예시는 배열 안에 객체를 수정할 때 앝은 복사가 되어 원본 변수의 값이 변경 되지만, 원시값은 기본적으로 깊은 복사라 기존 변수의 값과 다른 값을 도출해내는 걸 보여준다.

 

3. Spread 연산자 (전개 연산자)

 

const obj = {
	a : 1,
    b : {
    	c : 2
    }
};

const copyObj = {...obj}

copyObj.b.c = 3;

console.log(obj === copyObj) // false
console.log(obj.b.c === copyObj.b.c) // true

 


 

깊은 복사 Deep Copy 

깊은 복사란 복사된 객체가 다른 주소를 참조하여 내부의 값만 복사된 것을 말한다. 

 

깊은 복사 방법

1. JSON.parse & JSON.stringify

JSON.stringify()는 객체를 json 문자열로 변환하는데 이 과정에서 원본 객체와 참조가 모두 끊어진다. 객체를 json 문자열로 변환한 후, JSON.parse()를 이용해 다시 원래 객체로 만들 수 있다. 단점은, 다른 방법에 비해 느리고, 객체가 function일 경우 undefined로 처리한다.

 

const original = {
	a : 'a',
    number : {
    	one : 1,
        two : 2
    },
    
    array : [1,2,[3,4]]
}

let copy = JSON.parse(JSON.stringify(original));

copy.number.one = 2;
copy.array[2].push(5);

console.log(original === copy); // false
console.log(original.number.one === copy.number.one); // false
console.log(original.array === copy.array); // false

console.log(original) 
// { a: 'a', number : { one : 1, two : 2}, array : [1,2,[3,4]]}
console.log(copy)
// { a: 'a', number : { one : 2, two : 2}, array : [1,2,[3,4,5]]}

 

2. 재귀함수를 통한 복사

 

재귀 함수의 장단점은 다음 ?시간에 .. 

 

const orginal = {
	a : 'a',
    number : {
    	one : 1,
        two : 2
    },
    array : [1, 2, [3, 4]]
};

function deepCopy(original) {
	if(original === null || typeof original !== 'object') return;
    
    const copy = Array.isArray(original) ? [] : {};
    
    for(let key of Object.keys(original)){
    	copy[key] = deepCopy(original[key]);
    }
    
    return copy;
}

let copy = deepCopy(original);

copy.number.one = 3;
copy.array[2].push(5);

console.log(original === copy) // false;
console.log(original.number.one === copy.number.one); //false
console.log(original.array === copy.array); // false

console.log(original)
// { a : 'a', number : { one : 1, two : 2 }, array : [1, 2, [3, 4]]}
console.log(copy)
// { a : 'a', number : { one : 3, two : 2 }, array : [1, 2, [3, 4, 5]]}

 

3. Lodash 라이브러리 사용

 

라이브러리를 사용하면 쉽고 더 안전하게 깊은 복사를 할 수 있다. 코딩 테스트 때는 사용할 수 없다는 것이 단점이다.

 

const deepCopy = require('lodash.clonedeep')

const original = {
	a : 'a',
    number : {
    	one : 1,
        two : 2
    },
    array : [1, 2, [3, 4]]
};

let copy = deepCopy(original);

copy.number.one = 3;
copy.array[2].push(5);

console.log(original === copy) // false;
console.log(original.number.one === copy.number.one); //false
console.log(original.array === copy.array); // false

console.log(original)
// { a : 'a', number : { one : 1, two : 2 }, array : [1, 2, [3, 4]]}
console.log(copy)
// { a : 'a', number : { one : 3, two : 2 }, array : [1, 2, [3, 4, 5]]}