[JavaScript] - 얕은 복사, 깊은 복사

Minji Kim·2022년 10월 26일
0
post-thumbnail

React를 사용하며 변수 선언 시 예상치 못 한 결과를 방지하기 위해 작성한 글입니다.

변수 선언을 할 때 배열 또는 객체 값을 복사하는 경우가 많은데, JS에는 정말 다양한 복사 방법이 있다.
타입에 따라 다 다르지만 spread operator, push, assign, ... 등 다 결과만 봤을 때 유사하게 작용하는 것 같지만 복사에도 종류가 있으므로 예상치 못한 코드 결과를 마주칠 수 있다. React에서 아주 민감하게 작용하는 부분이라 꼭 알고 넘어가야 한다.

복사에는 얕은 복사, 깊은 복사 라고 칭하는 복사가 있으며 이에 대해 자세히 살펴보고자 한다.

얕은 복사, 깊은 복사

얕은 복사

  • 두 변수가 동일한 메모리 주소를 가리키게 된다.
  • 원본 객체는 하나인 상태에서 참조 값만 복사한다.
    → 동일한 레퍼런스(참조값, 메모리 주소)를 가진다는 의미

깊은 복사

  • 동일한 값을 가지지만 다른 메모리 주소를 가리킨다.
  • 다른 레퍼런스를 가진다. (= 독립적인 주소를 가지게 됨)
  • 원시 타입처럼 완전한 복사본을 만든다.

추가로, 객체의 경우 변수가 참조 값을 저장하고 있기 때문에 const로 선언해도 내부 프로퍼티 값 변경이 가능한 것이다. (참조 값은 변경 불가, 참조 값이 가리키는 객체는 변경 가능)

개념적으로는 이해가 갈 수 있지만, 실제 코드 결과를 보면서 더 깊게 이해해보자.

얕은 복사 = 단순 복사

const array = [1,2,3,4,5]
const test = array
test[0]=100
// test, array의 0 인덱스 모두 변경

console.log(test)
// output [100,2,3,4,5]
console.log(array)
// output [100,2,3,4,5]

console.log(test===array)
// output true

변수 testarray를 그대로 할당하면 같은 참조값을 따르므로 두 변수를 비교했을 때 같은 값이라 출력된다.

깊은 복사

const person = {
  name: 'minngki',
  age: '2',
  language: 'javascript'
}

const copiedPerson={...person};

console.log(person===copiedPerson)
// output false

spread operator를 사용하여 변수 copiedPersonperson을 얕은 복사한다면 두 변수는 다른 값이라 출력된다.

또한 공부하면서 가장 헷갈렸던 부분은 spread operator를 얕은 복사로서 소개가 많이 되고 있다는 것이다. 아주 틀린 말은 아니지만 얕은 복사로서 사용하면 안 되는 구문이다.

📌 "얕은 복사"와 React의 효율적인 상태업데이트를 위한 "얕은 비교"는 다르다.

깊은 복사의 함정

그러나 MDN 공식 문서에 따르면 주의해야할 점을 확인할 수 있다.

spread operator, assign 메서드 - 2 depth 이상부터는 참조 값을 전달하는 얕은 복사로 동작한다.
즉, 가장 바깥의 값들만 깊은 복사가 가능하고, 안 쪽의 값들은 같은 참조값을 가져 얕은 복사로만 작용한다.

이게 무슨 말이냐 하면,

const person = {
  name: 'minngki',
  age: '2',
  language: {first: 'javascript', second: 'python'}
}

const copiedPerson={...person};

console.log(person===copiedPerson)
// output false

아까 깊은 복사 예시코드에서 2 depth로 객체를 선언해보았다.
1 depth의 객체를 비교했을 때 똑같이 깊은 복사가 된 것 처럼 보이지만,

console.log(person.language===copiedPerson.language)
// output true

2 depth 객체에서부터는 얕은 복사로서 동작하는 것이다.

그렇다면 2depth에서도 복사를 실행하려면 어떻게 코드를 수정하면 될까 ?

...
const copiedPerson={...person, language: {...person.language}};

console.log(person.language===copiedPerson.language)
// output false

완벽한 깊은 복사

그러나 더 복잡한 객체 구조에서는 앞서 소개한 방식이 상당히 노가다스러운 작업일 수 있다.
완벽하게 깊은 복사를 하기 위해서는 다음 세 가지 방법을 참고하면 된다.

1. 재귀적으로 깊은 복사 수행

말 그대로 새로 함수를 선언해서 재귀적으로 수행하는 것을 말한다.

const person = {
  name: 'minngki',
  age: '2',
  language: {first: 'javascript', second: 'python'}
}

const copyObj = (original) => {
  const newObj = {}
  for (const prop in original) {
    newObj[prop] = original[prop]
  }
  return newObj
}

console.log(copyObj(person)===person)
// output false

가장 단순하지만, 이 또한 depth가 길어질수록 Time Complexity(시간 복잡도)가 늘어난다.
알고리즘 공부할 때 저 방식대로 해서 시간 초과로 머리 부여잡고 다시 풀었던 기억이 남.

2. JSON.parse()JSON.stringify() 메서드 사용

JSON.stringify을 통해 객체 전체를 문자열로 변환 후, 다시 JSON.parse을 이용하여 문자열을 객체 형태로 변환하는 것이다.
문자열로 변환하는 순간 참조값이 변경되므로 새로운 객체로서 사용할 수 있다.

const copyObj = (original)=>{
  return JSON.parse(JSON.stringify(original)) 
}

console.log(copyObj(person)===person)
// output false

3. Lodash의 cloneDeep 함수

고차함수 집합 및 함수형 라이브러리라고 하는데, 아직 사용할 일이 없어서 나중에 공부할 예정.. 참고 링크

정리

  1. 변수 선언 시 원본 객체를 수정하는 것보다 깊은 복사를 통해 사용하는 것이 좋다.
  2. depth가 길어질수록 시간 복잡도를 고려하여 spread operator, assign메서드는 지양하자.

참고

https://choar816.tistory.com/154
https://velog.io/@shin6403/Javascript-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%ACPart.1

profile
애기코더 응애

0개의 댓글