JavaScript의 shallow and deep copy

서정준·2023년 10월 21일
0

데이터 타입

자바스크립트 데이터 타입은 크게 두가지인 원시형(Primitive Type)과 참조형(Reference Type)으로 분리됩니다. 기본(원시)형에는 Number, String, Boolean, null, undefined 가 있으며 ES6 에서는 Symbol 도 추가되었습니다. 참조형은 대표적으로 객체(Object)가 있고 그 하위에 배열(Array), 함수(Function), 정규표현식(RegExp) 등이 있으며, ES6에서는 Map, Set, WeakMap, WeakSet 등도 추가되었습니다.

두 타입의 가장 대표적인 차이로는 기본형에는 바로 값을 그대로 할당한다는 것이고 참조형에는 값이 저장된 주소값을 할당(참조)한다는 것입니다.

기본형 타입에서 값 할당은 어떻게 이루어 질까?

var a = 10;

코드에서는 변수를 선언하고, 식별자를 a로 주었으며 number 타입인 10을 할당했다. 이 코드를 실행했을 때, 메모리의 한 공간을 확보한 다음, 식별자를 붙인다. 그리고 바로 10을 할당하지 않고 데이터를 저장하기 위한 다른 메모리 공간을 하나 더 확보하고 그곳의 주소를 식별자가 저장된 곳의 값으로 집어넣는다. 그리고 10이라는 값을 집어넣는다.

👉변수는 값의 위치(주소)를 기억하는 메모리 공간인데, 값의 위치란 값이 위치하고 있는 메모리 상의 주소(address)를 의미한다.

  • 선언 과정 : 공간을 확보하고 변수명과 주소를 매칭시키는 과정
  • 할당 과정 : 해당 변수가 가리키는 주소의 공간에 데이터를 저장하는 과정

참조형 타입에서 값 할당은 어떻게 이루어 질까?

var obj = {
    a : 1,
    b : 'b'
};

기본형 타입과 마찬가지로 메모리 공간을 확보하고 주소를 변수명과 매칭시키는 과정은 동일합니다(선언). 다음으로 할당과정을 할 차례인데 할당을 하려고 보니 그 값이 기본형이 아니고 참조형입니다. 참조형 데이터들은 프로퍼티(property)와 데이터(data), 즉 key : value로 묶인 쌍들로 이루어져 있습니다. 기본형 데이터와의 차이는 참조형 데이터 안에 있는 기본형 데이터를 저장하기 위해 기본형 데이터의 주소를 담은 공간을 새로 생성했다는 점이다.

To copy an object in JavaScript, you have three options:

Use the spread (...) syntax 👉얕은 복사
Use the Object.assign() method 👉얕은 복사
Use the JSON.stringify() and JSON.parse() methods 👉깊은 복사

깊은 복사란?

깊은 복사란, 기존 값의 모든 참조가 끊어지는 것을 말한다. 특히 복사할 때, 참조형 타입 값(객체)에서 내부에 있는 모든 값이 새로운 값이 되는 것을 말한다.

기본형 타입에서 깊은 복사

var a = 10;
var b = a;
console.log(a); // 10
console.log(b); // 10

var b = a; 을 수행하면 먼저 var 키워드가 있으니 선언과정부터 거치게 됩니다. 빈 공간인 314번을 확보하고 그 주소를 변수 b와 매칭시킵니다. 그리고 313번으로 이동하여 a값인 10 값을 읽어옵니다. 그리고 읽어들인 값 10를 가지고 b 를 찾아서 b가 가리키는 314번 공간에 10을 넣게됩니다.

복사했을 때, 서로의 주소가 달라졌다. 기본형 타입의 깊은 복사다. 서로에게 영향을 주지 않는다.

참조형 타입의 깊은 복사

var obj1 = {
  a: 10,
  b: 'abc',
};
var obj2 = obj1;
console.log(obj1 === obj2); // true

이렇게 객체 자체의 참조 값을 할당하면 프로퍼티 그룹을 바라보는 주소자체는 변경되지 않았기 때문에, 깊은 복사가 일어나지 않는다. 따라서 아래와 같이 obj2.a값을 변경 했을 경우 동일한 결과가 나온다.

