Post

얕은, 깊은 복사와 불변성 유지방법

복사 개념과 데이터 불변성 유지방법을 살펴보자.

얕은 복사, 깊은 복사


얕은 복사(Shallow Copy)


메모리 주소

얕은 복사는 새로운 변수가 원본과는 다른 메모리 주소를 가지지만,
원본 객체의 내부 객체나 배열의 경우에는 해당 객체나 배열의 참조를 복사하여 복사본에 저장한다.

따라서 복사된 객체의 내부 객체나 배열은 원본과 동일한 메모리 주소를 가리키게 되어,
내부 객체나 배열의 변경이 원본 객체에도 영향을 준다.

실제 값 내용

얕은 복사는 복사된 변수가 원본의 참조를 공유하기 때문에
만약 복사된 변수 혹은 원본 데이터 둘 중에 하나라도 값이 변경되면 둘 다 변경된다.

즉, 복사된 변수의 내부 객체나 배열의 값이 변경되면 원본 객체의 내부 객체나 배열도 영향을 받게 된다.

주의사항

얕은 복사는 주로 객체나 배열이 단순한 경우에 사용되며, 내부에 중첩된 객체가 있을 때는 주의가 필요하다.



깊은 복사(Deep Copy)


메모리 주소

깊은 복사는 새로운 변수가 완전히 새로운 메모리 주소를 할당받으므로
원본 객체와는 완전히 독립적인 복사본이 생성된다.

따라서 복사된 객체의 내부 객체나 배열은 모두 새로운 메모리 주소를 가리키게 되어,
내부 객체나 배열의 변경이 원본 객체에 영향을 주지 않는다.

실제 값 내용

깊은 복사는 모든 단계의 값이 재귀적으로 복사되므로 원본과 완전히 독립적인 복사본이 생성된다.
따라서 복사된 변수의 내부 객체나 배열이 변경되어도 원본 객체에는 영향을 주지 않으며,

마찬가지로 원본 객체의 내부 객체나 배열이 변경되어도 복사본에는 영향을 주지 않는다.

깊은 복사는 객체나 배열의 내부에 중첩된 객체가 있을 때 사용되며,
불변성을 유지해야 하는 경우에 매우 유용하다.

주의사항

이렇게 얕은 복사와 깊은 복사는 객체나 배열을 다룰 때 중요한 개념으로, 상황에 따라 적절한 복사 방식을 선택해야 한다.



불변성을 유지하려면 어떻게 해야하는가?


Object.freeze() 사용

객체를 동결하여 속성의 변경을 금지한다. 하지만 내부 객체의 변경까지는 방지하지 않는다.


Object.assign() 혹은 Spread연산자 사용

새로운 객체를 생성하고, 기존 객체의 속성을 복사하여 변경 할 수 있다.


Array메서드 사용

배열 메서드를 사용하여, 새로운 배열을 생성하거나 기존 배열을 수정하지 않고 요소추가 혹은 삭제할 수 있다.

여기서 자주 사용하는 Spread연산자를 예시로 들어보겠다.

스프레드 연산자는 복사하는 과정에서 객체나 배열의 구조를 그대로 유지하기 위해
내부 객체나 배열의 참조를 복사한다.

이로 인해 스프레드 연산자를 통해 생성된 복사본은 1뎁스만 깊은 복사가 이루어지며
1뎁스 이상의 내부 객체나 배열의 변경은 얕은 복사로 인해 복사본에도 영향을 준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const originalObject = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    country: 'USA'
  }
};

// Spread 연산자를 사용한 얕은 복사
const shallowCopyObject = { ...originalObject };

// 원본 객체 값 변경
originalObject.name = 'Kim';
originalObject.age = 35;
originalObject.address.city = 'Seoul';
originalObject.address.country = 'Korean';

console.log(originalObject);
// 출력: { name: 'Kim', age: 35, address: { city: 'Seoul', country: 'Korean' } }

console.log(shallowCopyObject);
// 출력: { name: 'John', age: 30, address: { city: 'Seoul', country: 'Korean' } }

위의 코드에서는 원본 객체의 name, age, address 속성의 값을 변경했다.
1뎁스의 프로퍼티인 name과 age는 값을 변경해도 깊은 복사로 인해 값이 따로 적용되지만
2뎁스인 address 프로퍼티의 값들은 얕은 복사로 적용되어 원본과 복사본의 값이 같은 것을 확인할 수 있다.

