해당 글은 정재남 님의 '코어자바스크립트' 책을 읽고 공부한 포스팅입니다.
따라서, 챕터의 내용은 상당히 생략되어 있으며, 느낀점과 기억해야 할 것들을 주관을 가득 담아 작성했습니다.
노트가 아닌 관계로 포스팅의 영양가는 거의 없으니, 감안하고 봐주셨으면 좋겠습니다.
Javascript 에서 데이터를 저장하는 방식은 모든 자료가 참조형인것을 지난 시간에 확인했습니다.
그렇다는 것은 변수나 객체를 복사할 경우, 실제 변수 영역의 값으로는 해당 데이터 영역의 주소를 공유한다는 것 까지 이어서 생각할 수 있습니다.
다음은 간단한 불변값을 할당받은 변수의 복사입니다.
let a = 123,
b = a;
해당 방식으로 변수의 값을 다른 변수에 저장할 경우, 실제 변수영역의 식별자 : a, b의 값은 @5003 등과 같은 메모리 주소를 참조하게 됩니다.
Javascript는 데이터 영역을 통해 원시 자료형과 같은 불변값 데이터를 관리하고 있기 때문이죠.
그렇다면 가변값에도 해당 방식이 옳은지 고민해보겠습니다.
우린 object를 가변값의 예시로 활용합니다.
object의 prop은 모든 데이터가 value로 올 수 있을 만큼 자유롭고, object는 심지어 불변값을 할당받은 변수와는 다른 방식으로 데이터를 저장하고 있습니다.
let obj = { a : 123, b : `Hello` };
간단한 객체를 먼저 생성해봅시다.
해당 obj는 변수 영역에는 식별자로 obj, 값으로는 데이터 영역의 주소를 참조합니다.
참조하는 데이터 영역의 값은 다시 프로퍼티 영역의 주소를 참조하고, 프로퍼티 영역은 식별자와 값으로 나뉘어져 다시 값에 데이터 영역의 주소를 참조합니다.
변수 영역 - 데이터 영역 - 프로퍼티 영역 사이에 일종의 흐름이 존재하게 되는 것이죠.
그렇다면, 복사한 객체는 어떤 방식으로 자료를 저장할까요?
let obj = { a : 123, b : `Hello` },
obj2 = obj;
머리속으로 그려보면 새로운 변수 영역 주소에 obj2 라는 식별자와 주소를 참조하는 값이 먼저 생성됩니다.
이후 값으로 obj 객체의 프로퍼티 영역 주소를 담고 있는 데이터 영역의 주소를 그대로 복사할겁니다.
이 경우, 문제는 복사 자체에 존재하지 않게 됩니다.
문제는 참조 복사 이후에 obj2와 obj 사이 동일한 key의 value가 변경될 때 발생합니다.
let obj = { a : 123, b : `string`, c : [1,2,3]};
let obj2 = obj;
obj2.a = 12345;
console.log(obj);
// { a: 12345, b: 'string', c: [ 1, 2, 3 ] }
console.log(obj2);
// { a: 12345, b: 'string', c: [ 1, 2, 3 ] }
위처럼 객체의 경우 복사 이후 원본 객체와 복사한 객체 모두 프로퍼티 내부의 값이 변경됩니다.
이는 심지어 const(상수)로 선언한 객체일지라도 내부 프로퍼티의 값이 변경된다는 점에서 단순 선언의 형태를 변경하는 것으로는 막을 수 없습니다.
const(상수)의 경우 변수 영역 값으로 불변값을 그대로 보유하지만, 이미 객체는 변수 영역에서 데이터 영역을 참조하고, 다시 프로퍼티 영역에서 데이터 영역을 참조하는 형태로 값을 보유하기 때문입니다.
const obj = { name : 123, name2 : 345};
obj.name = 123456;
console.log(obj);
obj = 123; // TypeError: Assignment to constant variable.
그래서 많은 사람들은 객체의 복사를 해결하기 위해 많은 고민을 했습니다.
이에 결국 다양한 방식의 복사가 등장했습니다.
다음은 복사의 종류에 따라 구분지어 생각해보겠습니다.
먼저 등장한 것은 단순 할당이 아닌 다양한 방법으로 새 객체를 생성하는 것이었습니다.
그러나 위 방식의 얕은 복사의 경우에도 다차원 구조의 객체에 대해서는 여전히 처음 이슈와 동일한 문제를 안고 있었죠.
그럼에도 단순한 객체를 복사해야 하는 상황에서는 아주 훌륭하게 작용할 수 있다고 생각합니다.
예시를 보시죠.
let obj = { a : 123, b : `string`, c : [1,2,3]};
let obj2 = { a : obj.a, b : obj.b, c : obj.c};
obj2.a = 12345;
console.log(obj, obj2);
해당 방식은 번거롭지만 안전합니다. 이를 편히 하기 위해 반복문을 사용하고, 혹은 함수를 통해 새로운 객체를 반환하는 등의 방식이 등장합니다.
// 얕은 복사
let copyObject = (target) => {
let result = {};
for (let props in target){
result[props] = target[props];
}
return result;
}
let obj = {
name : `dongwu Kim`,
phone : `010-1234-3456`
};
let obj2 = copyObject(obj);
console.log(obj, obj2);
obj2[`name`] = 123;
console.log(obj, obj2);
사실 위 두 방식은 같은 결과를 도출하는 코드입니다. 다만, 데이터의 양이 많아졌을 경우 아래 함수와 반복문을 사용하는 코드가 훨씬 간편할 수 있겠죠.
하지만 이러한 방식은 완전한 복사가 아닙니다.
이에 이러한 방식은 객체의 얕은 복사라고 불립니다.
그렇다면 왜 얕은 복사가 문제가 되는지부터 생각해봅시다.
// 얕은 복사
let copyObject = (target) => {
let result = {};
for (let props in target){
result[props] = target[props];
}
return result;
}
let obj = {
name : `dongwu Kim`,
phone : `010-1234-3456`,
innerObj : {
arr : [1, 2, 3, 4, 5]
}
};
let obj2 = copyObject(obj);
console.log(obj, obj2);
let innerObj = `innerObj`,
arr = `arr`;
obj2[innerObj][arr] = [1, 2, 3];
console.log(obj, obj2);
해당 방식으로는 중첩객체를 완전하게 복사할 수 없습니다.
이에 책에서는 먼저 재귀함수를 통한 방법으로 해결하고자 했으며, 결과는 성공적이었습니다.
// 깊은 복사 -> 중첩 객체
let copyObjectDeep = (target) => {
let result = {};
if(typeof target === `object` && target !== null){
// base case
for (let props in target){
result[props] = copyObjectDeep(target[props]);
// copyObjectDeep(target[props]) =>
// object 내부 key 중 object 가 밸류인 key만이 argument
}
}
else {
// 만약 불변값이 value 인 prop일 경우 그대로 반환하겠다.
result = target;
}
return result;
}
let obj3 = {
name : `dongwu`,
phone : `010-1234-5667`,
favorite : [`숨쉬기`, `누워있기`]
};
let obj4 = copyObjectDeep(obj3);
console.log(obj3, obj4);
위와 같은 방식으로 함수 정의 내 함수를 호출하는 재귀함수로 중첩구조의 복사를 해결할 수 있었습니다.
또한 배열의 경우 key : value 구조를 적용하기 위해 (index -> key) : (element -> value) 의 형태로 변경되었음을 확인할 수 있습니다.
Javascript Object 또한 대괄호([])로 해당 key 에 접근할 수 있다는 점에서 큰 문제는 없어보입니다.
그렇다면 흔히 우리가 아는 array와 object는 다른 자료구조를 가지는데, 과연 변경이 이루어졌는지도 고민을 해야겠죠?
그러나 아직 저의 지식 수준에서 생각하기에는 Javascript의 array 는 우리가 아는 타 언어의 array 구조와는 달리 hash table, index(integer)로 value들을 매핑한 구조를 갖는 일종의 object, 객체라는 결과가 나옵니다.
객체는 깊게 들어가면 결국 모든 것이다?
즉, Javascript 에서 object와 array는 동일한 자료구조(Hash table)를 사용하고 있으며, 이러한 결론은 다시 연산 내 동일한 step을 밟는다는 결과가 됩니다.
시간복잡도도 같고, 코드 내에서도 동일한 방식으로 접근이 가능하다면 해당 함수는 완전무결하지 않냐는 생각이 듭니다.
그러나 그렇지만은 않습니다.
먼저, 재귀함수는 메모리 부담이 대표적인 문제점으로 꼽히고 있습니다.
위와 같이 간단한 객체를 복사하는 것은 함수 내 함수 호출의 loop가 금방 종료되지만, 만약 우리가 다루는 객체의 내부 데이터가 억, 조 단위일 경우로 가정한다면 마냥 옳은 방법이 아니었지 않았을까 생각이 들었습니다.
재귀함수는 함수 내에서 자기 자신이 되는 함수를 호출하는 방법이기 때문에 이전의 개발자들은 때에 따라서 다른 방식을 사용해야 했을 겁니다.
이에 등장한 것이 Json.parse(Json.stringify(obj));
의 방법인데, 해당 방식은 안타깝게도 __proto__
, getter/setter, 메소드 데이터의 경우 복사가 불가능합니다.
아직은 거의 사용할 일 없어 있다는 수준으로만 이해하기로 했습니다.
Array.map() 메소드의 경우 원본 배열에 대한 영향이 없는 것으로 알고 있습니다.
그러나, 중첩객체 내부 배열에 대한 해당 메소드의 경우 원본에도 영향을 끼치게 됩니다.
다음은 예시코드입니다.
let obj3 = {
name : `dongwu`,
phone : `010-1234-5667`,
favorite : [`숨쉬기`, `누워있기`]
};
// let obj4 = copyObjectDeep(obj3);
let favorite = `favorite`
let obj4 = obj3;
obj4[favorite] = obj3[favorite].map((x) => {
let result = x + `를 좋아합니다.`;
return result;
})
console.log(obj3, obj4);
코드를 실행시키면 obj3 원본의 favorite value 값에 영향을 끼치는 것을 확인할 수 있습니다.
이는 위 내용 중, 중첩객체의 얕은복사에 해당하는 내용이 되겠네요.
그렇기에 Array.map() 메소드를 올바르게 실행하기 위해서는 새 객체를 리턴하는 방식의 깊은 복사를 진행해주어야 합니다.
Array.forEach()의 경우에도 마찬가지로 복사된 객체 외에 원본에 영향을 끼칩니다.
let obj3 = {
name : `dongwu`,
phone : `010-1234-5667`,
favorite : [`숨쉬기`, `누워있기`]
};
// let obj4 = copyObjectDeep(obj3);
let favorite = `favorite`
let obj4 = obj3;
obj4[favorite].forEach((x, idx) => {
let result = x + `를 좋아합니다.`;
obj4[favorite][idx] = result;
})
console.log(obj3, obj4);
이제 왜 깊은 복사라는 개념이 존재하는지 다른 예시를 확인할 수 있었습니다.
앞으로는 코드를 짤 때 보다 신중해질 수 있겠습니다.
그럼 이번 글은 여기서 마치겠습니다.
읽어주셔서 감사합니다.