데이터 타입의 종류는 크게 기본형과 참조형 2가지로 나뉜다.
기본형과 참조형을 구분 짓는 가장 큰 점은 실제 값 전체 복사 vs 실제 값이 들어있는 주소 값 복사이다.
let a = 1;
let b = a;
a += 10;
console.log(a) //11
console.log(b) //1
위의 코드를 살펴보면 a의 값이 변하더라도 b의 값은 그대로 인 것을 볼 수 있다.
let b = a; 코드를 통해 a값 전체가 복사되어 b라는 변수에 담겼다.
a, b는 서로 영향을 받지 않고 독립적으로 존재하는 것을 볼 수 있다.
기본형 데이터가 저장될 때는 아래와 같은 순서로 저장된다.
변수 영역에서 빈 공간(@1003 : 임의의 주소 값)을 확보한다.
확보한 공간의 식별자(변수명)를 a로 지정한다.
일단 데이터 영역에서 1을 찾고, 없으면 데이터 공간을 하나 만들어(@5004)에 숫자 1을 저장한다.
변수 영역에서 a라는 식별자를 검색한다.(@1003).
앞서 저장한 문자열의 주소(@5004)를 @1003의 공간에 연결한다.
a += 10 코드 처리
1이 저장된 공간에 11을 할당하는 대신 데이터 영역에서 11을 찾고, 없으면 새로 만들어 별도의 공간에 저장한다.
그 주소를 1003의 공간에 연결한다.
불변값(immutable)
위의 처리 과정에서 한 번 만든 값은 다른 값으로 변경되지 않는다. 대신 새로 만드는 과정을 통해서만 변경이 일어나고 있다. 이것이 불변 값의 성질이다. 한 번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않는다.
let a = { x : 1 };
let b = a;
a.x += 10;
console.log(a.x); //11
console.log(b.x) //11
기본형과 다르게 let b = a; 코드를 통해 {x : 1} 의 주소 값이 b라는 변수에 담겼다.
a, b를 콘솔로 찍어보면 a값만 변화시켰지만, b값도 +10이 된 것을 알 수 있다.
즉 a, b는 서로 영향을 받고 있는 것을 볼 수 있다.
참조형 데이터가 저장될 때는 아래와 같은 순서로 저장된다.
1. 변수 영역에서 빈 공간(@1002 : 임의의 주소 값)을 확보한다.
확보한 공간의 식별자(변수명)를 a로 지정한다.
임의의 데이터 저장 공간(@5001) 에 데이터를 저장하려고 보니 여러 개의 프로퍼티로 이뤄진 데이터 그룹이다. 이 그룹 내부의 프로퍼티(x) 들을 저장하기 위해 별도의 변수 영역을 마련하고, 그 영역의 주소(@7103~ ?)를 @5001에 저장한다.
@7103 에 x라는 프로퍼티 이름을 지정한다.
데이터 영역에서 숫자 1을 검색하고 없으면, 임의로 @5003에 저장하고, 이 주소를 다시 @7103에 저장한다.
a.x += 10 코드 처리
가변값(mutable)
위의 처리 과정에서 10을 더하기 전과 더한 후 변수 a가 바라보고 있는 주소는 여전히 @5001로 변하지 않는 것을 볼 수 있다. 즉 '새로운 객체'가 만들어진 것이 아니라 기존의 객체 내부의 값만 1에서 11로 바뀐 것이다. 데이터 영역에 저장된 값은 모두 불변값이지만(1,11), 변수(@7103)에는 다른 값을 얼마든지 대입할 수 있다.
그렇다면 왜 참조형은 주소 값을 복사하는 것일까?
항상 그렇듯 성능의 문제이다. 참조형에는 어떤 값이 들어갈지 모른다. 그 말인즉슨, 주소 값이 아닌 값 전체를 복사하게 되면 용량이 어마어마하게 커질 수도 있다는 뜻이다.
기본형 데이터 (primitive type)
참조형 데이터(reference type)
먼저 불변(immutability)이란 뭘까? 단어에서 유추해볼 수 있다시피 '변하지 않는' 뜻이라고 생각하면 되겠다.
그럼 '불변 객체'란? '변하지 않는 객체' 즉 이미 할당된 객체가 변하지 않는다는 뜻을 가지고 있다.
자바스크립트에서 불변 객체를 만들 수 있는 방법은 기본적으로 2가지 인데 const와 Object.freeze()를 사용하는 것이다.
const
자바스크립트 키워드 중 하나인 const이다. ES6문법부터 let과 const를 지원한다.
const 키워드는 변수를 상수로 선언할 수 있다, 일반적으로 상수로 선언된 변수는 값을 바꾸지 못하는 것으로 알려져 있다.
그렇다면 상수로 선언한 객체는 불변 객체일까?
const test = {};
test.name = "mingyo";
console.log(test); // {"mingyo"}
ES6에서의 const는 할당된 값이 상수가 되는 것이 아닌 바인딩된 값이 상수가 되는, 즉 test변수가 상수가 되기 때문에 const 키워드로 선언된 test변수에는 객체 재할당은 불가능하지만 객체의 속성은 변경 가능하다.
재할당이 불가능 한 이유는 변수와 값(객체) 사이의 바인딩 자체가 변경이 되기 때문에 상수인 test변수는 재할당이 불가능한 것이고
객체의 속성이 변경가능 한 이유는 실제 객체가 변경은 되지만 ( {} -> name : "mingyo" ) 객체와 변수(test)사이의 바인딩은 변경이 되지 않기 때문에 객체의 속성은 변경가능한 것이다.
때문에 비록 재할당은 불가능하지만 객체의 속성을 변경함으로 인해 변수에 바인딩된 객체의 내용까지 변경이 되기 때문에 불변객체라고 하기는 힘들다. 따라서 Object.freeze()라는 JS내장메소드도 살펴보도록 하겠다.
Object.freeze()
자바스크립트에서 기본적으로 제공하는 메소드인 Object.freeze() 메소드이다. 공식 문서에서는 "객체를 동결하기 위한 메소드" 라고 적혀있다.
그렇다면 이 메소드를 사용하면 불변 객체를 만들 수 있을까?먼저 이 메소드의 사용법부터 알아보면,
let test = {
name : 'kim'
}
Object.freeze(test);
사용법은 간단하다. test 변수에 key value를 가진 객체를 바인딩 후 Object.freeze(test)를 사용해 바인딩된 변수를 동결 객체로 만들었다. 때문에 test 객체는 객체의 속성을 변경하는 시도는 불가능하다.
test.name = 'Jung';
console.log(test) // {name: 'kim'}
위와 같이 객체의 속성을 변경하는 시도는 무시된다.
그러나 Object.freeze()는 동결된 객체를 반환하지만 객체의 재할당은 가능하다.
test = {
age : 15
};
console.log(test); // {age: 15}
위와 같이 객체의 재할당은 가능하다. 때문에 Object.freeze()도 불변 객체라고 할 수는 없을 것 같다.
그럼 결국 불변 객체는 어떻게 만들 수 있냐면..
const와 Object.freeze()를 조합하여 만들 수 있다. (const의 재할당불가 + Object.freeze()의 객체속성 변경불가)
그렇다면 아래 코드와 같이 사용하면 된다.
const test = {
'name' : 'jung'
};
Object.freeze(test);
먼저 const키워드로 바인딩 된 변수를 상수화 시킨 다음, Object.freeze()로 해당 변수를 동결 객체를 만들면
객체의 재할당과 객체의 속성 둘 다 변경불가능한 불변 객체가 된다.
결론부터 말하자면 얕은 복사는 객체의 참조값(주소 값)을 복사하고, 깊은 복사는 객체의 실제 값을 복사합니다.
먼저, 자바스크립트에서 값은 원시값과 참조값 두 가지 데이터 타입의 값이 존재합니다.
.원시값은 기본 자료형(단순한 데이터)을 의미합니다. Number, String, Boolean, Null, Undefined 등이 해당합니다. 변수에 원시값을 저장하면 변수의 메모리 공간에 실제 데이터 값이 저장됩니다. 할당된 변수를 조작하려고 하면 저장된 실제 값이 조작됩니다.
.참조값은 여러 자료형으로 구성되는 메모리에 저장된 객체입니다. Object, Symbol 등이 해당합니다. 변수에 객체를 저장하면 독립적인 메모리 공간에 값을 저장하고, 변수에 저장된 메모리 공간의 참조(위치 값)를 저장하게 됩니다. 그래서 할당된 변수를 조작하는 것은 사실 객체 자체를 조작하는 것이 아닌, 해당 객체의 참조를 조작하는 것입니다.
원시값을 복사할 때 그 값은 또 다른 독립적인 메모리 공간에 할당하기 때문에, 복사를 하고 값을 수정해도 기존 원시값을 저장한 변수에는 영향을 끼치지 않습니다. 이처럼 실제 값을 복사하는 것을 깊은 복사라고 합니다. 하지만 이것은 자료형을 깊은 복사한 것입니다.
const a = 'a';
let b = 'b';
b = 'c';
console.log(a); // 'a';
console.log(b); // 'c';
// 기존 값에 영향을 끼치지 않는다.
참조값을 복사할 때는 변수가 객체의 참조를 가리키고 있기 때문에 복사된 변수 또한 객체가 저장된 메모리 공간의 참조를 가리키고 있습니다. 그래서 복사를 하고 객체를 수정하면 두 변수는 똑같은 참조를 가리키고 있기 때문에 기존 객체를 저장한 변수에 영향을 끼칩니다. 이처럼 객체의 참조값(주소값)을 복사하는 것을 얕은 복사라고 합니다.
const a = {
one: 1,
two: 2,
};
let b = a;
b.one = 3;
console.log(a); // { one: 3, two: 2 } 출력
console.log(b); // { one: 3, two: 2 } 출력
// 기존 값에 영향을 끼친다.
일반적으로 복사라는 개념을 생각한다면 깊은 복사가 떠오를 것입니다.(제가 그랬습니다 ;;)
하지만 객체를 복사할 때 = 키워드를 사용해서 복사하면 얕은 복사가 돼서 기존 변수 또한 수정돼서 당황하게 될 겁니다. 그렇다면 자바스크립트에서 얕은 복사 혹은 깊은 복사를 하는 방법은 어떤 것이 있을까요?
얕은 복사란 객체를 복사할 때 기존 값과 복사된 값이 같은 참조를 가리키고 있는 것을 말합니다. 객체 안에 객체가 있을 경우 한 개의 객체라도 기존 변수의 객체를 참조하고 있다면 이를 얕은 복사라고 합니다.
Array.prototype.slice()
얕은 복사 방법의 대표적인 예라고 할 수 잇습니다. start부터 end 인덱스까지 기존 배열에서 추출하여 새로운 배열을 리턴하는 메소드 입니다. 만약 start와 end를 설정하지 않는다면, 기존 배열을 전체 얕은 복사합니다.
const original = ['a',2,true,4,"hi"];
const copy = original.slice();
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
copy.push(10);
console.log(JSON.stringify(original) === JSON.stringify(copy)); // false
console.log(original); // [ 'a', 2, true, 4, 'hi' ]
console.log(copy); // [ 'a', 2, true, 4, 'hi', 10 ]
기존 배열에는 영향을 끼치지 않아서 깊은 복사로 보일 수 있지만, 원시값을 저장한 1차원 배열일 뿐입니다.
원시값은 기본적으로 깊은 복사입니다. Slice() 메소드는 기본적으로 얕은 복사를 수행합니다.
const original = [
[1, 1, 1, 1],
[0, 0, 0, 0],
[2, 2, 2, 2],
[3, 3, 3, 3],
]; c
onst copy = original.slice();
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
// 복사된 배열에만 변경과 추가.
copy[0][0] = 99;
copy[2].push(98);
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
console.log(original);
// [ [ 99, 1, 1, 1 ], [ 0, 0, 0, 0 ], [ 2, 2, 2, 2, 98 ], [ 3, 3, 3, 3 ] ]출력
console.log(copy);
// [ [ 99, 1, 1, 1 ], [ 0, 0, 0, 0 ], [ 2, 2, 2, 2, 98 ], [ 3, 3, 3, 3 ] ]출력
만약 1차원 배열이 아닌 중첩 구조를 갖는 2차원 배열이면 얕은 복사를 수행하게 됩니다.
const original = [
{
a: 1, b: 2,
},
true,
];
const copy = original.slice();
console.log(JSON.stringify(original) === JSON.stringify(copy)); // true
// 복사된 배열에만 변경.
copy[0].a = 99;
copy[1] = false;
console.log(JSON.stringify(original) === JSON.stringify(copy)); // false
console.log(original);
// [ { a: 99, b: 2 }, true ]
console.log(copy);// [ { a: 99, b: 2 }, false ]
또 다른 예시 입니다. 배열 안에 객체를 수정하고자 할 경우 얕은 복사를 수행하는 것을 볼 수 있습니다. 하지만 원시값은 기본적으로 깊은 복사라 기존 변수에 있는 값과는 다른 값을 도출하는 것을 볼 수 있습니다.
Object.assign()
Object.assign(생성할 객체, 복사할 객체)
메소드의 첫 번째 인자로 빈 객체를 넣어주고 두 번째 인자로 복사할 객체를 넣어주면 됩니다.
const object = {
a: "a", number: {
one: 1,
two: 2,
},
};
const copy = Object.assign({}, object);
copy.number.one = 3;
console.log(object === copy); // fals
econsole.log(object.number.one === copy.number.one); // true
복사된 객체 copy 자체는 기존 object와 다른 객체지만 그 안에 들어가 있는 값은 기존 object안의 값과 같은 참조 값을 가리키고 있습니다.
MDN에서는 Object.assign()에 대해 이렇게 설명했습니다.
깊은 클로닝에 대한 주의사항깊은 클로닝에 대해서, Object.assign() 은 속성의 값을 복사하기때문에 다른 대안을 사용해야합니다. 출처 값이 객체에 대한 참조인 경우, 참조 값만을 복사합니다.
마찬가지로 얕은 복사입니다.
const object = {
a: "a",
number: {
one: 1,
two: 2, }
,};
const copy = {...object}
copy.number.one = 3;
console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // true
깊은 복사(Deep Copy) 방법
깊은 복사된 객체는 객체 안에 객체가 있을 경우에도 원본과의 참조가 완전히 끊어진 객체를 말합니다.
사실 이 글을 쓰는 진짜 이유입니다. 복사를 하는 목적은 기존 객체의 값만 복사본으로 가져와 별도로 활용하기 위함이 대부분이라고 생각합니다. 기존 객체까지 건드린다면 이것은 복사를 하는 목적에 벗어난다고 생각합니다.
JSON.parse && JSON.stringify
JSON.stringify()는 객체를 json 문자열로 변환하는데 이 과정에서 원본 객체와의 참조가 모두 끊어집니다.
객체를 json 문자열로 변환 후, JSON.parse()를 이용해 다시 원래 객체(자바스크립트 객체)로 만들어줍니다.
이 방법이 가장 간단하고 쉽지만 다른 방법에 비해 느리다는 것과 객체가 function일 경우, undefined로 처리한다는 것이 단점입니다.
const object = {
a: "a",
number: {
one: 1,
two: 2,
},
arr: [1, 2, [3, 4]],
};
const copy = JSON.parse(JSON.stringify(object));
copy.number.one = 3;
copy.arr[2].push(5);
console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // falseconsole.log(object.arr === copy.arr); // false
console.log(object); // { a: 'a', number: { one: 1, two: 2 }, arr: [ 1, 2, [ 3, 4 ] ] }
console.log(copy); // { a: 'a', number: { one: 3, two: 2 }, arr: [ 1, 2, [ 3, 4, 5 ] ] }
재귀 함수를 구현한 복사
복잡하다는 것이 단점입니다.
const object = {
a: "a",
number: {
one: 1,
two: 2,
},
arr: [1, 2, [3, 4]],
};
function deepCopy(object) {
if (object === null || typeof object !== "object") {
return object;
} // 객체인지 배열인지 판단
const copy = Array.isArray(object) ? [] : {};
for (let key of Object.keys(object)) { copy[key] = deepCopy(object[key]);
}
return copy;} const copy = deepCopy(object);
copy.number.one = 3;
copy.arr[2].push(5);
console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false
console.log(object); // { a: 'a', number: { one: 1, two: 2 }, arr: [ 1, 2, [ 3, 4 ] ] }
console.log(copy); // { a: 'a', number: { one: 3, two: 2 }, arr: [ 1, 2, [ 3, 4, 5 ] ] }
Lodash 라이브러리 사용
라이브러리를 사용하면 더 쉽고 안전하게 깊은 복사를 할 수 있습니다. 설치를 해야 한다는 점과 일반적인 개발에는 효율적이겠지만, 코딩 테스트에는 사용할 수 없다는 것이 단점입니다.
const deepCopy = require("lodash.clonedeep")
const object = {
a: "a",
number: { one: 1, two: 2,
},
arr: [1, 2, [3, 4]],
};
const copy = deepCopy(object);
copy.number.one = 3;
copy.arr[2].push(5);
console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false
console.log(object); // { a: 'a', number: { one: 1, two: 2 }, arr: [ 1, 2, [ 3, 4 ] ] }
console.log(copy); // { a: 'a', number: { one: 3, two: 2 }, arr: [ 1, 2, [ 3, 4, 5 ] ] }
출처자료)
슬기로운 개발생활
거미줄코딩
뺑슨 개발 블로그:티스토리