[JS] 불변객체와 복사 (얕은 복사와 깊은 복사)

박시은·2024년 1월 2일
0

JavaScript

목록 보기
7/58
post-thumbnail

Data Type의 메모리 할당 》에 대해 안다는 전제 하에 포스팅합니다.


▶ 불변객체

  • 불변 객체의 정의
    • 객체를 예로 들면, 객체의 속성에 접근해서 값을 변경하면 가변이 성립하고, 객체 데이터 자체를 변경(새로운 데이터를 할당)하고자 한다면 기존 데이터는 변경되지 않으므로 불변하다라고 했었다.
    • 하지만, 객체의 가변성은 아래 코드처럼 가변이기 때문에 기존에 있는 객체도 영향을 받는다는 단점이 존재한다.
// user 객체를 생성
var user = {
	name: 'wonjang',
	gender: 'male',
};

// 이름을 변경하는 함수, 'changeName'을 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티(속성)에 접근해서 이름을 변경 -> 가변
var changeName = function (user, newName) {
	var newUser = user;
	newUser.name = newName;
	return newUser;
};

// 변경한 user정보를 user2 변수에 할당
// 가변이기 때문에 user1도 영향을 받게 된다.
var user2 = changeName(user, 'twojang');

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

// user과 user2이 똑같아지는 현상 발생 -> 똑같은 주소 가리킴
console.log(user.name, user2.name); // twojang twojang
console.log(user === user2); // true

위의 예제를 아래와 같이 개선할 수 있다. (새로운 객체 리턴(새 주소 할당)하는 방식 → 불변)

// user 객체를 생성
var user = {
	name: 'wonjang',
	gender: 'male',
};

// 이름을 변경하는 함수 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티에 접근하는 것이 아니라, 아예 새로운 객체를 리턴(새 주소를 할당하게 됨) -> 불변
var changeName = function (user, newName) {
	return {
		name: newName,
		gender: user.gender,
	};
};

// 변경한 user정보를 user2 변수에 할당
// 불변이기 때문에 user1은 영향이 없다.
var user2 = changeName(user, 'twojang');

// 아래 로직이 정상적으로 수행
if (user !== user2) {
	console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name); // wonjang twojang
console.log(user === user2); // false

  • 하지만 위 방법은 다음과 같은 문제점이 있다.
    • changeName 함수는 새로운 객체를 만들기 위해 변경할 필요가 없는 gender 프로퍼티를 하드코딩으로 입력함 ⇒ 만일 이러한 속성이 10개라면?
    • 따라서, 얕은 복사의 방법이 존재한다!



▶ 복사

불변성을 유지하는 방법 중 하나이다.

▷ 얕은 복사

바로 아래 단계의 값만 복사한다.

  • for ~ in 구문을 활용
var copyObject = function (target) {
	var result = {};

  	// for ~ in 구문을 활용하여, 객체의 모든 프로퍼티에 접근 -> 하드코딩 필요 x
	// 이 copyObject로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경
	for (var prop in target) {
		result[prop] = target[prop];
	}
	return result;
}


// user은 항상 copyObject를 사용해 복사 (새 주소 할당 -> 불변성 유지)
var user = {
	name: 'wonjang',
	gender: 'male',
};

var user2 = copyObject(user);
user2.name = 'twojang';

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

console.log(user.name, user2.name);
console.log(user === user2);

  • but 얕은 복사도 완벽한 복사를 할 수 없다는 한계가 존재한다.
    • 얕은 복사는 바로 아래 단계의 값만 복사하므로 (위의 예제), 중첩된 객체의 경우 참조형 데이터가 저장된 프로퍼티를 복사할 때, 주소값만 복사한다는 문제 발생
    • ex. 아래 코드) ser.urls 프로퍼티도 불변 객체로 만들어야 한다.
var user = {
	name: 'wonjang',
	urls: {
		portfolio: 'http://github.com/abc',
		blog: 'http://blog.com',
		facebook: 'http://facebook.com/abc',
	}
};

var user2 = copyObject(user);

user2.name = 'twojang';

// 바로 아래 단계에 대해서는 불변성을 유지하기 때문에 값이 달라진다.
console.log(user.name === user2.name); // false

// 더 깊은 단계에 대해서는 불변성을 유지하지 못하기 때문에 값이 같다.
user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio); // true

// 아래 예도 똑같다.
user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog); // true

▷ 깊은 복사

내부의 모든 값들을 하나하나 다 찾아서 모두 복사한다. (얕은 복사의 한계 해결)

  • 중첩된 객체에 대한 깊은 복사
    • 아래 코드는 중첩된 객체의 중첩된 객체를 또 copy 해야하는 문제가 발생한다.
