JavaScript

불변 객체 | 얕은 복사 & 깊은 복사

bas96 2021. 6. 18. 12:49

가변성을 가진 참조형 데이트의 '가변'은 데이터 자체가 아니라 내부 프로퍼티를 변경할 때만 성립하고, 데이터 자체를 변경하고자 한다면 (기본형 데이터와 마찬가지로) 기존 데이터가 변하지는 않습니다. 

 

내부 프로퍼티를 변경 할 필요가 있을 때마다 매번 새로운 객체를 만들어 재할당 하기로 규칙을 정하거나 immutable.js, immer.js  등의 라이브러리를 사용한다면 객체도 불변성을 확보할 수 있습니다. 

 

객체가 불변성을 확보해야하는 경우는 언제일까요?

값으로 전달받은 객체에 변경을 하더라도 원본 객체는 변하면 안되는 경우입니다. 

예를 들어 정보가 바뀐 시점에서 알림을 보내거나, 바뀌기 전의 정보와 바뀐 후의 정보를 가시적으로 보여줘야 하는 등의 기능을 구현해야 하는 경우가 있습니다. 

이럴 경우레는 변경 전/후 서로 다른 객체를 바라보게 만들어야합니다.

 

저번 포스팅에서 "변수 복사 비교"를 다루었을 때, obj2 자체 레퍼런스를 바꾸었을 때는 obj1 !== obj2 였지만 obj2.c의 값(데이터 자체)을 바꾸었을 때는 obj1 === obj2의 결과가 나온 것을 알 수 있었습니다.

 

만약 제가 개명을 해서 회원정보의 이름을 바꾼다고 하였을 때 알림이 뜨게 하기위해서는 user라는 객체의 [name]을 직접 수정하는 것이 아니라, 새로운 객체(user2)를 만들어서 user 와 비교하여야 합니다.

 

const user = {
	name: 'Hyojin',
    gender: 'female'
};

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

let user2 = changeName(user, 'Jin');

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

console.log(user.name, user2.name); // Hyojin Jin
console.log(user === user2); // false

 

'얕은 복사' & '깊은 복사'

'얕은 복사'

성공적으로 이름을 변경하고 정보가 변경되기 전, 후의 서로 다른 객체를 비교할 수 있었습니다.

 

그런데 위의 복사는 바로 아래 단계의 값만 복사하는 '얕은 복사'의 방법이었습니다.

더 쉽게 말하자면 객체 안의 객체 즉, 중첩된 객체를 복사 할 경우에는 그 주솟값만 복사한다는 말입니다. 원본과 사본 모두 같은 참조형 데이터 주소를 바가리키게 되고 그럼 사본<=>원본 이 바뀔때 마다 같이 바뀝니다.

 

'깊은 복사'

만약 위의 예시에서 user 객체 안에 adress 라는 객체가 또 있다면, user.name, user.gender는 얕은 복사로 변경이 가능하지만 adress는 수정하더라도 user객체와 user2의 객체 안의 adress 안의 데이터 값이 똑같이 함께 바뀌게 되어 비교할 수 없을 것입니다.

const user = {
	name: 'Hyojin',
    gender: 'female'
    adress : {
    	 country : 'Republic of KOREA',
         state: 'SEOUL'
    }
};

 

그래서 이렇게 중첩된 객체에서는 '깊은 복사'를 해야지만 user.adress 프로퍼티에 대해서도 불변 객체로 만들 수 있습니다. 

let user2 = copyObject(user);
user2.adress = copyObject(user.adress);

 

정리하면 어떤 객체를 복사하고, 객체 내부의 모든 값을 복사해서 완전히 새로운 데이터를 만들고자 할 때,

기본형 데이터는 그냥 복사하면 되지만 참조형 데이터는 다시 그 내부의 프로퍼티를 복사해야 합니다.

 

객체 내 객체까지 깊은 복사를 하는 함수

const changeUserInfoDeep = function(target) {
	let result = {};
    if(typeof target === 'object' && target !== null) {
    	for(let prop in target) {
        result[prop] = changeUserInfoDeep(result[prop]);
        }
    } else {
    	result = target;
    }
    return result;
};

- target이 객체일 경우 내부 프로퍼티들을 순횐하며 함수를 호출

- 객체가 아닌 경우 target을 그대로 지정 (첫 번째 얕은 복사와 같음)

참고: target !== null을 준 이유는 typeof 명령어가 null도 'object'를 반환하기 때문

더 간단한 '깊은 복사'

깊은 복사를 더 간단하게 하는 방법은 객체를 JSON 문법으로 표현된 문자열로 전환했다가, 다시 JSON 객체로 바꾸는 방법입니다.

 

이 방법은 프로젝트를 하면서 http 통신을 할 때 많이 쓰였는데 '깊은 복사'라는 개념은 모르고 string <--> object 개념으로만 알고 사용했었습니다.

프로젝트에서 Http 통신할 때 사용했던 JSON 문법

JSON 문법은 이렇게 httpRequest로 받은 데이터를 저장한 객체를 복사할 때 많이 쓰입니다.

다만 한계점은 메서드나 getter/setter 등은 JSON으로 변경할 수 없고, 순수한 정보만 다룰 때 활용하기 좋은 방법입니다. 

 

자바스크립 문법에 대해 자세히 공부하기 시작하고 원래 쓰는 문법과 코드의 원리를 더 자세히 알 수있어서 이해도가 깊어지니 더 재미있는 것 같습니다 ㅎㅎ