본문 바로가기

FrontEnd

JavaScript 얕은 복사와 깊은 복사

JavaScript

자바스크립트 얕은 복사, 깊은복사

const FormInputList = [
  {
    id: 1,
    FormTitle: '첫번째 프로젝트',
    item: [
      {
        id: 1,
        FormTitle: '프로젝트 제목',
        FormInput: '프로젝트 제목을 입력해주세요',
        name: 'name',
        type: 'text',
      },
      {
        id: 2,
        FormTitle: '프로젝트 대표 이미지',
        FormInput: '프로젝트 대표 이미지를 등록해주세요',
        name: 'thumbnailUrl',
        type: 'file',
      },
(...)

  {
    id: 2,
    FormTitle: '투자 목표 및 구성',
    item: [
      {
        id: 1,
        FormTitle: '투자 목표 금액',
        FormInput: '투자 목표 금액을 입력해 주세요',
        name: 'goalAmount',
        type: 'number',
      },
      {
        id: 2,
        FormTitle: '프로젝트 투자 오픈',
        FormInput: '프로젝트 투자 공개일시를 정해주세요',
        name: 'openingDate',
        type: 'date',
      },
      
      (...)

 

해당 데이터로 총 4개의 Form 페이지를 만들고 각각의 Input에 값을 적어 넣을 수 있도록 하려고 했는데 첫 번째 Form 첫 Input 문항에 글을 적게 되면 이상하게 다른 Form들의 첫 Input 문항에도 똑같은 값이 입력되는 현상이 발생했다.
문제의 원인을 찾아보니 자바스크립트의 객체 복사에도 종류가 있었다.

바로 객체의 얕은 복사와 깊은 복사의 차이를 알아야 이 문제를 해결할 수 있었다..

 

데이터 타입

JavaScript의 데이터 타입은 크게 두 가지로 구분할 수 있습니다: 기본형 데이터와 참조형 데이터입니다. 기본형 데이터는 다음과 같은 종류가 있습니다: Number, String, Boolean, Null, Undefined, Symbol 등이 있습니다. 기본형 데이터는 변수에 직접 값을 할당할 때 해당 값 자체가 변수에 저장됩니다. 예를 들어, var a = 'abc';라는 코드에서 변수 a에는 'abc'라는 문자열 값이 직접 메모리에 할당됩니다. 반면에 참조형 데이터는 다음과 같은 종류가 있습니다: Object, Array, Function, Date 등이 있습니다. 참조형 데이터는 변수에 할당될 때, 해당 값의 메모리 주소가 변수에 저장됩니다. 이는 실제 데이터가 메모리 영역의 주소에 할당되어 있는 것을 의미합니다. 이러한 데이터 타입의 구분은 데이터 변환을 자유롭게 할 수 있게 하고 동시에 메모리를 효율적으로 관리하기 위한 목적으로 이루어집니다. 기본형 데이터는 값 자체가 변수에 저장되기 때문에, 변수 간의 값을 복사할 때 값 자체가 복사되어 독립적인 값으로 사용됩니다. 이는 데이터의 변경이 다른 변수에 영향을 주지 않고 자유롭게 이루어질 수 있는 장점을 제공합니다. 반면에 참조형 데이터는 변수에는 실제 데이터가 저장된 메모리 주소가 할당되어 있기 때문에, 변수 간에 값이 공유됩니다. 즉, 여러 변수가 동일한 데이터를 참조하고 있는 것이죠. 이는 데이터의 효율적인 공유와 메모리 관리를 가능하게 해 줍니다. 이러한 데이터 타입의 이해는 JavaScript에서 변수와 데이터 처리에 있어서 중요한 역할을 합니다. 데이터 타입에 대한 이해를 바탕으로 적절한 변수의 사용과 데이터 변환을 수행할 수 있으며, 메모리 관리와 성능 최적화에도 도움이 됩니다.

 

변수 복사

원시값은 값을 복사할 때 다른 메모리에 복사된 값을 할당하기때문에 원래의 값과 복사된 값들은 서로 영향을 없다.

하지만 참조값은 값을 복사하는 게 아니라 참조(메모리의 주소)를 복사한다.

이 메모리의 주소가 바로 핵심이다.

 

let a = 10;
let b = a;
let obj1 = { c: 10, d: 'ddd'};
let obj2 = obj1;

b = 15;
obj.c =20;

 

위와 같은 경우에는

a !== b
obj1 === obj2

 

이런 결과를 봤을때 객체를 복사하고 값을 변경했을 때 원본 값을 지켜주는 불변 객체를 만들어 주는 원칙을 잘 지켜야 한다.

 

얕은 복사

얕은 복사는 가장 상위 객체만 새로 생성하고 내부 객체들은 참조 관계인 경우를 뜻한다.

복사가 되는 방식은 바로 아래 단계의 값만 복사하는 방법으로 이루어진다.

 

1. 전개 연산자

가장 쉬운 방법으로 흔히 사용되는 방식이다. 

const obj = {
  a: 1,
  b: {
    c: 2,
  },
};

const copiedObj = {...obj}

copiedObj.b.c = 3
console.log(copiedObj.b.c) // 3
console.log(obj.b.c) // 3

obj === copiedObj // false
obj.b.c ===copiedObj // true

 

2. Object.assing()

const obj = {
  a: 1,
  b: {
    c: 2,
  },
};

const copiedObj = Object.assign({}, obj);

copiedObj.b.c = 3
console.log(copiedObj.b.c) //3
console.log(obj.b.c) //3

obj === copiedObj // false
obj.b.c === copiedObj.b.c //true

 

3. Array.slice()

 

arr = [1,2,3]
// 얕은복사
arr2 = arr.slice();

// 결과확인
arr2 // [1,2,3]
arr2[0] = 4;
arr2 // [4,2,3]
arr1 // [1,2,3

 

직렬화 & 역직렬화

자바스크립트의 복사 깊은 복사를 학습하기 앞서 중요한 개념이다

 

직렬화 또는 (serialization)은 데이터 구조나 객체 상태를 다른 컴퓨터 환경에 저장하거나 전송하기 위해 동일한 포맷으로 변환하는 과정입니다. 이를 위해 데이터를 일련의 바이트로 변환하여 저장하거나 전송할 수 있습니다. 반대로, 역직렬화(deserialization)는 일련의 바이트로부터 데이터 구조를 추출하여 원래의 객체 상태를 재구성하는 과정이다.

직렬화/역직렬화 과정을 보면 객체는 무조건 새로운 객체를 생성합니다. 참조 타입이든 값 타입이든 전부 깊은 복사를 수행하여 독립적인 객체를 생성할 수 있습니다..

NSCoding: Archiving과 Distribution을 위해 인코딩/디코딩을 가능하게 해주는 프로토콜입니다.

이 프로토콜은 NSObject로부터 상속을 받으므로 클래스에서만 사용할 수 있습니다.

 

Codable: JSON과 같은 외부 표현 형식으로 데이터를 인코딩/디코딩할 수 있게 해주는 프로토콜입니다. 이 프로토콜은 값 타입이나 참조 타입 모두에서 사용할 수 있습니다. 다만, 객체의 다형성을 지원하지 않는 한계가 있습니다.
이를 통해 직렬화/역직렬화 과정에서 데이터를 안전하게 저장하고 전송할 수 있으며, 다른 환경에서도 데이터를 재구성할 수 있습니다.

 

깊은 복사

깊은 복사는 내부 객체까지 모두 새로 생성하여 복사한 것을 의미합니다.

 

1. 재귀를 통한 복사

 

반복문이 작동하면서 복사 대상이 참조형 타입일 경우 그 내부의 값들도 하나하나 전부 새로 복사하여 생성한다.

 

let arr = [1, 2, [3, 4]];

function copy(someArr) {
  let result = [];
  
  for (let el of arr) {
    if (typeof el === 'object') {
      result.push(copy(el));
    } else {
      result.push(el);
    }
  }
  
  return result;
}

let copyArr = copy(arr); 
// copyArr = [1, 2, [3, 4]];

copyArr[2].push(5);

// arr = [1, 2, [3, 4]];
// copyArr = [1, 2, [3, 4, 5]];

 

2. JSON.stringify, JSON.parse

 

JSON.stringify는 데이터를 문자열로 변환하고, JSON.parse는 변환된 문자열을 다시 원래의 객체로 변환해 줍니다. JSON.stringify를 사용할 때는 원본 객체와의 참조가 모두 끊어지게 되며 JSON.parse를 사용할 때는 새로운 객체가 생성되어 원본 객체와는 다른 주소를 참조하게 되는 방식입니다.

하지만 주의해야 할 점은 JSON.stringify 시에는 메서드, 숨겨진 프로퍼티, 그리고 getter/setter와 같이 JSON으로 직접 변환할 수 없는 프로퍼티들은 무시되게 됩니다. JSON은 데이터를 단순한 키-값 쌍의 형태로 표현하기 때문에 이러한 복잡한 속성들은 이 과정에서 제외됩니다.

또한  JSON 방식은 데이터 양이 많아질수록 성능이 저하될 수 있다는 점에도 주의해야 하는데 JSON.stringify와 JSON.parse는 데이터를 직렬화하고 역직렬화하는 과정에서 문자열의 변환 작업이 필요하며, 이는 큰 규모의 데이터에서 성능 이슈를 야기할 수 있습니다.

따라서 JSON.stringify와 JSON.parse를 사용할 때는 데이터 구조와 용량을 고려하여 적절한 사용을 해야 합니다. 데이터가 많거나 복잡한 경우에는 다른 깊은 복사 방식을 사용해봐야 한다.

 

3. 라이브러리

 

사실 2번의 방법은 왠만하면 사용되지 않습니다. 요즘엔 잘 만들어진 라이브러리를 사용하는 것이 가장 안정성이 있기 때문에 immer 라이브러리 혹은 immutable 라이브러리를 사용한다면 쉽게 깊은 복사가 가능할뿐더러  Redux, Recoil 같은 글로벌 상태 관리 앱에서도 보다 간편하게 깊은 복사를 사용할 수 있습니다.

이와 같은 방법으로 내부 객체까지 깊은 복사를하여 원본 객체의 불변성을 유지된다면

더 효율적이고 쉽게 객체를 이용한 개발이 가능합니다.

'FrontEnd' 카테고리의 다른 글

TypeScript & 제네릭  (0) 2024.03.20
JWT  (0) 2024.03.20
What is JavaScript? And React?  (1) 2024.03.19
DOM  (0) 2023.11.03
CSS flexbox  (1) 2023.11.02