:ledger: 객체의 불변성을 유지,변경 방법에 대해 알아보자!

불변 객체는 내용을 변경할 수 없는 객체를 의미하며, 얕은 복사와 깊은복사로 객체의 불변성을 유지하거나 변경하는 방법을 알아본다.

:one: 얕은 복사

얕은 복사는 객체의 1차 속성만을 복사하고, 내부 객체는 원본 객체와 같은 참조를 공유한다.

  • 얕은 복사는 객체의 최상위 속성(1차 속성)만 복사한다.
  • 내부 객체나 배열과 같은 2차 속성은 원본 객체와 동일한 참조를 가진다.
  • 얕은 복사는 객체의 구조가 단순하고, 중첩된 객체가 없거나 중첩 객체를 공유해도 문제가 없을 때 사용하기 적합하다.
  • 객체의 깊은 속성(2차 속성 이상)을 변경해야 할 때는 주의가 필요하며 이러한 변경은 원본 객체에도 영향을 미칠 수 있음

:pushpin: 1-1) 1차 속성이란?

1차 속성이라고 하니 처음에 어색했는데, 아래의 예시를 보면 이해할 수 있다.

const person = {
  name: 'rarrit', // 1차 속성
  address: { // 1차 속성
    city: 'seoul', // 2차 속성
  }
};
const personCopy = { ...person };
personCopy.name = "rarrit change"; // 1차 속성 변경
personCopy.address.city = "incheon"; // 내부 객체의 2차 속성 변경

console.log(person.name); // "rarrit change" (1차 속성은 독립적으로 존재)
console.log(person.address.city); // "incheon" (내부 객체는 공유됨)

:pushpin: 1-2) 문법

for-in문을 사용한 얕은 복사
var copyObject = function (target) {
  var result = {
    name: 'minkyu',
  };
  // for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근할 수 있음.  
  // 이 copyObject로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경하면 된다.
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
}

var user = {
  name: 'wonjang',
  gender: 'male',
};

// 위 함수를 사용하여 얕은 복사
var user2 = copyObject(user);
user2.name = 'twojang';

console.log(user2.name); // twojang : user2의 name은 변경됌
console.log(user.name); // wonjang : user의 1차 속성 값은 변경되지 않음

if (user !== user2) {
  console.log('유저 정보가 변경되었습니다.');
}

console.log(user === user2); // false
spread 연산자를 사용한 얕은 복사
// 객체
const x = {a: 1, b: 2};
const y = { ... x };
console.log(y); // a: 1, b: 2

y.a = 2;
console.log(x.a); // 1
console.log(y.a); // 2

// 배열
const arr = [1,2,3];
const arrCopy = [...arr];

console.log(arrCopy); // [1,2,3]
arrCopy.push(4); 

console.log(arr); // [1,2,3]
console.log(arrCopy) // [1,2,3,4]

:pushpin: 1-3) 정리

  • 유의할 점
    • 얕은 복사는 1차 속성만을 복사하며, 중첩된 객체는 원본과 동일한 참조를 가진다.
    • 내부 객체의 변경 사항은 원본 객체에도 영향을 미친다.
    • 중첩 객체가 많은 경우, 얕은 복사는 원본 객체에 영향을 미칠 확률이 커서 그럴 경우 원치않은 오류들을 만날 수 있음
    • 내부 객체의 변경을 원하지 않을 때는 얕은 복사를 사용하면 안된다. (위의 내용 조심!)
  • 사용하기 좋은 예
    • 객체의 상위 속성만 변경할 때 유용함

:two: 깊은 복사

깊은 복사는 객체의 모든 속성(중첩된 객체 포함)을 새롭게 복사하여 원본 객체와 독립된 새로운 객체를 생성함

:pushpin: 2-1) 사용 이유

만약 객체의 모든 속성을 새롭게 복사해야 할 경우, 원본 객체가 변경되지 않아야 할 경우에 사용한다. 아래는 얕은 복사를 사용한 예시이다. [확인] 주석을 확인 해보자.

