[F-Lab 모각코 챌린지 56일차] 불변 객체 Immutable Object

Nami·2023년 7월 26일
0

66일 포스팅 챌린지

목록 보기
56/66

불변 객체 Immutable Object

한 번 생성된 후 그 상태를 변경할 수 없는 객체.

즉, 객체가 생성된 후 그 내부의 상태(값)를 수정할 수 없고, 새로운 값을 가진 새로운 객체를 생성해야 한다.

참조형 데이터의 가변은 데이터 자체가 아닌 내부 프로퍼티를 변경할 때만 성립한다. 데이터 자체를 변경하고자 하면(새로운 데이터를 할당하고자 하면) 기본형 데이터와 마찬가지로 기존 데이터는 변하지 않는다.

그렇다면, 내부 프로퍼티를 변경할 필요가 있을 때 마다 매번 새로운 객체를 만들어 재할당하기로 규칙을 정하거나 자동으로 새로운 객체를 만드는 도구를 활용한다면 객체 역시 불변성을 확보할 수 있는 것이다!

어떤 상황에 필요할까?

값으로 전달받은 객체에 변경을 가하더라도 원본 객체는 변하지 않아야하는 경우!

var user = {
	name: 'Huijeong',
  	gender: 'female'
};

var changeName = function (user, newName) {
	var newUser = user;
  	newUser.name = newName;
 	return newUser;
};