var user = {
	name: 'wonjang',
	urls: {
		portfolio: 'http://github.com/abc',
		blog: 'http://blog.com',
		facebook: 'http://facebook.com/abc',
	}
};

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

// 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);

🔖 재귀적 수행

따라서 재귀적 수행을 사용하여 깊은 복사를 완벽 구현할 수 있다.

  • 결론
    • 객체의 프로퍼티 중, 기본형 데이터는 그대로 복사 + 참조형 데이터는 다시 그 내부의 프로퍼티를 복사 ⇒ 재귀적 수행
    • 재귀적으로 수행한다?
      • 함수나 알고리즘이 자기 자신을 호출하여 반복적으로 실행되는 것을 말한다.
      • 모든 요소 하나하나 불변성 유지하면서 바꾸기 가능

  • 아래와 같이 재귀적 수행은 완벽히 다른 객체를 반환하여 깊은 복사를 완벽하게 구현한다.
var copyObjectDeep = function(target) {
	var result = {};
	if (typeof target === 'object' && target !== null) {
		for (var prop in target) {
			result[prop] = copyObjectDeep(target[prop]); // 재귀적 수행
		}
	} else {
		result = target;
	}
	return result;
}

//결과 확인
var obj = {
	a: 1,
	b: {
		c: null,
		d: [1, 2],
	}
};
var obj2 = copyObjectDeep(obj);

obj2.a = 3;
obj2.b.c = 4;
obj2.b.d[1] = 3;

console.log(obj);
console.log(obj2);



▶ 문제📒

가장 아래의 코드가 실행 되었을 때, “Passed ~” 가 출력되도록 getAge 함수를 채워보자.

var user = {
  name: "john",
  age: 20,
};

var getAged = function (user, passedTime) {
  // 여기를 작성해 주세요!
};

var agedUser = getAged(user, 6);

var agedUserMustBeDifferentFromUser = function (user1, user2) {
  if (!user2) {
    console.log("Failed! user2 doesn't exist!");
  } else if (user1 !== user2) {
    console.log(
      "Passed! If you become older, you will be different from you in the past!"
    );
  } else {
    console.log("Failed! User same with past one");
  }
};

agedUserMustBeDifferentFromUser(user, agedUser);

▷ 풀이

  • 매커니즘
    • 2개의 인자를 받는 getAged 함수의 passTime의 숫자 만큼 나이를 먹은 유저를 복사하면 된다.
    • ex. passTime이 2이면 name="John", age:22 인 객체가 함수를 통해 생성될 것이다.
    • 즉, 호출 시 agedUser 객체와 원래 있었던 agedUser 를 비교해서 상황에 따른 메세지를 출력한다.

  • 복사를 하지 않은 코드 → failed 출력
var getAged = function (user, passedTime) {
  var newUser = user;
  newUser.age += passedTime;

  return newUser; // Failed! User same with past one
};

  • user2만 나이를 먹게 설정해 주었는데 user1도 나이가 먹은 것을 알 수 있다.
    → js에서 객체는 직접 값을 저장하는 것이 아니라 별도의 공간을 따로 두고 값을 참조하기 때문!
  console.log(user1); // { name: 'john', age: 26 }
  console.log(user2); // { name: 'john', age: 26 }

  • 해결
    • 기존 객체를 변경하지 않고 새로운 객체를 생성하여 두 객체가 서로 다르도록 설정해줘야한다.
    • 따라서 getAged 함수 내에서 객체를 생성하고, 기존 객체의 속성을 복사한 후에 나이를 업데이트해야한다.
    • 이렇게 함으로써 기존 객체와 업데이트된 객체가 독립적으로 존재하게 되고, 두 객체는 메모리 상에서 각각 다른 위치에 저장되어 서로 영향을 주지 않는다.
    • 즉, 순회하면서 새로운 객체를 생성해야 할 것이다.
// 객체 만들어 프로퍼티 복사하기
var getAged = function (user, passedTime) {
  var result = {};
  for (var prop in user) {
    result[prop] = user[prop];
  }
  result.age += passedTime;
  return result;
};

▷ 전체 코드

var user = {
    name: "john",
    age: 20,
}

// 객체 만들어 프로퍼티 복사하기
var getAged = function (user, passedTime) {
    var result = {};
    for (var prop in user) {
        result[prop] = user[prop];
    }
    result.age += passedTime; 
    return result;
}


var agedUser = getAged(user, 6);

var agedUserMustBeDifferentFromUser = function (user1, user2) {
    if (user1 !== user2) { 
        console.log("Passed! If you become older, you will be different from you in the past!")
    } else {
        console.log("Failed! User same with past one");
    }
}

agedUserMustBeDifferentFromUser(user, agedUser);
profile
블로그 이전했습니다!

0개의 댓글