기본형 데이터와 참조형 데이터

최재홍·2023년 4월 5일
0

메모리를 기준으로 다시 한번 생각해보는 주요 개념

  1. 변수 vs 상수
       a. 변수 vs 상수
           i. 변수 : 변수 영역 메모리를 변경할 수 있음
           ii. 상수 : 변수 영역 메모리를 변경할 수 없음
  2. 불변하다 vs 가변하다
           i. 불변하다 : 데이터 영역 메모리를 변경할 수 없음
           ii. 가변하다 : 데이터 영역 메모리를 변경할 수 있음

한 변수에 데이터를 저장하고, 또 다른 변수에 같은 데이터를 저장(복사)하여, 복사된 데이터를 변경하는 과정에 대해서 생각해보자.

기본형 데이터는 이러한 과정으로 진행될 것이다.

var a = 1;
var b = a;
b = 2;

console.log(a)
>1
console.log(b)
>2

var a = 1에서 a는 '변수 영역', 1은 '데이터 영역'이라고 할 수 있을 것이다. 'a'라는 식별자는 변수 영역으로서 메모리의 한 부분을 차지하고, '1'이라는 데이터가 '담긴' 메모리 영역에 대한 주소를 할당받게 된다.
한편, 새롭게 변수 영역으로서 메모리에 저장된 'b'라는 식별자 역시도 'a'가 저장된 주소를 할당받게 되어 최종적으로 'a'가 바라보고 있는 '1'이라는 데이터를 전달받게 된다.
그 이후, 'b'의 데이터를 2로 변경하게 되면, '2'라는 데이터를 담은 새로운 메모리 영역이 생겨나고, 그 영역에 대한 주소값을 'b'가 할당 받으면서 과정이 마무리 된다.
그 이후에 'a'와 'b'는 독립적으로 존재하게 되며, b의 변경은 a에 아무런 영향도 미치지 못 한다. 그래서 기본형 데이터는 '불변하다'고 표현할 수 있는 것이다.

하지만 참조형 데이터를 복사하고, 임의로 변경했을 때의 과정은 이렇게 진행된다.

var obj1 = { c: 10, d: "ddd" };
var obj2 = obj1; 

obj2.c = 20;

console.log(obj1 === obj2);
>true

얼핏 생각하기로는 obj1[c]obj2[c]가 각각 10과 20으로 다르기 때문에 콘솔로 'false'가 찍힐 것이라고 예상하기 쉽지만, 참조형 데이터의 저장 방식 때문에 위와 같은 결과가 초래된다.

우리는 obj2.c라는 표현식으로 'obj1'의 'c'가 아닌, 다름아닌 바로 'obj2'의 'c'라고 명시했다고 생각하지만 컴퓨터가 데이터를 저장하는 방식에 따르면 참조형 데이터가 생성되는 시점에서 obj1도 obj2도 결국 'c'라는 똑같은 영역을 참조하게 되기 때문이다. 그래서 c에 대한 변경점이 생기면 원본도 영향을 받게 되는 것이다.

하위에 프로퍼티를 가진 참조형 데이터는 변수에 할당된 주소를 따라갔을 때, '특정 값'이 할당된 메모리 영역으로 도착하는 것이 아니라, '어디를 참조할지'가 저장되어 있는 메모리영역으로 도착하기 때문에 복사된 새로운 변수가 참조할 주소를 바꾸는 것으로 기존의 변수가 참조할 주소도 바뀌게 된다.

그렇기 때문에 참조형 데이터는 '불변하지 않다', '가변하다'고 표현할 수 있게 된다.

이 문제에 대한 예시는 다음과 같다.

<script>
// user 객체를 생성
var user = {
	name: 'wonjang',
	gender: 'male',
};

// 이름을 변경하는 함수, 'changeName'을 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티(속성)에 접근해서 이름을 변경했네요! -> 가변
var changeName = function (user, newName) {
	var newUser = user;
	newUser.name = newName;
	return newUser;
};