var user2 = changeName(user, 'Nam');
if(user !== user2){
	console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // Nam Nam
console.log(user === user2); // true

객체의 가변성으로 인한 문제점을 보여주는 예시.
원본이 변경되어 큰 오류를 낳는다.

해결방법

var user = {
	name: 'Huijung',
  	gender: 'female'
};

var changeName = function (user, newName) {
	return {
    	name: newName,
      	gender: user.gender
    };
};

var user2 = changeName(user, 'Nam');

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

console.log(user.name, user2.name); // Huijeong Nam
console.log(user === user2); // false

changeName 함수가 새로운 객체를 반환하도록 수정했다. 하지만 새로운 객체를 만들도록 해서 변경할 필요가 없는 프로퍼티(gender)까지 하드코딩으로 입력했다.

대상 객체에 정보가 많을수록 변경해야 할 정보가 많을수록 사용자가 입력하는 수고가 늘어날 것이다. 이런 방식보단 대상 객체의 프로퍼티 개수에 상관없이 모든 프로퍼티를 복사하는 함수를 만드는 편이 더 좋을 것이다.

let user = {
  name: "huijeong",
  gender: "female"
};

function copyObject(target) {
  let result = {};
  for(let prop in target) {
    result[prop] = target[prop];
  }
  return result;
}

let user2 = copyObject(user);
user2.name = 'nam';

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

console.log(user.name, user2.name); // huijeong nam
console.log(user === user2); // false

copyObject 함수는 for in 문법을 이용해 result 객체에 target 객체의 프로퍼티들을 복사하는 함수이다.
BUT copyObject 함수를 활용해서 객체를 만들었을 때, 가장 아쉬운 점은 얕은 복사만을 수행한다는 점

for...in

  • for...in 문은 상속된 열거 가능한 속성들을 포함하여 객체에서 문자열로 키가 지정된 모든 열거 가능한 속성에 대해 반복한다. (Symbol로 키가 지정된 속성은 무시.)
    MDN에 있는 정의인데 너무 어렵게 설명해서 쉽게 설명해보겠다.
  • 객체의 속성들을 열거(Iterate)하기 위해 사용되는 반복문
  • 객체의 프로퍼티를 하나씩 순회하면서 해당 프로퍼티의 키를 변수에 할당하여 사용할 수 있다.
  • 주의할 점은 for...in 반복문은 배열에 사용하지 않는 것
for (variable in object) {
  // 실행할 코드
}

const person = {
  name: 'John',
  age: 30,
  gender: 'male',
};

for (let prop in person) {
  console.log(`${prop}: ${person[prop]}`);
}

얕은 복사 Shallow Copy

  • 얕은 복사는 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주솟값만 복사하는 방법
  • 객체나 배열을 복사할 때 원본 객체의 최상위 레벨의 요소들만을 새로운 객체로 복사하는 것
  • 원본 객체의 중첩된 객체나 배열은 복사하지 않고 참조를 공유하는 것.
  • 얕은 복사는 복사하는 객체의 크기가 크거나 중첩 구조가 복잡하지 않을 때 효과적으로 사용

Object.assign()

  • 얕은 복사를 수행하는 방법 중 가장 간단한 방법
  • 혹은 전개 연산자(...)를 사용
  • 최상위 레벨의 요소들만 복사하며, 중첩된 객체는 원본 객체와 같은 참조를 가리킨다.
// 배열의 얕은 복사
const originalArray = [1, 2, 3];
const copiedArray = originalArray.slice(); // 또는 [...originalArray];

copiedArray[0] = 99;
console.log(originalArray); // 출력 결과: [1, 2, 3] (원본 배열은 변경되지 않음)

// 객체의 얕은 복사
const originalObj = { name: 'John', age: 30 };
const copiedObj = Object.assign({}, originalObj); // 또는 { ...originalObj };

copiedObj.name = 'Alice';
console.log(originalObj.name); // 출력 결과: John (원본 객체는 변경되지 않음)

BUT

  • 중첩된 객체의 경우 얕은 복사로는 참조를 공유하므로 예상치 못한 결과가 발생할 수 있음.
  • 중첩된 객체까지 완전히 복사하고 싶다면 깊은 복사(Deep Copy)를 사용
  • 중첩된 객체까지 복사해야 하는 경우, 깊은 복사를 고려해야 함.

깊은 복사 Deep Copy

  • 깊은 복사는 객체의 모든 속성을 복사하고 원본 객체와 완전히 독립적인 객체를 만드는 방법!
  • 객체나 배열을 복사할 때, 원본 객체의 모든 속성과 중첩된 객체, 배열까지 모두 완전히 새로운 객체로 복사하는 것
  • 얕은 복사(Shallow Copy)와 달리 중첩된 객체까지 복사하여 객체 간의 참조를 공유하지 않음
  • 원본 객체가 변경되더라도 복사된 객체에는 영향을 주지 않음.

재귀적인 방법

객체의 속성을 순회하면서 중첩된 객체가 있을 경우 해당 객체도 재귀적으로 복사하는 방법.

// 재귀적인 방법으로 깊은 복사 수행
function deepCopy(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  
  let copy;
  if (Array.isArray(obj)) {
    copy = [];
    for (let i = 0; i < obj.length; i++) {
      copy.push(deepCopy(obj[i]));
    }
  } else {
    copy = {};
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        copy[key] = deepCopy(obj[key]);
      }
    }
  }

  return copy;
}
// 예시 객체
const originalObj = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    country: 'USA'
  }
};
// 깊은 복사 수행
const copiedObj = deepCopy(originalObj);
// 중첩된 객체 변경
copiedObj.address.city = 'Los Angeles';
// 원본 객체는 변경되지 않음
console.log(originalObj.address.city); 
// 출력 결과: New York (원본 객체는 변경되지 않음)

JSON을 이용한 방법

JSON.parse()JSON.stringify() 메서드를 사용하여 객체를 JSON 문자열로 변환한 뒤 다시 객체로 변환하는 방법이다.

function deepCopyWithJSON(obj) {
  return JSON.parse(JSON.stringify(obj));
}

// 예시 객체
const originalObj = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    country: 'USA',
  },
};

// 깊은 복사 수행
const copiedObj = deepCopyWithJSON(originalObj);

// 중첩된 객체 변경
copiedObj.address.city = 'Los Angeles';

// 원본 객체는 변경되지 않음
console.log(originalObj.address.city); // 출력 결과: New York

2개의 댓글

comment-user-thumbnail
2023년 7월 27일

아 너무 잘봤습니다! 좋은 내용 감사합니다!

1개의 답글