CS 상식/소프트웨어 공학

[JS] 값을 복사하는 여러 방법과 차이점 - 얕은 복사 / 깊은 복사

SparkIT 2025. 1. 29. 19:40

프로그래밍 언어를 사용하다보면 변수를 선언하고 해당 변수값과 같은 값을 가지는 다른 변수를 선언하고 싶은 경우가 많이 생깁니다. 하지만 이때 값을 복사하는 방식에 따라 차이점이 존재하고 이로 인해 예기치 못한 문제가 발생할 수 있습니다. 오늘은 자바스크립트에서 값을 복사하는 방법들과 그 차이점에 대해 알아보겠습니다.

( 시작하기에 앞서 기본적으로 자바스크립트의 데이터 타입에 대한 간단한 이해가 필요합니다. )

 

[Javascript] 원시 타입, 참조 타입에 대한 이해

원시 타입(Primitive Type)이란?✅ 정의원시 타입 데이터는 값 자체를 저장하는 데이터 타입을 의미합니다. 예를 들어, 'A'라는 변수에 '10'이라는 값을 지정한다면 스택 메모리에 'A'라는 변수는 '10'이

sparkit.tistory.com

참조 타입과 연관된 얕은 복사, 깊은 복사

우선 원시 타입얕은 복사, 깊은 복사 등의 개념과 관련 없다고 생각하셔도 됩니다. 그 이유는 원시 타입은 값 자체를 스택 메모리에 바로 저장하는 방식이기 때문에 복사하는 과정에서 항상 완전하게 복사되기 때문입니다.

이와 반대로 참조 타입은 복사하는 과정에 여러 방법과 차이가 존재합니다.

 


얕은 복사(Shallow Copy)란?

✅ 정의

참조값(힙 메모리 주소)을 복사하는 것을 의미합니다. 참조값이 가리키는 실제 원본 데이터에는 아무런 영향을 끼치지 않습니다. 즉, 참조값만 가지고 있으면 해당 원본 데이터를 공유할 수 있는 것입니다. 이런 이유로 원본 데이터에 대한 수정은 참조 복사된 변수 모두에 영향을 끼칩니다.

 

얕은 복사 - MDN Web Docs 용어 사전: 웹 용어 정의 | MDN

객체의 얕은 복사는 복사본의 속성이 복사본이 만들어진 원본 객체와 같은 참조 (메모리 내의 같은 값을 가리킴)를 공유하는 복사입니다. 따라서 원본이나 복사본을 변경하면, 다른 객체 또한

developer.mozilla.org

변수 value_1, value_2, value_3, value_4가 모두 참조 복사되었다고 가정하면, 결국 하나의 동일한 데이터를 바라보고 있는 형태다

위와 같이 value_1 변수를 얕은 복사로 value_2, value_3, value_4 변수를 만들어냈다고 가정합니다. 그럼 value_1, value_2, value_3, value_4는 모두 하나의 값인 { name: "Kevin", age: 25 } 을 공유하고, 어떤 변수를 수정하든 모든 변수가 모두 영향을 받습니다.


⚠️ 주의할 점

기본적으로 얕은 복사는 참조값을 공유하는 방식입니다. 하지만 1차원 배열 혹은 1차원 객체에서 깊은 복사처럼 동작합니다. 얕은 복사와 깊은 복사를 구분할 수 있는 더 정확한 조건은 중첩된 배열 혹은 중첩된 객체에서 내부값들이 완전히 복사되는지 여부입니다. 이는 아래 예시를 통해 더 자세하게 설명하겠습니다.

 

📝 예제 모음

⭐️ '=' 연산자

1. 먼저 person 변수를 선언하고 { name: "Kevin", age: 25 }라는 값을 초기화시킵니다. 즉, person 변수는 참조 타입 변수가 되고 아래와 같은 메모리 구조를 가집니다.

let person = { name: "Kevin", age: 25 };

 

2. 이때 alien 변수를 선언하고, = 연산자를 이용해 person과 동일한 값으로 복사합니다. 이때 alien 변수는 메모리에 다음과 같이 저장됩니다.

// let person = { name: "Kevin", age: 25 };
let alien = person;

즉, 실제 데이터(힙 메모리에 존재하는 데이터)는 공유합니다. 마치 별칭(alias)와 비슷하죠. 이런 이유로 다음과 같은 일들이 일어납니다.

  • person의 name을 Sujin으로 수정한다면?
    ➡️ alien의 name도 Sujin으로 수정됩니다.
  • alien의 name을 James로 수정한다면?
    ➡️ person의 name도 James로 수정됩니다.

 


⭐️ 스프레드 연산자

1. 먼저 person 변수를 선언하고 { name: "Kevin", age: 25 }라는 값을 초기화시킵니다. 즉, person 변수는 참조 타입 변수가 되고 아래와 같은 메모리 구조를 가집니다.