이로 인해 스프레드 연산자는 1뎁스의 범위만 깊은 복사가 이루어진다는 사실을 알 수 있다.


문제해결

뎁스에 상관없이 깊은 복사를 이루어내려면 JSON.parseJSON.stringify를 활용해야 한다.

예시코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const originalObject = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    country: 'USA',
    details: {
      street: '123 Main St',
      zip: '10001',
      coordinates: {
        latitude: 'a',
        longitude: 'b'
      }
    }
  }
};

// 깊은 복사 함수
function deepCopy(obj) {
  return JSON.parse(JSON.stringify(obj));
}

// 깊은 복사 수행
const deepCopyObject = deepCopy(originalObject);

// 원본 객체 값 변경
originalObject.name = 'Kim';
originalObject.age = 35;
originalObject.address.city = 'Seoul';
originalObject.address.country = 'Korean';
originalObject.address.details.street = '456 Elm St';
originalObject.address.details.zip = '20002';
originalObject.address.details.coordinates.latitude = 'c';
originalObject.address.details.coordinates.longitude = 'd';

console.log(originalObject);
// 출력:
// {
//   name: 'Kim',
//   age: 35,
//   address: {
//     city: 'Seoul',
//     country: 'Korean',
//     details: {
//       street: '456 Elm St',
//       zip: '20002',
//       coordinates: { latitude: 'c', longitude: 'd' }
//     }
//   }
// }

console.log(deepCopyObject);
// 출력:
// {
//   name: 'John',
//   age: 30,
//   address: {
//     city: 'New York',
//     country: 'USA',
//     details: {
//       street: '123 Main St',
//       zip: '10001',
//       coordinates: { latitude: 'a', longitude: 'b' }
//     }
//   }
// }

JSON.parseJSONstringify를 사용하여 객체를 완전히 새로운 복사본에 변환시킬 수 있다.
이를 통해 복사된 객체는 원본 객체와 독립적으로 동작하게 된다.

주의사항


이 방법은 완전한 복사본이 되고, 뎁스에 상관없이 모든 객체와 배열을 완벽하게 복사할 수 있다.
하지만 이 방법은 함수나 원시 타입의 속성을 복사하지 않는다는 점에 주의해야 한다.

완전한 복사방법

함수나 원시 타입 속성까지 포함하여 완전한 깊은 복사를 하려면
외부 라이브러리를 사용하는 것이 바람직하다.

Lodash 라이브러리를 이용한 깊은 복사 예제코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const _ = require('lodash');

// 깊은 복사를 하려는 객체
let obj = {
    a: 1,
    b: {
        c: 2,
        d: [3, 4, 5],
        e: {
            f: 6
        }
    },
    g: function() {
        console.log('Hello');
    }
};

// Lodash를 사용하여 깊은 복사
let newObj = _.cloneDeep(obj);

// 복사된 객체를 변경해도 원본에는 영향을 주지 않음
newObj.a = 10;
newObj.b.c = 20;
newObj.b.d.push(7);
newObj.b.e.f = 30;

// 원본 객체 출력
console.log("원본 객체:", obj);

// 복사된 객체 출력
console.log("복사된 객체:", newObj);

결론


얕은 복사깊은 복사는 객체나 배열을 다룰 때 매우 중요한 개념이다.

얕은 복사는 복사본의 메모리주소가 원본 메모리주소를 참조하고 있어서 원본 값이 변경되면
메모리 주소가 같기 때문에 똑같이 바뀌게 된다.

반면

깊은 복사는 새로운 메모리공간에 복사된 값을 할당하여 완전히 독립적인 복사본을 생성한다.

또한, 불변성을 유지하려면 얕은 복사보다는 깊은 복사를 사용해야되며,
이를 위해 JSON.parseJSON.stringify를 활용한다.

하지만 JSON.parseJSON.stringify를 이용한 방법은 함수나 원시 타입 속성은 복사되지 않는다.
따라서 모든 값을 깊은 복사하려면 외부라이브러리를 사용하자.

얕은 복사는 주로 객체나 배열이 단순한 경우 사용되며, 내부에 중첩된 객체가 있을 때는 주의해야 한다.
-> 위의 스프레스 연산자 코드참고.

따라서 상황에 따라 적절한 복사 방식을 선택하여 불변성을 유지하는 것이 중요하다.

This post is licensed under CC BY 4.0 by the author.