obj2.a = 20;
console.log(obj1); // {a: 20, b: 'abc'}
console.log(obj2); // {a: 20, b: 'abc'}
console.log(obj1 === obj2); // true

그렇다면 깊은 복사를 어떻게 할 수 있을 것인가?

Deep copy example

The following snippet replaces the Object.assign() method by the JSON methods to carry a deep copy the person object:

let person = {
    firstName: 'John',
    lastName: 'Doe',
    address: {
        street: 'North 1st street',
        city: 'San Jose',
        state: 'CA',
        country: 'USA'
    }
};


let copiedPerson = JSON.parse(JSON.stringify(person));

copiedPerson.firstName = 'Jane'; 
copiedPerson.address.street = 'Amphitheatre Parkway';
copiedPerson.address.city = 'Mountain View';

console.log(person);


/*
Output

{
    firstName: 'John',
    lastName: 'Doe',
    address: {
        street: 'North 1st street',
        city: 'San Jose',
        state: 'CA',
        country: 'USA'
    }
}
*/

In this example, all values in the copiedPerson object are disconnected from the original person object.

얕은 복사란?

  • 참조형 타입의 값이 바로 아래 단계의 값만 복사하는 방법이다.
  • React의 경우 얕은 복사를 통해 state값을 비교하고, 변경 되었다면 리랜더링 한다.
var obj1 = {
  a: 1,
  b: {
    c: 2,
  },
};
var obj2 = { ...obj1 };
console.log(obj1 === obj2); // false
console.log(obj1.b === obj2.b); // true

쉽게 말하면 {} 이 껍데기는 새로 생성된 객체이며 새로운 주소를 갖게 되었고 spread 연산자로 풀어진 프로퍼티들은 처음 선언된 obj1의 프로퍼티들이 사용되었다.

Shallow copy example

Consider the following example:

let person = {
    firstName: 'John',
    lastName: 'Doe',
    address: {
        street: 'North 1st street',
        city: 'San Jose',
        state: 'CA',
        country: 'USA'
    }
};


let copiedPerson = Object.assign({}, person);

copiedPerson.firstName = 'Jane'; 						// disconnected

copiedPerson.address.street = 'Amphitheatre Parkway'; 	// connected
copiedPerson.address.city = 'Mountain View'; 			// connected

console.log(copiedPerson);


/*
Here is the output:

{
    firstName: 'Jane',
    lastName: 'Doe',
    address: {
        street: 'Amphitheatre Parkway',
        city: 'Mountain View',
        state: 'CA',
        country: 'USA'
    }
}
*/

However, when you show the values of the person object, you will find that the address information changed but the first name:

console.log(person);

Output:

/*
{
    firstName: 'John',
    lastName: 'Doe',
    address: {
        street: 'Amphitheatre Parkway',
        city: 'Mountain View',
        state: 'CA',
        country: 'USA'
    }
}
*/

The reason is that the address is reference value while the first name is a primitive value.

👉 spread (...) 와 Object.assign()는 one level만 복사한다. two level부터는 shallow copy가 이루어진다.

More formally, two objects o1 and o2 are shallow copies if:

  1. They are not the same object (o1 !== o2).
  2. The properties of o1 and o2 have the same names in the same order.
  3. The values of their properties are equal.
  4. Their prototype chains are equal.

👉 복사된 객체는 명백히 별개의 객체이다.

react에서 사용하는 복사

react에서 DOM을 변경시키는 방법은 props와 상태 값을 변경하는 것이다. 상태 값이 변경되면 React는 새 데이터로 DOM을 다시 그린다. React의 모든 상태값을 불변으로 취급힌다.

const [count, setCount] = useState(0);
(...)

setCount(5);

원시 값은 불변값이므로 count가 0에서 5로 변경되었고(0 값은 메모리에 존재하는데 5가 메모리에 새로 생성되어 count 변수의 주소가 변경되었다) 그렇다면 객체는 어떨까?

