질문과 피드백은 언제나 환영!

댓글로 남겨주세요:)

자바스크립트

[Javascript] Shallow copy Deep copy / 객체의 복사

깅강이 2024. 1. 21. 01:40

 

JS 에서도 얕은 복사와 깊은 복사가 존재한다. 각각이 무엇인지 알아보자. 

얕은 복사와 깊은 복사에 대해 설명하기 이전에 Reference value와 Primitive value에 대해 정리하고 들어가자.

 


 

Reference  value

 

복잡한 데이터 타입을 참조할 때 사용되는 개념.기본적으로, 레퍼런스 벨류는 데이터의 실제 값이 아니라, 그 데이터가 저장된 메모리의 주소를 가리키는 참조(Reference)이다. 이러한 참조를 통해 프로그램은 해당 메모리 주소에 저장된 실제 데이터에 접근하고 조작할 수 있다. 

JavaScript에서는 객체, 배열, 함수 등은 레퍼런스 타입으로 취급된다. 이러한 레퍼런스 타입은 변수에 데이터의 실제 값 대신 메모리 주소(레퍼런스)가 저장된다. 예를 들어, 객체를 변수에 할당하면, 실제 객체의 데이터가 변수에 복사되는 것이 아니라, 객체가 저장된 메모리 주소의 참조가 변수에 저장되는 것이다. 

 

 

reference value의 주요 특징은 다음과 같다. 

  1. 공유된 주소: 두 변수가 동일한 객체나 배열을 참조하고 있다면, 한 변수를 통해 객체의 내용을 변경하면 다른 변수를 통해서도 변경된 내용을 볼 수 있다. 이는 두 변수가 같은 메모리 주소를 가리키기 때문이다.
  2. 메모리 관리 효율성: 대규모의 데이터 구조를 다룰 때, 데이터의 복사본을 여러 번 생성하는 것보다 하나의 데이터에 대한 참조를 공유하는 것이 메모리 사용 면에서 더 효율적이다.
  3. 동적인 데이터 구조: 레퍼런스 벨류를 사용하면 프로그램 실행 중에 데이터 구조의 크기나 내용을 쉽게 변경할 수 있다.

 

Primitive value

primitive value는 데이터의 실제 값이 직접 변수에 저장되는 기본 데이터 타입이다. 이러한 값들은 메모리에 있는 특정 위치에 직접 저장되며, 변수에 값을 할당하거나 변수 사이에서 값을 전달할 때 값 자체가 복사된다.

프리미티브 타입의 예로는 다음과 같은 데이터 타입들이 있다.

 

  1. 숫자(Number): 정수 및 실수를 포함한 모든 숫자 타입.
  2. 문자열(String): 텍스트 데이터를 나타내는 문자열.
  3. 불리언(Boolean): 참(True) 또는 거짓(False)의 두 가지 값만을 가질 수 있는 타입.
  4. Undefined: 정의되지 않은 값.
  5. Null: 의도적으로 비어 있는(빈 값) 상태를 나타내는 값.
  6. 심볼(Symbol): ES6에서 도입된 고유하고 변경 불가능한 데이터 타입.
  7.  

특징은 다음과 같다.

  • 불변성(Immutability): 프리미티브 값을 변경할 수 없으며, 값의 변경이 필요한 경우 새로운 값을 생성해야 한다.
  • 값의 복사: 프리미티브 타입의 변수를 다른 변수에 할당하면 값 자체가 복사. 이로 인해 두 변수는 독립적인 값을 가지며, 한 변수의 값이 변경되어도 다른 변수에는 영향을 주지 않는다.

 


 

Shallow copy, 얕은 복사란

 

복사한 사본이 복사하고 싶은 원본과 연결되어 있어서 복사한 사본을 수정하면 원본도 바뀌는 현상이 발생한다.

복사한 사본과 원본이 연결 되어 있는 이유는

얕은 복사를 할 경우 변수가 저장되어 있는 메모리의 주소를 복사해 오기 때문이다. 아래 그림을 보면 이해가 쉽다. 

 

a라는 변수에는 [1,2,3] 이라는 배열이 들어가 있다. 배열의 경우 reference value이기 때문에 a에는 데이터의 실제 값이 아니라, 그 데이터가 저장된 메모리의 주소가 들어가있다.  b에도 역시 배열이 아니라 배열이 저장된 주소가들어있고 그 주소를 따라가면 배열이 저장된 메모리를 찾을 수 있다. 만약 여기서 const c = b라는 구문을 통해 c에다가 b를 복사해보자. 

b에 들어있는 것은 메모리 주소 값이므로 c에도 같은 메모리 주소값이 복사된다. 이때  c 배열에 4를 추가해보자.

그러면 c 메모리 주소에 들어있는 배열에 4를 추가해야 하므로 0x04번 메모리에 저장된 배열에 4를 추가하게 된다.

따라서 0x04 번의 메모리를 가리키고 있는 b 역시 [1,2,3,4]가 된다.

 

 

보통 복사를 하는 목적은 원본과 분리해 수정을 하거나 사용하는 것이기 때문에 얕은 복사는 큰 문제를 일으킬 수 있다.

 

Deep copy

 

복사한 사본이 복사하고 싶은 원본과 상관 없이 값으로 존재하기 때문에  const c = b 처럼 할당연산자로 복사해도 서로 상관 없는 존재로 복사됩니다. 

그런데 primitive 한 value말고 reference value는 어떻게 deep copy를 해야할까? 이에 대한 답을 3가지로 분류해 정리해보았다.

 


 

1. JSON 메소드 사용

stringify 메소드로 객체를 문자열로 변환 -> parse로 다시 문자열을 객체로 변환

 

장점: 간단하다.

단점: Date, undefined, Infinity, NaN 등 객체 데이터에는 쓸 수 없다. 

 

const original = { a : 1, b : {c : 2} };
const deepCopy = JOAN.parse(JSON.stringify(original));

 

 

2. 재귀 함수 사용

 사용자 정의 재귀함수를 작성해 객체의 모든 프로퍼티를 순회 후 중첩된 객체에 대해 재귀적으로 복사를 수행

 

장점: 다양한 타입이 들어있어도 처리 가능, 중첩된 객체 처리 가능

단점: 구현 복잡

function deepCopy(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return obj; // 프리미티브 값이거나 null이면 그대로 반환
    }

    const copy = Array.isArray(obj) ? [] : {};

    for (const key in obj) {
        copy[key] = deepCopy(obj[key]); // 재귀 호출
    }

    return copy;
}

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

 

 

3. 구조 분해 할당 

ES6부터 지원하는 구조분해할당을 사용해 객체,배열을 복사. 1단계만 깊은 복사이다. 즉 객체 안에 객체가 있다면 내부 객체는 얕은 복사가 된다. 이를 유의해 사용해야 한다. 

 

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