데이터 타입은 크게 기본형, 참조형 두 가지로 나눌 수 있다
값의 크기가 크다 를 전제로 한다.
따라서 기본적으로 Object 의 형을 가지며 Array, Function 등이 이에 해당한다.
이 참조형과 기본형을 구분하는 기준으로 1. 값의 복제 방식, 2. 불변성 여부를 들 수 있다.
1. 복제 방식
- 기본형 : 값이 담긴 주소값을 바로 복제
- 참조형 : 값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제
참조형에 대한 이해가 조금 어려운데, 이후에 데이터 영역에 관해 설명하면서 다시 봐보도록 하자.
2. 불변성의 여부 : 이 또한 메모리, 데이터 영역의 관점에서 봐야 한다.
- 기본형 : 불변성 O
- 참조형 : 불변성 X, 가변적
불변성에 대해서 더 알기 전에 배경 지식을 먼저 파악하자
데이터의 크기, 흔히 말하는 비트와 바이트에 대해 먼저 알고 가야 한다.
컴퓨터가 이해할 수 있는 가장 작은 단위, 0과 1을 말한다.
1비트가 늘어날 때마다 표현할 수 있는 가짓수는 2^n 크기로 계속 증가하고, 그만큼 데이터가 클 수록 이 비트로 표현하기 어려워진다.
해서 바이트의 개념이 등장한다.
1바이트 = 8비트 의 크기이다. 일반적으로 메모리 주소를 이 바이트 단위로 나타낸다.
//선언과 할당을 구분지어서
var str;
str = 'test';
//선언과 할당을 붙여서
var str = 'test';
변수 선언/할당을 다음과 같이 두가지 경우로 나눌 수 있다.
이 과정을 메모리의 관점으로 나타낸다면
변수 영역 | 주소 | 1002 | 1003 |
데이터 | str / @5002 | ||
데이터 영역 | 주소 | 5002 | 5003 |
데이터 | 'test' |
다음처럼 보여줄 수 있다.
변수 영역과 데이터 영역을 먼저 나누는데 이를 다시 주소와 데이터 로 나눈다.
주소값에 들어간 1002, 1003 ... 은 당연히 예시 주소값이다.
var str = 'test' 코드를 봤을 때, 이를 str 과 'test' 로 나눌 수 있다.
이 'test' 는 데이터, str 은 변수로 취급하고, 선언이 먼저 이루어지므로 빈 주소 1002의 데이터에 str 을 넣는다.
이후 str 에 들어갈 데이터 'test' 를 다시 데이터 영역의 빈 주소인 5002 의 데이터에 넣는다.
이제 중요한 포인트는, 이 str 에 해당하는 값 'test' 는 이 데이터 자체를 불러오는 것이 아니라 데이터의 주소값을 받아오는 것이다.
이를 보고 '값(test) 를 변수 - 데이터 영역에 바로 대입할 수 있지 않냐? 굳이 주소값으로 불러와야 하냐?' 라고 물을 수 있다. 그렇게 하지 않는 이유로는 2가지가 있는데,
1. 데이터를 자유롭게 변환하기 위해
2. 메모리를 효율적으로 관리하기 위해
가 그것이다.
1번의 경우, 만약 변수 - 데이터 영역에 값을 직접적으로 넣는다면 이후 코드 시행 중 값이 변동되거나 기존 값 자체가 클 경우 이 [ str / 'test' ] 가 차지하는 주소가 더 늘어나기 때문이다. 만약 1003의 주소에 빈 값이 아니라 어떠한 데이터가 들어있었다고 한다면, 1002의 주소가 더 늘어날 경우 오른쪽으로 계속 밀려날 것이다. 값을 하나 바꾸기 위해서 다른 데이터의 주소를 전부 변경해줘야 하는 비효율적인 상황이 발생한다.
2번의 경우는 같은 데이터를 여러번 저장해야 할 때를 예시로 들 수 있다.
예를 들어 Var_1 = 1, Var_2 = 1, Var_3 = 1 과 같이 같은 값을 여러번 저장해야 할 때가 있다. 이 Var_1, Var_2, Var_3 이 모두 별도의 변수 - 주소 영역을 갖게 될 텐데, 1이라는 값을 값을 변수 - 데이터 영역에 바로 대입하게 된다면 모두 메모리를 각자 차지하게 된다.
그럴 필요 없이 1 이라는 값을 별도의 영역에 저장하고, 이 주소값을 불러온다면 [ Var_1 / @1주소값 ], [ Var_2 / @1주소값 ], [ Var_3 / @1주소값 ] 으로 더 효율적으로 메모리를 관리할 수 있게 된다.
이러한 두가지의 이유로 변수와 데이터를 서로 다른 영역으로 저장하는 것이다.
다시 데이터 형으로 돌아간다. 먼저 기본형 데이터부터 살펴보자.
1. 변수 vs 상수
- 변수 : 변수 영역 메모리 변경 가능
- 상수 : 변수 영역 메모리 변경 불가능
의 특징을 가지고 가자.
2. 불변 vs 가변
여기서 이 '불변하다' 는 데이터 영역의 메모리를 변경할 수 없다 는 뜻을 내포한다.
예를 들어 var a = 8; 이라고 할 때 a는 변수 영역, 8은 데이터 영역에 저장된다고 위에서 설명했다.
여기서 a = 9; 로 대입할 경우, a의 메모리 영역 중 데이터에 들어간 주소값이 바뀌게 된다.
변수 영역 | 주소 | 1002 | 1003 |
데이터 | a / @5002 -> @5003 | ||
데이터 영역 | 주소 | 5002 | 5003 |
데이터 | 8 | 9 |
이처럼 변수 영역의 메모리를 변경하는 것이고, 이게 가능하다는 것은 '변수' 임을 말한다.
하지만 데이터 영역의 8 이라는 값은 그대로 남아있다. 9라는 값을 추가해 이를 대입한 것이지, 8이라는 값 자체를 바꾼게 아니라는 말인데, 이는 데이터 영역은 메모리를 변경하지 못한다는 뜻이 되고, 이를 '불변하다' 라고 할 수 있다.
이 사용하지 않는 값 8은 '가비지' 라고 부르는데, 이러한 가비지 메모리들을 정리해주는 객체 '가비지 컬렉터' 에 의해 자동적으로 메모리 관리가 진행된다.
var obj = {a : 1, b : ‘bbb’};
이러한 코드가 있다고 하고, 이 참조형 데이터의 변수 할당 과정을 먼저 그려보자.
변수 영역 | 주소 | 1001 | 1002 |
데이터 | obj / @5001, @5002 | ||
데이터 영역 | 주소 | 5001 | 5002 |
데이터 | 1 | 'bbb' |
이렇게 그린다고 하더라도, 이는 a와 b라는 Key 값이 반영되지 않은 부정확한 표현이다.
이러한 객체, 참조형 데이터의 경우 그 데이터만을 위한 별도의 저장공간이 생성된다.
obj 별도 영역 | 주소 | 7001 | 7002 |
데이터 | a / @5001 | b / @5002 |
이렇게 a와 b가 들어가는 별도의 메모리 영역이 생성되고, 이 a와 b에 1과 'bbb'의 데이터 주소가 들어간다.
따라서 변수 영역의 obj 데이터가 가지는 주소값 또한 @7001, @7002 가 되어야 맞다.
이러한 참조형 데이터의 경우 기본형 데이터와의 차이점으로 이 별도의 저장 공간이 있냐/없냐 를 들 수 있다.
또한 이 참조형 데이터는 불변하지 않다. = 가변하다.
예를 들어 변수를 추가할 경우,
1. 데이터 영역은 여전히 불변하다. ( 값을 새로 추가하는 행동으로 이뤄짐. 기존 값 관여 X )
2. 변수 영역이 아니라 이 별도 영역이 변경되고 있다. = 가변하다
5003 |
'new Data' |
7002 |
b / @5002 -> @5003 변경! |
여기까지 이해했다면 위에서 언급한 ' 참조형 : 값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제' 의 내용을 이해할 수 있을 것이다.
그렇다면 이 일차원 객체가 아니라, 객체 안에 객체가 존재하는 중첩 객체의 경우는 어떨까?
var obj = {
x: 3,
arr: [3, 4, 5],
}
메모리 저장 방식 설명은 위에 했으니 빠르게 그림으로 보여주고 넘어가도록 한다.
쉽게 말해서 객체 obj 를 위한 별도 공간 하나, 객체 안의 객체인 arr 을 위한 별도 공간이 하나 더 생성되는 방식이다.
데이터 영역의 3, 4, 5 값은 재사용 여지가 있으면 추가하지 않고 주소값을 가져와 그대로 재사용한다.
// STEP01
var a = 10;
var obj1 = { c: 10, d: 'ddd' };
// STEP02 : 복사 수행
var b = a;
var obj2 = obj1;
이렇게 설명할 수 있다.
//기본형 데이터
var a = 10;
var b = a;
//참조형 데이터
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
b = 15;
obj2 = { c: 20, d: 'ddd'};
이렇게 obj2 = obj1 과 같은 코드를 통해 객체 자체를 변경할 경우, 아예 새로운 메모리 영역이 생긴다.
obj2 를 변경해도 obj1에는 영향이 있지 않다는 것이다.
이를 코드로 나타내면 다음과 같다.
var user = {
name: 'wonjang',
gender: 'male',
};
var changeName = function (user, newName) {
var newUser = user;
newUser.name = newName;
return newUser;
};
var user2 = changeName(user, 'twojang');
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // twojang twojang
console.log(user === user2); // true
크게 중요한 내용만 살펴보면 먼저 user 객체를 선언, user2 객체를 선언하는데 user 객체의 데이터에서 changeName 함수를 통해 name 값만 변경해주었다.
이 name 값을 변경하는 행위는 user 와 user2 객체가 서로 다르기를 원하는 의도지만, 실상은 그렇지가 않다.
저 if 문의 시행을 보면 출력되지 않는 것으로 알 수 있다.
이는 다른 파트의 게시글에서 같은 개념을 소개한 적이 있어 첨부한다.
2024.07.17 - [유니티 학습/세미나 자료] - #3. 클래스와 오브젝트
그럼 이를 개선해보도록 하자
var user = {
name: 'wonjang',
gender: 'male',
};
var changeName = function (user, newName) {
return {
name: newName,
gender: user.gender,
};
};
var user2 = changeName(user, 'twojang');
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // wonjang twojang
console.log(user === user2); // false
changeName 의 함수 로직을 변경, 새로운 객체를 할당하는 방식으로 수정했다.
결과적으로 if 문이 정상적으로 출력되는 모습을 볼 수 있다.
이를 통해 알 수 있는 것은 메모리 관점에서 의도적이지 않은 동작을 피하기 위해, 가변성을 띄는 데이터들을 불변의 방식으로 바꿔주려는 노력이 항상 필요하다는 것이다.
물론 이 코드 또한 최선은 아니다.
만약 user 와 user2의 속성이 많다면 changeName 에서 수정해야 하는 코드들이 너무나 많아질 것이다.
그 말은 즉 '유지보수성' 이 떨어진다는 말이다.
이를 해결하기 위해 '얕은 복사' 개념이 등장한다.
위 코드에서 changeName 함수를 이런 패턴으로 바꿀 수 있다.
var copyObject = function (target) {
var result = {};
for (var prop in target) {
result[prop] = target[prop];
}
return result;
}
for - in 문을 통해 객체의 모든 프로퍼티에 접근하도록 하는 것이다.
이 패턴을 사용해 코드를 개선하면 다음과 같다.
var copyObject = function (target) {
var result = {};
for (var prop in target) {
result[prop] = target[prop];
}
return result;
}
var user = {
name: 'wonjang',
gender: 'male',
};
var user2 = copyObject(user);
user2.name = "twojang";
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // twojang twojang
console.log(user === user2); // true
하지만 이 또한 완벽하지 않다.
중첩된 객체, 객체 안에 객체가 할당된 구조에 대해서는 완벽하게 복사하는 것이 불가능하단 점이다.
이런 경우 얕은 복사에서 더 깊게 들어간 '깊은 복사' 를 사용해야 한다.
먼저, 얕은 복사가 왜 완벽하게 동작하지 않을까?
copyObject 함수를 보면, 이 함수의 속성이 target 의 depth 를 하나만 돌면서 복사하기 때문이다.
var user = {
name: 'wonjang',
urls: {
portfolio: 'http://github.com/abc',
blog: 'http://blog.com',
facebook: 'http://facebook.com/abc',
}
};
var user2 = copyObject(user);
user2.name = 'twojang';
// depth 1단계에서는 불변성 유지 - 정상 작동
console.log(user.name === user2.name); // false
// depth 2~ 단계에서는 불변성 유지 X
user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio); // true
// 동일한 이유
user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog); // true
이를 개선한 깊은 복사에서는 내부의 모든 값을 하나하나 다 찾아서 모두 복사하는 과정을 거친다.
var user2 = copyObject(user);
user2.urls = copyObject(user.urls);
이러한 과정을 거쳐줘야 하지만, 이렇게 중첩마다 코드를 하나씩 추가할 수 없기에, '재귀적 수행' 을 사용한다.
자기 자신을 호출하며 반복 실행하는 재귀의 개념을 활용하는 것이다.
var copyObjectDeep = function(target) {
var result = {};
if (typeof target === 'object' && target !== null) {
for (var prop in target) {
result[prop] = copyObjectDeep(target[prop]);
}
} else {
result = target;
}
return result;
}
//결과 확인
var obj = {
a: 1,
b: {
c: null,
d: [1, 2],
}
};
var obj2 = copyObjectDeep(obj);
obj2.a = 3;
obj2.b.c = 4;
obj2.b.d[1] = 3;
console.log(obj);
console.log(obj2);
깊은 복사까지 정상적으로 구현된 코드는 다음과 같고, 불변성을 유지한 채로 진행되게 된다.
두 개념 모두 '없음' 을 나타내는 값이다. 다만 차이가 있다.
개발자가 이 값을 직접 지정하는 경우는 거의 없다. 값이 할당되어야 하는데 그렇지 못한 경우, 자동적으로 부여되는 것이 대부분이다.
만약 직접 사용하려고 한다면, 결론부터 말하자면 쓰지마라. null 을 사용하면 된다. 특히 다른 개발자와 협업을 하는 경우 이 undefined 를 일부러 넣었다면 이게 의도된 동작인지 아닌지 구분하기 어려워지는 이유때문에 그렇다.
이 undefined 가 나타나는 경우는 크게 3가지로 나눌 수 있다.
1. 변수에 값이 지정되지 않은 경우, 데이터 - 메모리 영역 주소를 지정하지 않은 식별자에 접근 할 때
2. ' . ' 이나 ' [] ' 으로 접근하려고 하는데, 해당 데이터가 없을 때
3, Function 인데 return 문이 없을 때
var a;
console.log(a); // (1) 값을 대입하지 않은 변수에 접근
var obj = { a: 1 };
console.log(obj.a); // 1
console.log(obj.b); // (2) 존재하지 않는 property에 접근
// console.log(b); // 오류 발생
var func = function() { };
var c = func(); // (3) 반환 값이 없는 function
console.log(c); // undefined
미리 알아두어야 할 점으로 자바스크립트에는 이 null 에 버그가 하나 있다.
(typeof Null) 을 출력할 경우 object 라고 출력되는데, 이건 자바스크립트의 언어적 버그로 객체는 분명히 아니다.
코드로 살펴보자.
var n = null;
console.log(typeof n); // object <- 이게 버그
//동등연산자(equality operator) : 타입까진 일치하지 않아도 됨
console.log(n == undefined); // true
console.log(n == null); // true
//일치연산자(identity operator) : 타입까지 완벽하게 일치해야 true
console.log(n === undefined);
console.log(n === null);
두번째 동등연산자와 세번째 일치연산자의 경우로, undefined 와 null 은 '없다' 라는 값은 같아도 서로 다른 타입이라는 것을 알 수 있다.
#9. This (0) | 2024.11.08 |
---|---|
#8. 실행 컨텍스트 (3) | 2024.11.07 |
#6. 숙제 - 문자열 임의대로 정렬 (0) | 2024.11.05 |
#5. 일급 객체, Map (4) | 2024.11.05 |
#4. ES6 - JavaScript Version (0) | 2024.11.05 |