let person = { name: "Kevin", age: 25 };

 

2. 이때 alien 변수를 선언하고, 스프레드 연산자를 이용해 person과 동일한 값으로 복사합니다. 이때 alien 변수는 메모리에 다음과 같이 저장됩니다.

// let person = { name: "Kevin", age: 25 };
let alien = { ...person };

❗️여기서 궁금한 점이 생길 것입니다.

위와 같은 상황이라면 '얕은 복사'가 아니라 '깊은 복사'가 아닐까요? 새로운 객체를 만들어 서로 다른 참조값(힙 메모리 주소)을 가지고 있습니다. 그럼에도 불구하고 왜 스프레드 연산자로 복사하는 과정을 얕은 복사라고 할까요? 이는 중첩 객체일 경우 조금 달라지기 때문입니다.

 

3. person 변수를 조금 더 복잡하게 초기화시키겠습니다. { name: "Kevin", age: 25, career: { university: "seoul", company: "goolge" }  }라는 값으로 초기화시킵니다.

// let person = { name: "Kevin", age: 25 };
// let alien = { ...person };
person = { name: "Kevin", age: 25, career: { university: "seoul", company: "goolge" }  };

이때 앞서 선언했던 person과 약간 다릅니다. 데이터가 중첩된 객체일 경우는 다음과 같이 모든 객체가 힙 메모리 주소 하나에 모두 저장되지 않습니다. 그림과 같이 나뉘어 저장됩니다.

 

4. 이때 위에서 했던 방식과 동일하게 alien 변수를, 스프레드 연산자를 이용해 person과 동일한 값으로 복사합니다.

// let person = { name: "Kevin", age: 25 };
// let alien = { ...person };
// person = { name: "Kevin", age: 25, career: { university: "seoul", company: "goolge" }  };
alien = { ...person };

이 부분에서 확인할 수 있는 사실은 다음과 같습니다.

  • 중첩된 객체의 최상위 객체는 새로운 힙 메모리에 생성된다( 깊은 복사 )
  • 중첩된 객체의 내부 객체는 참조값을 공유한다( 얕은 복사 )

즉, 스프레드 연산자를 통해 중첩된 객체를 복사하는 경우는 최상위 부분만 깊은 복사되고 내부(하위) 부분은 얕은 복사되기 때문에 얕은 복사라고 부릅니다. 주의할 점에서 언급한 부분이 바로 이 내용입니다.

 

 


⭐️ Array.prototype.slice() / Array.from() / Object.assign() / Object.create() ...

위에서 설명한 스프레드 연산자와 비슷한 방식의 얕은 복사 방법은 위와 같이 여러 가지 존재합니다. 복사가 되는 원리도 동일합니다.

 

 


깊은 복사(Deep Copy)란?

✅ 정의

참조 타입을 복사할 때 값 자체(힙 메모리에 존재하는 값)를 복사하는 것을 의미합니다. 이 경우는 새로운 객체를 새롭게 만들어 내는 것이므로 원본과 복사된 변수는 서로 영향을 주고 받지 않습니다.

 

깊은 복사 - MDN Web Docs 용어 사전: 웹 용어 정의 | MDN

객체의 깊은 복사는 복사본의 속성이 복사본이 만들어진 원본 객체와 같은 참조(메모리 내의 같은 값을 가리킴)를 공유하지 않는 복사입니다. 따라서 원본이나 복사본을 변경할 때, 다른 객체가

developer.mozilla.org

 

변수 value_1을 깊은 복사해 value_2를 만들면 값을 동일하지만 서로 다른 데이터를 각자 바라봅니다

 

📝 예제 모음

⭐️ JSON.parse()와 JSON.stringify()

1. person 변수를 선언하고 { name: "Kevin", age: 25, career: { university: "seoul", company: "goolge" }  }라는 값으로 초기화시킵니다.

let person = { name: "Kevin", age: 25, career: { university: "seoul", company: "goolge" }  };

 

2. alien 변수를 선언하고 JSON.parse와 JSON.stringify를 활용해 깊은 복사해줍니다.

// let person = { name: "Kevin", age: 25, career: { university: "seoul", company: "goolge" }  };
let alien = JSON.parse(JSON.stringify(person));

 

이렇게 깊은 복사를 하게 되면 두 변수는 값만 동일하지 전혀 다른 메모리 주소에 저장되기 때문에 서로 영향을 주고받지 않습니다.

  • person의 name을 Sujin으로 수정한다면?
    ➡️ alien의 name은 Kevin으로 유지
  • alien의 name을 James로 수정한다면?
    ➡️ person의 name은 Kevin으로 유지
  • person의 career.university를 new york으로 수정한다면?
    ➡️ alien의 career.university는 seoul로 유지