JavaScript의 얕은 복사와 깊은 복사
JavaScript에서는 객체를 복사하는 두가지 방법이 있다.
하나는 얕은 복사(shallow copy)이고, 하나는 깊은 복사(deep copy)이다.
두 복사의 차이점을 알기 이전에 참조타입과 원시타입에 대해서 알고 있어야한다.
원시타입(primitive Type)과 참조타입(reference Type)
원시타입과 참조타입의 차이점에 대해서 먼저 짚어보고 가도록 하자.
원시타입(primitive Type)
- 숫자타입(number Type) : 정수, 부동소수점 숫자, NaN(Not a Number), Infinity 등을 나타낸다.
- 문자열 타입(String Type) : 문자열을 나타낸다.
- 불리언 타입(Boolean Type) : true 또는 false 값을 나타낸다.
- null 타입(null Type) : 값이 없음을 나타낸다.
- undefined 타입(undefined Type) : 값이 할당되지 않았음을 나타낸다.
- 심볼 타입(Symbol Type) : ES6에서 추가된 데이터 타입으로 객체 속성의 키로 사용된다.
원시 타입의 값은 변수에 직접 저장되고, 값이 복사될 때 변수와 함께 복사된다.
이러한 특성 때문에 두 변수가 독립적인 값을 가지게 된다.
위 예제에서 변수 a에 값을 할당한 후 변수 b에 a값을 할당한다.
그러나 a값을 변경하더라도 b값은 변경되지 않는다.
이것은 변수 a와 b가 각각의 값을 가지고 있기 때문이다.
참조타입(reference Type)
참조타입은 객체(object), 배열(array), 함수(function) 등을 포함한다.
참조타입의 값은 객체나 배열이 저장된 메모리 주소를 가리키는 포인터이다.
변수에 할당된 값은 메모리 주소이며, 이 주소를 통해 객체나 배열에 접근할 수 있다.
위 예제코드에서 객체 personInfo
를 생성하고, 이 객체를 anotherPersonInfo
변수에 할당한다.
이때 anotherPersonInfo
변수에는 personInfo
객체의 메모리 주소가 저장된다.
이후, anotherPersonInfo
변수의 age 속성 값을 변경하면 personInfo
객체의 age속성 값도 함께 변경된다.
이는 personInfo
와 anotherPersonInfo
변수가 같은 객체를 가르키기 때문이다.
값은 변수에 직접 저장되지 않고, 메모리의 힙(heap)에 저장된다.
따라서 참조 타입의 값은 변수와 함께 복사되지 않으며, 변수가 참조하는 객체나 배열이 변경되면 변수의 값도 변경된다.
얕은 복사(shallow copy)
객체와 해당 속성을 모두 복사하지만 중첩된 객체나 배열은 원본 객체와 동일한 메모리 위치를 참조한다.
이는 중첩된 객체를 변경하면 복사된 객체뿐만 아니라 원본 객체에도 영향을 미친다는 의미이다.
아래의 예제를 한번 살펴보자
let personInfo = { name: 'potato', age:99 };
let anotherPersonInfo = personInfo;
위와 같이 코드를 작성하고 console.log를 찍었을때의 personInfo
의 age값과 anotherPersonInfo
의 age값을 살펴보자.
두 값 모두 age가 99로 되어있는것을 확인할 수 있다.
그럼 여기서 anotherPersonInfo.age = 10;
으로 한번 변경해본 후 에 console.log로 출력되는 값을 확인해보자.
anotherPersonInfo.age = 10;
으로 하였는데, personInfo.age
의 값도 10으로 변경된것을 확인할 수 있다.
이것으로 참조타입(reference type)값은 얕은 복사가 된다는것을 확인할 수 있다.
객체와 해당 속성을 모두 복사하지만 원본 객체와 동일한 메모리 위치를 참조하기 때문이다.
참조타입값은 복사본이 바뀌면 원본도 같이 변경된다.
이를 방지하는 방법 또한 존재한다.
바로 Object.assign()메서드를 사용하는 것이다.
Object.assign()메서드 사용하기
앞선 예제코드에서 사용하던 let anotherPersonInfo = personInfo;
를 사용하지 않고,let anotherPersonInfo = Object.assign({}, personInfo);
를 사용하면anotherPersonInfo.age = 10;
으로 변경했을때 anotherPersonInfo
가 독립적으로 값을 변경하고있는것을 확인할 수 있다.
여기서 사용한 Object.assign()메서드에 대해서 좀 더 알아보자면,
Object.assign(어디에다가, 어떤값을 불러올건지) 로 사용해주면된다.
위 예제코드에서는 {}빈 객체에다가 personInfo의 값을 불러오고 있다.
Object.assign()메서드만 이러한 역할을 해주는것은 아니다.
ES6에서 부터 도입된 스프레드 연산자를 사용하는 방법도 있다.
스프레드 연산자(...) 사용하기
스프레드 연산자를 사용하였을때도 Object.assign()과 동일한 결과를 도출하는것을 확인할 수 있다.
하지만 이것은 완전한 방지법이 아니다.
새로운 예제코드로 함께 살펴보도록 하자.
기존에 있던 personInfo
라는 객체 안에 skills라는 객체를 또 생성하였다.
그리고 이후 anotherPersonInfo.skills.primary = 'Full Stack';
로 변수의 값을 변경하였을때
예제코드의 이미지에서 확인할 수 있듯이 personInfo의 skills도 변경되었고, anotherPersonInfo의 skills도 변경되었음을 알 수 있다.
이것으로 통해 원본 객체인 personInfo
에도 영향이 끼쳤음을 알 수 있다.
즉, 얕은 복사란 객체를 복사할 때 기존값과 복사된 값이 같은 참조를 가리키고 있는 것을 의미한다.
객체 안에 객체가 있을 경우 한개의 객체라도 기존 변수의 객체를 참조하고 있다면 이를 얕은 복사라고 한다.
- 객체를 복사할 때, 해당 객체만 복사하여 새 객체를 생성한다.
- 복사된 객체의 인스턴스 변수는 원본 객체의 인스턴스 변수와 같은 메모리 주소를 참조한다.
- 따라서, 해당 메모리 주소의 값이 변경되면 원본 객체 및 복사 객체의 인스턴스 변수 값은 같이 변경된다.
깊은 복사(Deep Copy)
그렇다면 이번에는 깊은 복사를 하는 방법에 대해서 알아보자.
전체 복사를 통해 모든 속성과 중첩된 객체 또는 배열에 대한 새 메모리 위치를 사용하여 새 객체를 생성하는 복사본을 의미한다.
이는 복사된 객체나 중첩된 객체 또는 배열을 변경해도 원본 객체에는 영향을 끼치지 않는것을 의미한다.
JSON.parse(JSON.stringify()) 를 사용하는 방법
여기서 우리는 let anotherPersonInfo = JSON.parse(JSON.stringify(personInfo));
에 주목해야한다.
JSON.parse()
메서드는 JSON 문자열의 구문을 분석하고, 그 결과 JavaScript값이나 객체를 생성한다.
옵션으로 reviver 함수를 인수로 전달할 경우, 결과를 반환하기 전에 변형할 수 있다.
JSON.stringify()
메서드는 JavaScript 값이나 객체를 JSON문자열로 변환한다.
옵션으로 replacer를 함수로 전달할 경우 변환 전 값을 변형할 수 있고, 배열로 전달할 경우 지정한 속성만 결과에 포함한다.
personInfo
에 calculateAge
함수와 joiningDate
를 추가한 후 다시한번 해당 메서드의 작동에 대해서 알아보도록 하자.
우리는 위 예제코드에서console.log(typeof(personInfo.calculateAge));
와 console.log(typeof(anotherPersonInfo.calculateAge));
부분에 우선 주목해보자.
우리는 JSON.parse()메서드와 JSON.stringify()메서드를 사용하면서 깊은 복사를 수행한것으로 보이지만 anotherPersonInfo
에서 undefined가 출력되고있음을 확인할 수 있다.
이는 함수가 손실되었음을 의미한다.
또한 console.log(typeof(personInfo.joiningDate));
와 console.log(typeof(anotherPersonInfo.joiningDate));
를 통해
날짜에 대한 객체 또한 손실되었음을 알 수 있다.
이것을 어떻게 해결해야할까?
재귀함수를 사용하여 깊은 복사를 실현하는 함수를 짜는 방법도 있고,
lodash라는 라이브러리를 사용하여 cloneDeep메소드를 진행하는 방법도 있다.
JQuery를 이용하거나 immutable.js를 이용하는 방법 등 다양한 방법이 있다고 하지만 이것은 다음에 자세히 알아보도록 하자.
- 객체를 복사할 때, 해당 객체와 인스턴스 변수까지 복사한다.
- 전부를 복사하여 새 주소에 담기 때문에 참조를 공유하지 않는다.
- 객체가 참조 타입의 멤버를 포함할 경우 참조값의 복사가 아닌 참조된 객체 자체가 복사되는 것을 의미한다. 원본의 참조는 더이상 하지 않는다.
참고자료 : shallowCopy vs deepCopy youtube, Object.assign MDN, JSON.parse MDN, JSON.stringify MDN
'개발공부 > 기술면접준비' 카테고리의 다른 글
innerHTML, innerText, textContent의 차이 (0) | 2023.08.31 |
---|---|
[JavaScript]원시자료형과 참조자료형, 콜 스택(call stack)과 메모리 힙(memory heap)에 대하여 (0) | 2023.08.31 |
JavaScript의 event.preventDefault()와 event.stopPrapagation()에 대해서 (0) | 2023.08.29 |
스코프(Scope)에 대해서 (0) | 2023.08.29 |
id 속성과 class 속성의 차이점 (0) | 2023.08.28 |