[JavaScript] Deep copy 와 Shallow copy 에 대한 이해

데브쟁이·2021년 9월 28일
1
post-thumbnail

JavaScript로 개발하면서 Copy(복사)를 수도 없이 해보았을 것입니다. 하지만 잘못 알고 사용하면 함정에 빠지기 쉽습니다. 그리고 함수형 프로그래밍 등에서 말하는 데이터 불변성에 대해서도 들어본 적이 있을 것입니다. 다음에서 JavaScript에서 값을 안전하게 복사하는 방법을 알아 봅시다.

Copy 란?

프로그래밍에서 우리는 변수에 값을 저장합니다. Copy를 한다는 것은 동일한 값으로 새 변수를 초기화 한다는 의미입니다. 그러나 고려해야 할 큰 잠재적 함정이 있습니다. Deep copy(깊은 복사)와 Shallow copy(얕은 복사) 입니다. 깊은 복사는 새 변수의 모든 값이 복사되고 원래 변수에서 연결이 끊어지는 것을 의미합니다. 얕은 복사는 특정 (하위) 값이 여전히 원래 변수에 연결되어 있음을 의미합니다.

복사를 제대로 이해하려면 JavaScript가 값을 저장하는 방법을 알아야 합니다.

Primitive data types

기본 자료형 (Primitive data types) 에는 다음이 포함 됩니다.

  • Number — e.g. 1
  • String — e.g. 'Hello'
  • Boolean — e.g. true
  • undefined
  • null

이러한 자료형으로 변수를 생성하면 변수에는 실제 값이 저장 됩니다. 즉, JavaScript에서 기본 자료형을 복사하는 것에 대해 실제로 걱정할 필요가 없습니다. Copy를 하면 실제로 사본이 됩니다. 예를 들어 보겠습니다.

const a = 5
let b = a // copy
b = 6

console.log(b) // 6
console.log(a) // 5

"b = a" 를 실행하여 복사본을 만듭니다. 이제 b에 새 값을 재할당하면 b의 값은 변경되지만 a는 변경되지 않습니다.

Composite data types — Objects and Arrays

복합 자료형(Composite data types) 의 경우 기본 자료형 과는 다르게 동작 합니다. 기술적으로 배열도 객체이므로 동일한 방식으로 동작합니다. 객체의 경우 실제로 인스턴스화될 때 한 번만 저장되며 변수에 할당하면 해당 값에 대한 포인터(참조)가 생성됩니다. 다음 코드를 봅시다.

const a = {
  en: 'Hello',
  de: 'Hallo',
  es: 'Hola',
  pt: 'Olà'
}

let b = a
b.pt = 'Oi'

console.log(b.pt) // Oi
console.log(a.pt) // Oi

위의 예제에서 "b = a" 실행시에 얕은 복사(Shallow Copy) 가 일어 납니다. 복사 이전 변수가 변경된 값이 아닌 원래 값을 가질 것으로 예상하기 때문에 종종 문제가 발생합니다. 개념을 제대로 파악하지 못한 개발자의 경우 디버깅 하는데 시간을 소요하게 됩니다.

Object 복사

객체(Object) 의 복사본을 만드는 방법에는 여러 가지가 있습니다.

Spread operator

ES2015와 함께 도입된 연산자로 간단하게 객체를 복사 할 수 있습니다. 모든 값을 새 객체로 Spread(확산) 합니다. 다음과 같이 사용할 수 있습니다.

const a = {
  en: 'Bye',
  de: 'Tschüss'
}

let b = { ...a }
b.de = 'Ciao'
console.log(b.de) // Ciao
console.log(a.de) // Tschüss

const c = {...a, ...b}

그리고 c와 같이 두 개체를 병합하는 데 사용할 수도 있습니다.

Object.assign()

이 방법은 스프레드 연산자가 주변에 있기 전에 주로 사용되었으며 기본적으로 동일한 작업을 수행합니다. Object.assign() 메서드의 첫 번째 인수가 실제로 수정되고 반환되기 때문에 주의해야 합니다. 일반적으로 기존 데이터 수정을 방지하기 위해 첫 번째 인수로 빈 객체를 전달합니다.

const a = {
  en: 'Bye',
  de: 'Tschüss'
}

let b = Object.assign({}, a)
b.de = 'Ciao'
console.log(b.de) // Ciao
console.log(a.de) // Tschüss

함정 : Nested Objects

중첩된 객체(Nested Objects) 와 배열의 경우 앞서 두가지 방법(Spread Operator, Object.assign()) 으로 복사 하더라도 해당 객체 내부의 중첩된 객체는 포인터/참조일 뿐이므로 사본이 생성 되지 않습니다. 따라서 중첩된 개체를 변경하면 두 인스턴스 모두를 변경하게 됩니다. (얕은 복사)

const a = {
  foods: {
    dinner: 'Pasta'
  }
}

let b = { ...a }

b.foods.dinner = 'Soup' // 두 객체 모두 변경됨
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Soup

const c = {...a, foods: {...a.foods}} // 이렇게는 가능

Deep copy 1

중첩된 객체가 얼마나 깊은지 모른다면? 큰 객체를 수동으로 살펴보고 중첩된 모든 개체를 손으로 복사하는 것은 매우 지루한 일입니다. 아무 생각 없이 복사하는 방법이 있습니다. 객체를 문자열로 바꾸고 바로 다음과 같이 파싱 합니다.

const a = {
  foods: {
    dinner: 'Pasta'
  }
}

let b = JSON.parse(JSON.stringify(a))
b.foods.dinner = 'Soup'

console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta

Deep copy 2

다음과 같은 함수를 구현하여 객체를 깊은 복사할 수 있습니다. 재귀적으로 모든 객체를 복사합니다.

function deepCopy(obj) {
  if (obj === null || typeof obj !== "object") {
    return obj;
  }

  let copy = {};
  for (let key in obj) {
    copy[key] = deepCopy(obj[key]);
  }
  return copy;
}

Array 복사

배열 복사는 정말 많이 사용합니다. 배열도 결국 객체이기 때문에 복사 방법은 비슷합니다.

Spread operator

객체와 마찬가지로 스프레드 연산자를 사용하여 배열을 복사할 수 있습니다.

const a = [1, 2, 3]
let b = [...a]
b[1] = 4

console.log(b[1]) // 4
console.log(a[1]) // 2

Array functions — map, filter, reduce

이 함수 들은 원래 배열의 모든(또는 일부) 원소가 포함된 새 배열을 반환합니다.

const a = [1, 2, 3]
let b = a.map(el => el)
b[1] = 4

console.log(b[1]) // 4
console.log(a[1]) // 2

Array.slice()

이 함수는 일반적으로 특정 인덱스에서 시작하여 선택적으로 원래 배열의 특정 인덱스에서 끝나는 원소들을 포함하는 배열을 얻고자 할 때 사용됩니다. array.slice() 또는 array.slice(0)로 복사 할 수 있습니다.

const a = [1, 2, 3]
let b = a.slice(0)
b[1] = 4

console.log(b[1]) // 4
console.log(a[1]) // 2

Nested arrays

중첩된 배열의 경우 중첩된 객체와 동일한 이슈가 있습니다. 객체 처럼 문자열로 바꾸어 다시 파싱하는 방법을 사용할 수 있습니다.

const a = [1, [2], 3]
let b = JSON.parse(JSON.stringify(a))
b[1] = [4]

console.log(b[1]) // [4]
console.log(a[1]) // [2]
profile
풀스택 개발자를 꿈꾸는 잡부

0개의 댓글