이번 포스팅에서는 JavaScript만의 고유한 특징 중의 하나인 얕은복사(Shallow Copy)와 깊은복사(Deep Copy)의 개념에 대해 알아보도록 하겠다. 얕은복사와 깊은복사의 개념을 알아보기에 앞서 아래의 코드를 보면서 JavaScript에서 객체를 어떤식으로 인식하는지 살펴보자.
console.log(1===1)
console.log({}==={}) // 빈 객체와 빈 객체를 같다고 놓으면 false가 나온다.
const a = {}
const b = {}
console.log(a===b) // false
console.log([]===[]) // false (배열도 객체이기 때문이다)
console.log( 1===1 )의 결과를 살펴보면 우리가 잘 알고 있는 것처럼 number의 값이 1로 같기 때문에 그 결과 값이 true로 나오게 된다. 그렇다면 두개의 빈 객체 a = { } 과 b = { } 를 비교하면 어떻게 될까? 빈 객체와 빈 객체를 비교했기 때문에 console.log( )로 a===b의 결과를 보면 true가 나올 것만 같다. 하지만 그 결과는 false가 된다는 사실을 발견할 수 있다. 객체와 마찬가지로 두 배열을 비교한 [ ]===[ ] 역시 결과값이 false가 나오게 된다. 똑같이 생긴 객체 혹은 배열을 비교했는데 왜 결과값이 false로 나오게 되는 것일까?
JavaScript에서는 객체를 생성할 때마다 해당 객체가 저장되는 메모리의 공간에 새로운 값들을 부여하면서 생성한다. 쉽게 말해, 똑같은 빈 객체 두개를 생성했어도 그 객체들은 서로 다른 공간에 서로 다른 값들을 가지면서 저장되어 있는 것이다. 그 결과 똑같은 빈 객체를 비교했음에도 같지 않다는 결과값이 출력되는 것이다. 배열의 경우도 마찬가지이다. JavaScript에서는 배열을 객체로 인식하고 있기 때문에 객체에서와 같은 결과값이 출력되게 된다. 그렇다면 객체 안에 있는 요소들을 비교했을 때는 어떤 결과값이 출력되게 될까?
a = {
name : "ingoo"
}
b = {
name : "ingoo"
}
console.log(a===b) // false
console.log(a.name === b.name) // true
위의 코드에 나와있는 a객체와 b객체는 우리가 보기에 똑같은 요소를 가지고 있는 두 객체이다. 하지만 a===b를 비교한 결과값은 false가 나오게 되고 a의 요소 중 name의 값과 b의 요소 중 name의 값을 비교하면 그 결과값은 true가 됨을 확인할 수 있다. 이러한 결과가 나오게 되는 이유는 객체 안에 들어가 있는 값들은 변수라고 생각해도 무방하기 때문이다. a객체 안의 name이라는 변수에 "ingoo"라는 문자열을 저장했고 b객체 안의 name이라는 변수에 "ingoo"라는 문자열을 저장해서 이 두 문자열 "ingoo"==="ingoo"를 비교하는 과정으로 컴퓨터가 처리를 한다고 생각하면 된다.
그렇다면, JavaScript에서는 객체와 객체를 비교하는 과정들을 어떤식으로 처리해야만 하는가에 대한 이슈가 존재하게 된다. 여기서 등장하는 개념이 바로 얕은복사와 깊은복사이다. 간단하게 얕은복사와 깊은복사의 차이에 대해 설명해보면 깊은복사는 복사할 객체 안에 있는 요소들의 데이터만을 복사해서 다른 객체 안에 집어넣는다는 개념이고 얕은복사는 복사할 객체를 다른 객체가 참조하는 형태로 복사가 이루어진다는 개념이다. 이제 깊은복사와 얕은복사에 대해 조금 더 자세히 알아보도록 하자.
< 깊은 복사 >
깊은복사를 하는 과정을 코드를 통해 살펴보면서 깊은복사에 대해 알아보도록 하자.
// 깊은복사
const a = {name: "ingoo"}
const b = {} // b를 객체로 선언해줘야한다.
Object.assign(b, a) // a의 중괄호 안에 있는 내용 전체를 복사해서 b의 객체 안에 넣었다.
// (완벽하게 참조해서 가져오는 것이 아니다.)
console.log(a) // {name: "ingoo"}
console.log(b) // {name: "ingoo"}
console.log(a===b) // false (안에 있는 데이터만 복사해서 가져왔기 때문)
깊은복사에서 주의해야할 점은 복사할 a객체를 집어넣을 b를 객체로 선언해 주어야 한다는 점이다. const b = { };를 사용해 b를 객체로 선언하고 난 후에 Object.assign(b, a) 를 이용해 b객체에 a객체 안에 있는 요소들만을 복사해서 집어넣게 되는 것이다. Object.assign( )에서 첫번째 인자에는 복사한 것을 집어넣을 객체, 두번째 인자에는 복사할 객체를 넣어주면 된다. console.log( )를 찍어보면 a객체와 b객체 모두 똑같이 {name: "ingoo"} 형태인 것을 확인할 수 있다. 하지만 a===b를 이용해서 두 객체를 비교해 본 결과 false가 나오는 것을 확인할 수 있다. 이는 b가 a를 온전하게 참조하고 있는 것이 아닌 단순히 a 안에 있는 데이터만을 긁어와서 b 안에 집어넣었기 때문이라고 생각할 수 있다. a객체와 b객체가 생성될 때 두 객체를 구분하기 위해 부여되었던 값이 여전히 다르기 때문에 console.log(a===b)의 결과값이 false가 되는 것이다.
깊은복사를 활용하는 케이스를 살펴보도록하자.
// 중복값 제거
const dupArr = [1,2,3,1,2]
const temp = new Set(dupArr) // new Set()을 이용해 중복값을 제거해준다.
console.log(temp) // {1, 2, 3}
const uniqueArr = [...temp] // temp 안에 있는 요소만 가져와서 배열 안에 넣을 수 있다.
console.log(uniqueArr) // [1, 2, 3]
위의 코드에서처럼 dupArr = [1, 2, 3, 1, 2]에 존재하는 중복값을 제거해서 새로운 배열을 생성하고 싶은 경우 깊은복사 개념을 활용할 수 있다. new Set( )을 이용하여 중복값을 제거한 후 Set 객체를 temp 변수에 할당해준 다음 temp를 출력하면 {1, 2, 3}이 나오는 것을 확인할 수 있다. 하지만 나는 배열 형태로 결과값을 얻고 싶은데 출력된 값은 배열 형태가 아니다. 이때 uniqueArr = [...temp] 을 이용해 Set 안에 있는 요소들만을 가져와서 배열 안에 넣는 과정을 실행해 볼 수 있다. 그 결과 uniqueArr는 배열 형태이면서 1, 2, 3을 원소로 가지게 된 것을 확인할 수 있다. 참고로 여기서 등장한 ...temp는 Spread 문법인데 이와 관련된 설명은 생략하도록 하겠다.
참고) 깊은복사를 할 때 아래와 같이 Spread 문법을 사용하면 원본객체는 훼손하지 않는 상태에서 데이터들만을 복사해 오게 된다.
const blockHeader = {
hash: 000000000000000000000000000, // 블록의 이름이라고 생각하자 (16진수로 표현)
timestamp: 1641519310, // 특정 날을 0으로 두고 +1씩 하면서 시간을 계산하는 방법
height: 717521, // 블록의 번호, 몇번째 블록인지를 나타냄
difficulty: 4, // 채굴 난이도
nonce: 4006, // 블록을 생성하기 위해 풀어야 하는 문제
}
const blockBody = [
{
fee:"0.00000000 BTC",
Hash: 8898498489456546
},
{
fee:"0.00000000 BTC",
Hash: 8898498489456546
},
{
fee:"0.00000000 BTC",
Hash: 8898498489456546
}
]
// 블록은 객체라고 생각하면 쉽다.
// ... : Spread 문법
const block = {
blockHeader: {...blockHeader},
bolckBody: [...blockBody]
}
console.log(block)
// {blockHeader: {...}, blockBody: Array(3)}
< 얕은복사 >
얕은복사는 깊은복사에서처럼 복사할 객체 안에 있는 데이터만을 긁어와서 다른 객체 안에 집어넣는다는 개념과는 다른 개념이다. 깊은복사와 다르게 얕은복사는 참조하는 형태를 띠고 있다. 객체 a와 변수 b가 있을 때 b변수에 a객체의 주소값을 부여하게 되면 해당 주소는 a객체를 가리키게 되고 b변수는 a객체를 참조하는 형태의 객체가 되는 것이다. C언어에서 등장하는 포인터 개념과 유사한 방식이라고 생각하면 된다. 이렇게 생성된 b객체는 a객체를 참조하고 있는 형태로 존재하기 때문에 a객체와 b객체를 같다고 비교했을 때 true값이 출력되게된다. 이제 얕은복사를 하는 과정을 코드를 통해 살펴보자.
// 얕은복사
const a = {name: "ingoo"}
let b;
b = a; // 정확하게 a를 참조해서 가져오는 것
// b는 선언한 상태에서 b에 a를 할당해야 한다.
console.log(a) // {name: "ingoo"}
console.log(b) // {name: "ingoo"}
console.log(a===b) // true
b.key = 180; // b는 a를 참조하고 있기 때문에 값이 같이 바뀐다.
console.log(a) // {name: "ingoo", key: 180}
console.log(b) // {name: "ingoo", key: 180}
console.log(a===b) // true
a.age = 33 // a를 변경해도 b가 같이 컨트롤된다.
console.log(a) // {name: "ingoo", key: 180, age:33}
console.log(b) // {name: "ingoo", key: 180, age:33}
console.log(a===b) // true
얕은복사를 할 때 주의할 점은 변수 b를 선언한 상태에서 b에 a객체를 할당해야 한다는 점이다. 얕은복사는 복사할 객체인 a를 b가 참조하는 과정으로 이루어지기 때문에 변수 b에 a객체를 할당하는 방법을 통해 b변수에 a객체의 주소값을 저장하게 된다. 그 결과 b변수를 불러왔을 때 b변수는 a객체를 가리키게 되고 a객체를 온전히 참조하게 되는 것이다.
코드를 살펴보면 변수 b에 a객체를 할당한 결과 console.log( )로 출력해보았을 때 같은 결과값을 출력한다는 것을 확인할 수 있다. 뿐만아니라 b변수가 a객체를 온전히 참조하고 있기 때문에 console.log(a===b)의 결과 역시 true로 나오게 되는 것을 볼 수 있다.
+) 얕은복사는 참조의 형태를 띠고 있는 개념이기 때문에 위의 코드에서처럼 b.key = 180; 혹은 a.age = 130; 과 같이 a, b 둘 중 하나의 값이 변경되더라도 서로에게 영향을 미치게 된다.
'JavaScript' 카테고리의 다른 글
JavaScript - DOM (1) (0) | 2022.01.10 |
---|---|
JavaScript - 콜백(callback) (0) | 2022.01.10 |
JavaScript기초 - 정리(4) 배열(array) , 객체(object) (0) | 2022.01.04 |
JavaScript기초 - 정리(3) 변수 선언 , 이스케이프 문자 , else if문 , 함수 (0) | 2022.01.04 |
JavaScript기초 - 정리(2) if문 , for문 , 함수 (0) | 2022.01.04 |