const [obj, setObj] = useState({
  a: 10,
  b: {
    c: 'abc',
  },
});

return (
  <div>
    <p>{obj.b.c}</p>
    <button
      onClick={() => {
        obj.b.c = 'def';
        setObj(obj);
      }}
    >
      Click
    </button>
  </div>
);

obj는 새로 만들어진 객체가 아니라, 상태값을 선언할 때 이미 만들어져있었던 obj를 다시 setState 함수에 넣어주었고, obj와 b.c프로퍼티가 변경된 obj는 서로 같은 주소를 가지고 있기 때문에 react 입장에서 동일한 객체라 인식하고 업데이트 하지 않은 것이다.
※ react는 상태를 비교할 때 Object.is() 를 사용한다.
그렇다면 주소를 변경한 Object를 넣어주자.

<button
  onClick={() =>
    setObj({
      ...obj,
      b: { c: 'def' },
    })
  }
>
  Click
</button>

setState에 새로운 객채({})를 만들었고, 얕은 복사로 spread 문법을 활용해 객체를 풀어주었다.(앞서 살펴본 Object.assing() 메서드도 동일하게 동작한다) 그리고 b.c의 값을 변경했다. 잘 동작한다. 그렇다면 깊은 복사도 가능할까?

<button
  onClick={() => {
    const obj2 = JSON.parse(JSON.stringify(obj));
    obj2.b.c = 'def';
    setObj(obj2);
  }}
>
  Click
</button>

역시 마찬가지로 obj가 바라보고 있는 주소 자체가 변경되었기 때문에 깊은 복사도 동작한다. react 공식 Doc에서 지속적으로 등장하는 단어가 있다. 그것은 ‘shallow’ 인데, DOM을 비교할 때에도 react는 얕은 비교를 한다. 왜 깊은 복사도 가능하지만 얕은 복사를 권장할까?
이유는 성능에 있다. react - reconciliation

만약 Object의 깊이가 1000개라고 가정하자. 그리고 999번째의 깊이의 5번째 값이 변경되었다. 그렇다면 Object의 1000개의 깊이를 모두 돌면서 값을 비교해야한다. 비교를 해야하는 연산이 매우 많기 때문에 연산하는 도중에 성능이 낮은 기기에서는 어플리케이션이 죽을 것이다.
하지만, 얕은 비교를 한다면 객체 내부의 깊이가 몇개든 상관없이 가장 외부의 값만 비교를 해서 변경되었는지 판단하고 변경 되었다면 변경된 객체 전체를 새로 상태값에 집어넣어 렌더링을 해주면 된다. 따라서 react에서는 불변 값을 지키며, 얕은 복사의 상태값만 렌더링 되도록 되어 있다.

사전
(출처: Chat GPT)

  • Primitive (원시 데이터 타입)
    이 값들은 메모리에 직접 저장됩니다. 예를 들어, 정수, 부동 소수점 수, 문자열, 불리언 (true 또는 false)은 모두 원시 데이터 유형에 해당합니다. 원시 데이터 유형은 간단하며 메모리 사용량이 비교적 작습니다.
  • reference (참조 데이터 타입)
    변수가 실제 데이터를 포함하는 것이 아니라 데이터의 위치 또는 주소를 가리키는 방식으로 동작하는 것을 의미합니다. 예를 들어, 객체, 배열, 함수 등은 참조 데이터 유형에 해당합니다.
    간단한 비유로 설명하면, 참조 데이터 타입은 우편 번호와 유사합니다. 우편 번호를 사용하면 특정 위치에 있는 집을 찾을 수 있습니다. 집 자체를 우편 번호로 저장하지 않고, 우편 번호를 사용하여 집을 찾습니다.

출처
https://pozafly.github.io/javascript/shallo-copy-and-deep-copy/
https://webclub.tistory.com/638
https://www.javascripttutorial.net/object/3-ways-to-copy-objects-in-javascript/
https://developer.mozilla.org/en-S/docs/Glossary/Shallow_copy

profile
통통통통

0개의 댓글