서론
오랜만에 타입스크립트를 다루고 있습니다. 특히 코딩 테스트를 위해서 다루고 있는데, 오랜만에 타입스크립트의 배열을 다루다보니 얕은 복사와 깊은 복사에 관한 문제가 발생했습니다. 예전에 공부했던 내용이지만 정리한 적은 없는 것 같아 한 번 정리하고 넘어가려고 합니다.
TypeScript의 값 분류
TypeScript(JavaScript)는 "객체 기반 언어"(Object-Oriented Programming Language)입니다.
이는 객체를 활용하여 데이터를 구성하고 조작하는 데 최적화되어 있음을 의미합니다. 배열도 객체로 다루어지며, 대부분의 연산은 객체 참조를 기반으로 동작합니다. 대신 모든 값이 객체인 것은 아닙니다.
Primitive Value (기본형 값)
- 불변형(Immutable) 값입니다.
- 값을 직접 복사하며, 다른 변수에 영향을 미치지 않습니다.
-
기본형 값에는 다음이 포함됩니다.
number
,string
,boolean
,null
,undefined
,symbol
,bigint
-
메모리 저장 방식
- Primitive 값은 스택(Stack)에 직접 저장됩니다.
- 값 자체가 변수에 저장되며, 독립적으로 동작합니다.
Reference Value (참조형 값)
- 객체처럼 동작하며, 값이 아닌 참조를 복사합니다.
-
참조형 값에는 다음이 포함됩니다.
object
,array
,function
,Map
,Set
,Date
등
-
메모리 저장 방식
- 참조형 값은 힙(Heap)에 저장되며, 변수에는 객체의 메모리 주소(참조)가 저장됩니다.
- 따라서, 여러 변수가 동일한 객체를 참조할 수 있습니다.
Primitive vs Reference 차이
Primitive Value 예시 (Immutable)
let a = 42;
let b = a;
b = 100;
console.log(a); // 42 (원본 값은 변경되지 않음)
console.log(b); // 100
a
와 b
는 독립적인 값을 가지고 있으며, 하나를 변경해도 다른 값에 영향을 미치지 않습니다.
Reference Value 예시 (Mutable)
const arr1 = [1, 2, 3];
const arr2 = arr1; // 참조 복사
arr2[0] = 100;
console.log(arr1); // [100, 2, 3] (원본 배열도 변경됨)
console.log(arr2); // [100, 2, 3]
arr1
과 arr2
는 동일한 배열 객체를 참조합니다. 하나를 수정하면 다른 것도 영향을 받습니다.
얕은 복사와 깊은 복사
얕은 복사란?
- 얕은 복사(Shallow Copy)는 객체의 1차원 수준만 복사하는 것을 의미합니다.
- 배열이나 객체를 복사할 때, 최상위 수준의 값만 복사되며, 만약 내부 요소가참조형 값(Reference Value)이라면, 이 값들은 참조(주소)만 복사됩니다.
- 따라서, 얕은 복사된 객체나 배열은 원본과 일부 데이터를 공유하게 되어, 한쪽을 수정하면 다른 쪽도 영향을 받을 수 있습니다.
깊은 복사란?
- 깊은 복사(Deep Copy)는 객체의 모든 수준을 복사하여, 원본과 완전히 독립적인 객체를 생성합니다.
- 중첩된 데이터(예: 객체 안의 객체, 배열 안의 객체)까지 새로운 메모리를 할당합니다.
얕은 복사의 동작
얕은 복사가 어떻게 작동하는지 예시를 통해 메모리 구조와 함께 자세히 알아보겠습니다.
예제 1: Primitive Value
const arr1 = [1, 2, 3];
const arr2 = [...arr1]; // 얕은 복사
arr2[0] = 100;
console.log(arr1); // [1, 2, 3]
console.log(arr2); // [100, 2, 3]
위는 언뜻 보기에는 깊은 복사로 동작하는 것으로 보이지만, const arr2 = [...arr1];
는 얕은 복사를 시행합니다. "깊은 복사처럼 작동한다"는 것은 단지 불변형 값 덕분에 두 배열이 독립적이기 때문입니다.
메모리 구조
arr1
이 생성되면, 배열 자체는 힙(Heap)에 저장되고, 각 요소(1
,2
,3
)는 Primitive Value이므로 스택(Stack)에 저장됩니다.-
const arr2 = [...arr1]
- 스프레드 연산자(
...
)는arr1
의 각 요소를 복사하여 새로운 배열arr2
를 만듭니다. - 배열 자체는 새로운 객체로 힙(Heap)에 저장되지만, 복사된 Primitive 값들은 각각 스택(Stack)에 저장됩니다.
- 스프레드 연산자(
-
arr2[0] = 100
arr2
의 첫 번째 값을 스택(Stack)에서 변경합니다.arr1
은 독립적인 배열이므로 영향을 받지 않습니다.
예제 2: Reference Value
const arr1 = [{ a: 1 }, { b: 2 }, { c: 3 }];
const arr2 = [...arr1]; // 얕은 복사
arr2[0].a = 100;
console.log(arr1); // [ { a: 100 }, { b: 2 }, { c: 3 } ]
console.log(arr2); // [ { a: 100 }, { b: 2 }, { c: 3 } ]
이 예제에서는 "얕은 복사"의 문제가 발생하며, 참조가 공유됩니다.
메모리 구조
-
arr1
이 생성되면, 배열 자체는 힙(Heap)에 저장되고, 배열의 각 요소({ a: 1 }
,{ b: 2 }
,{ c: 3 }
)도 힙(Heap)에 저장됩니다.arr1
배열은 힙에 저장된 객체들의 참조(주소)를 가집니다.
-
const arr2 = [...arr1]
:- 스프레드 연산자(
...
)는arr1
의 각 요소의 참조(주소)를 복사하여 새로운 배열arr2
를 만듭니다. - 배열
arr2
는 새로운 객체로 힙(Heap)에 저장되지만, 각 요소는arr1
과 동일한 객체를 참조합니다.
- 스프레드 연산자(
-
arr2[0].a = 100
:arr2[0]
이arr1[0]
과 동일한 객체를 참조하므로, 변경이 두 배열에 영향을 미칩니다.
결론
얕은 복사는 항상 1차원만 복사하는 것이 핵심입니다. 하지만, 배열의 요소가 불변형 값(Primitive Value)인지, 참조형 값(Reference Value)인지에 따라 결과가 달라질 뿐입니다.
얕은 복사는 항상 동일한 작업을 수행합니다.
- 1차원 요소만 복사하고, 요소가 불변형이면 값 복사, 참조형이면 참조 복사.
결과 차이는 배열 요소의 유형(불변형 vs 참조형)에 따라 다릅니다.
- 불변형 값 → 깊은 복사처럼 작동.
- 참조형 값 → 얕은 복사로 인해 참조 문제 발생.
요약
-
얕은 복사의 본질
- 배열이나 객체의 1차원 요소만 복사합니다.
- 요소가 불변형 값이면 값을 복사하고, 참조형 값이면 참조를 복사합니다.
-
결과의 차이
- 배열의 요소가 불변형 값이면, "깊은 복사처럼" 보일 뿐입니다.
- 배열의 요소가 참조형 값이면, "얕은 복사처럼" 참조를 공유합니다.
얕은 복사와 깊은 복사를 언제 사용해야 할까?
1. 얕은 복사(Shallow Copy)
-
언제 사용하나요?
- 데이터가 불변형 값(Primitive Value)로만 이루어진 경우.
- 객체나 배열을 복사할 때, 수정하지 않는 1차원 데이터만 필요할 때.
-
장점:
- 수행 속도가 빠르고, 메모리를 덜 사용.
- 간단한 데이터 구조에서는 충분히 적합.
2. 깊은 복사(Deep Copy)
-
언제 사용하나요?
- 데이터에 참조형 값(Reference Value)이 포함된 경우.
- 데이터가 중첩된 구조(객체 안의 객체, 배열 안의 배열)를 가지는 경우.
- 복사본이 원본과 완전히 독립적이어야 할 때.
-
장점:
- 원본 데이터와 완전히 독립적이므로, 수정해도 영향을 주지 않음.
결론
- 복사 작업에서 얕은 복사와 깊은 복사의 차이를 명확히 이해하고, 데이터 구조와 사용 목적에 따라 올바른 방법을 선택해야 합니다.
- 단순히 "복사다!"라고 덤비면 참조 문제나 의도치 않은 결과가 발생할 가능성이 큽니다.
- 문제가 복잡하거나 확신이 없을 때는 깊은 복사를 기본으로 고려하는 것이 안전합니다.