// 변경한 user정보를 user2 변수에 할당하겠습니다.
// 가변이기 때문에 user1도 영향을 받게 될거에요.
var user2 = changeName(user, 'twojang');

// 결국 아래 로직은 skip하게 될겁니다.
if (user !== user2) {
	console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name); // twojang twojang
console.log(user === user2); // true
</script>

이렇게 참조형 데이터는 가변성의 문제점을 가지고 있기 때문에 복사를 할 때 유의하여 복사할 필요가 있다. 다음은 이러한 유의점을 염두에 두고 개선한 개선 예시이다.

<script>
// user 객체를 생성
var user = {
	name: 'wonjang',
	gender: 'male',
};

// 이름을 변경하는 함수 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티에 접근하는 것이 아니라, 아에 새로운 객체를 반환 -> 불변
var changeName = function (user, newName) {
	return {
		name: newName,
		gender: user.gender,
	};
};

// 변경한 user정보를 user2 변수에 할당하겠습니다.
// 불변이기 때문에 user1은 영향이 없어요!
var user2 = changeName(user, 'twojang');

// 결국 아래 로직이 수행되겠네요.
if (user !== user2) {
	console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name); // wonjang twojang
console.log(user === user2); // false 👍
</script>

이렇게 새로운 객체를 선언하면 메모리는 새로이 선언된 객체를 위한 영역을 별도로 마련하기 때문에 단순히 프로퍼티의 이름이 같다고, 같은 주소를 참조하지 않게 된다.

하지만 위의 방법도 최선은 아니다. changeName()함수가 새로운 객체를 만드는 과정에서 'gender'라는 프로퍼티를 하드코딩으로 입력했기 때문이다. 만약 이러한 프로퍼티가 수없이 많다면 우리는 하드코딩으로 이에 대처할 수 밖에 없을 것이다.

그래서 제시된 방법이 '얕은 복사'이다. 예시는 다음과 같다.

<script>
//이런 패턴은 어떨까요?
var copyObject = function (target) {
	var result = {};

	// for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근할 수 있습니다.
	// 하드코딩을 하지 않아도 괜찮아요.
	// 이 copyObject로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경하면
	// 되겠죠!?
	for (var prop in target) {
		result[prop] = target[prop];
	}
	return result;
}
</script>

빈 객체를 선언하고 복사하고자 하는 객체의 프로퍼티에 일괄적으로 대응할 수 있는 for ~ in구문으로 객체의 모든 프로퍼티에 접근하여 복사해올 수 있다.

하지만 이 역시도 '얕은 복사'라는 이름에 맞게 근본적인 해결책이 되지는 못 한다. 왜냐하면 프로퍼티에 참조형 데이터가 다시 할당된 중첩된 객체에 대해서는 완벽한 복사를 할 수 없기 때문이다.

  1. 얕은 복사 : 바로 아래 단계의 값만 복사. 문제점은 중첩된 객체의 경우 참조형 데이터가 저장된 프로퍼티를 복사할 때, 주소값만 복사
  2. 깊은 복사 : 내부의 모든 값들을 하나하나 다 찾아서 모두 복사하는 방법

그래서 이러한 문제점을 근본적으로 해결하기 위해서는 참조형 데이터를 복사할 때 내부 프로퍼티에 대한 값으로 참조형 데이터가 등장할 때마다 '얕은 복사'를 반복해서 중첩된 depth에 대해서도 복사가 진행되어야 한다.
따라서 위에서 선언된 copyObject()가 실행되면서 참조형 데이터를 만날 때마다 더이상 참조형 데이터를 만나지 않을 때까지 함수 자신 내부에서 자기자신을 다시 실행시켜야 한다. 이를 '재귀적 수행'이라고 한다.
그래서 이 모든 문제점을 개선한 복사 방식이 다음과 같다.

<script>
var copyObjectDeep = function(target) {
	var result = {};
	if (typeof target === 'object' && target !== null) {
		for (var prop in target) {
			result[pop] = copyObjectDeep(target[prop]);
		}
	} else {
		result = target;
	}
	return result;
}
</script>

0개의 댓글