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

Gee·2022년 2월 27일
0
post-thumbnail

본 포스팅은 코어자바스크립트 라는 책을 기반으로 작성되었습니다.

1.불변객체

불변객체란 ? 어떤 객체 내부의 프로퍼티들을 변경할 수 없도록 되어있는 객체입니다.

1) 불변객체의 필요성

let user = {
	name: 'Jaenam',
    gender: 'male',
}

const changeName = function(User, newName) {
	let newUser = User;
	newUser.name = newName;
    return newUser
}

let user2 = changeName(user, "A")
let user3 = changeName(user, "B")
let user4 = changeName(user, "C")

해당 코드를 실행했을 시에 결과물이 어떻게 나오실지 예상이 되시나요?

console.log(user2.name) // C
console.log(user3.name) // C
console.log(user4.name) // C

앞에서 작성한 A,B,C 의 이름으로 출력되는 것이 아니라 마지막에 바꾸었던 C라는 이름으로 출력이 됩니다.
이러한 일이 발생하는 이유는, changeName() 함수의 동작을 살펴보면
user2~user4 까지의 모든 변수는 같은 주소 공간을 바라보고 있고, 그 주소 공간중 name 프로퍼티가 가리키는 데이터를 변경하였기 때문에, 모든 변수에 담긴 name 프로퍼티 값이 같이 변경된 듯한 현상이 발생하는 것입니다. 이런 식으로 객체를 생성한다면, 원하지 않는 객체의 값마저 변경되어 버립니다. 이러한 경우를 위해 불변객체가 필요하게 됩니다.
(객체의 가변성에 따른 문제점)

문제상황 해결

앞선 문제상황의 원인은 모든 변수가 같은 주소 공간을 바라보고있기 때문입니다.
따라서 문제를 해결하기 위해서는 원본 객체 내부의 프로퍼티들을 복사한 새로운 객체를 생성하여 변수에 할당해주어야 합니다.

let user = {
	name: 'Jaenam',
    gender: 'male',
}

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

let user2 = changeName(user, "A")
let user3 = changeName(user, "B")
let user4 = changeName(user, "C")

console.log(user2.name) // A
console.log(user3.name) // B
console.log(user4.name) // C

변경된 changeName 함수를 다시 살펴보면, 이번에는 변수를 선언하고, 매개변수로 전달된 User변수를 User변수가 가리키는 주소 공간으로 가리키게 하는 것이 아닌, 새로운 객체를 생성하여 User의 내부 프로퍼티들을 담아주었습니다.
하지만, 이러한 방식은 새로운 객체를 만들면서 변경할 필요가 없는 기존 객체의 프로퍼티를 하드 코딩으로 입력해야하기 때문에 대상 객체에 정보가 많을수록, 변경해야 할 정보가 많을 수록 수고가 많아질 것입니다.

다른 방식으로는 기존 정보를 복사해서 새로운 객체를 반환하는 함수(얕은 복사)를 만들거나
라이브러리를 통한 불변객체 강제화 방법(immutable.js, baobao.js)도 있습니다.

2. 얕은복사와 깊은복사

1 ) changeName() 함수 일반화

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

let people = {
	name : "A",
    age : 10 
}
let people2 = copyObject(people)
people2.name = "B"
console.log(people) // { name: "A", age: 10}
console.log(people2) // { name: "B", age: 20 }
  1. 함수의 첫 줄에서 newObject라는 빈 객체를 생성합니다.
  2. 두 번째줄의 "for in"문을 이용하면 특정 객체의 property들을 순환하며 prop이라는 변수에 담아줍니다. 이는 각 property 의 키 값에 해당합니다.
  3. 세번째줄에서는 newObejct에 target값을 복사하는 과정이 일어납니다. 첫번째 for문을 실행될때만을 조금 더 자세히 설명드리면, 매개변수인 target객체의 첫번째 property(name)가 prop변수에 담기게 됩니다. 이것을 이용하면 "newObject["name"]"..를 만들 수 있습니다. 그 후, target[prop]을 이용하여 원본 객체를 나타내는 target의 각 property 값을 불러올 수 있습니다.

단순히 객체를 복사하는 과정에 초첨을 맞추어 작성된 함수입니다. 이름을 변경하여 새로운 객체를 반환하는 것에 초첨이 맞추어져있기 때문에, age, gender를 변경하기 위해서는 그것을 위한 함수를 또 만들어야 합니다.

2 ) 얕은 복사의 문제점

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

let user = {
	name: "JYJ",
    friends: ["A","B","C"]
}

user2 = copyObject(user)
user2.friends[0] = "D";

console.log(user.friends) // ["D","B","C"]
console.log(user2.friends) //  ["D","B","C"]

changeName()과 같은 상황이 발생하게 됩니다. copyObject()함수에서는 새로운 객체를 생성하여 새로운 객체에 target객체의 프로퍼티들을 할당하고 있습니다. 여기까지는 문제가 없어보입니다. 하지만, target 객체 내부의 프로퍼티가 또 다른 객체를 가리키는 참조변수라면 어떨까요?

결국 user 과 user2에 담긴 friends property에는 같은 객체를 가리키는 주소값이 담기게 될 것입니다.
이것의 원인은 "얕은 복사"를 했기 때문인데요. 얕은 복사는 한 객체가 가지는 바로 하위의 property들만을 복사하는 것을 말합니다. 이때문에, user객체가 가지고 있는 또 다른 객체인 friends배열에 대해 복사가 이루어지지 않아, user와 user2가 같은 frineds배열을 바라보게 되는 것입니다.

3) 깊은 복사

얕은 복사를 통해 한단계만 복사를 하게 되어서, 내부에 또 객체가 있는 경우에 발생하는 문제를 해결하기 위해서는 깊은 복사를 통해 해결하면 됩니다. 깊은 복사는 재귀적 복사를 통해 내부에 존재하는 모든 단계의 객체들을 복사하는 것을 의미합니다.

const deepCopyObject = function(target) {
	let newObject = {};
    
    if(typeof target === "object" && target !== null) {
    	for (let prop in target) {
        	newObejct[prop] = deepCopyObject(target[prop])
        }
    }else {
    	newObject = target;
    }
    return newObject
}

let user = {
	name: "JYJ",
	friends: ["a","b","c"]
}

user2 = deepCopyObject(user)
user2.friends[0] = "d"

console.log(user.friends) // ["a","b","c"]
console.log(user2.friends) //["d","b","c"]

위의 deepCopyObject()함수의 경우에는 앞서 말씀드린 얕은 복사의 문제점을 해결하기 위해 새로 정의한 깊은 복사입니다. 재귀 호출을 통해, 내부의 프로퍼티가 객체인 경우에는 다시 한 번 복사를 하게 되는 함수입니다.

이외에도 아래의 방법들을 통해 깊은 복사를 할 수 있습니다.
Lodash의 cloneDeep 함수 사용
JSON.parse()와 JSON.stringify()함수 사용

profile
작은 실패, 빠른 피드백, 다시 시도

0개의 댓글