var copyObject = function (target) {
  var result = {
    name: 'minkyu',
  };
  // for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근할 수 있음.  
  // 이 copyObject로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경하면 된다.
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
}

var user = {
  name: 'wonjang',
  gender: 'male',
  infos: {
    info1 : 1, // [확인] 변경할 값
    infos2 : 2
  }
};

// 위 함수를 사용하여 얕은 복사
var user2 = copyObject(user);
user2.infos.info1 = 1000; // [확인] 중첩된 객체를 변경

console.log(user.infos.info1); // 1000
console.log(user2.infos.info1); // 1000

if (user.infos.info1 === user2.infos.info1) {
  console.log('중첩된 객체가 동일하게 변경되었습니다.'); // [확인] true
}

console.log(user.infos.info1 === user2.infos.info1); // [확인] true
  • 얕은 복사를 할 경우 1차 속성은 변경이 되지만 2차 속성(중첩된 객체)는 동일하게 변경되어, 원본 객체까지 변경이되기에 이럴 경우 깊은 복사를 통해 중첩된 객체까지 복사를 해야한다.
  • 만약 프로젝트를 진행하다가 위와 같이 복사 후 값을 변경하면 .. holy moly..!!

:pushpin: 2-2) 문법

기존 얕은 복사 후 안쪽 객체까지 다시 얕은 복사 실행

아래의 [1] ~ [n] 을통해 설명하겟다.

// [1] 기존 얕은 복사 함수 생성
var copyObject = function (target) {
  var result = {
    name: 'minkyu',
  };
  // for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근할 수 있습니다.
  // 하드코딩을 하지 않아도 괜찮아요.
  // 이 copyObject로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경하면
  // 되겠죠!?
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
}

// 중첩된 객체에 대한 [깊은 복사] 살펴보기
var user = {
  name: 'wonjang',
  urls: {
    portfolio: 'http://github.com/abc',
    blog: 'http://blog.com',
    facebook: 'http://facebook.com/abc',
  }
};

// [2] 1차 copy
var user2 = copyObject(user);

// [3] 2차 copy -> 이렇게까지 안쪽 객체까지 복사를해줘야함
user2.urls = copyObject(user.urls);

user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio);

user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog);

// 결론 : 객체의 프로퍼티 중, 기본형 데이터는 그대로 복사 + 참조형 데이터는 다시 그 내부의 프로퍼티를 복사 ⇒ 재귀적 수행!
JSON.parse(JSON.stringify(original))

이 문법을 사용하면 쉽게 깊은 복사가 가능하다. 아래의 예시를 확인해보자.

const user = {
  name: "rarrit",
  address: {
    city: "seoul",
  }
};

const userCopy = JSON.parse(JSON.stringify(user)); // 깊은 복사
userCopy.name = "rarrit2"; // 1차 속성 변경
userCopy.address.city = "soeoul2"; // 내부 객체 속성 변경

console.log(userCopy.name); // "rarrit2"
console.log(userCopy.address.city); // "seoul2" (원본 객체는 변경되지 않음)
  1. 이해해야 할 점
    • 깊은 복사는 모든 중첩된 객체까지 재귀적으로 복사함
    • 원본 객체와 복사된 객체는 완전히 독립이다.
  2. 조심해야 할 부분
    • 복사 대상이 크거나 깊이가 깊을 경우, 성능에 영향을 미칠 수 있음.
    • JSON.parse(JSON.stringify()) 방법은 함수, undefined, Infinity, NaN 등의 특별한 값들을 처리하지 못함.

:fire: 마무리

JavaScript의 얕은 복사와 깊은 복사를 알아보며, 어떤 차이가 있는지 알 수 있었다.
1차 속성만 간단하게 변경이 필요할 경우 얕은 복사를 사용하고 객체의 불변성을 유지해야 하는 경우에는 깊은 복사를 사용하도록 한다.