1) 데이터 타입
1. 데이터 타입의 종류
-
javasscript의 데이터 타입은 크게 두 가지로 나뉜다: 기본형, 참조형
- 기본형: number, string, boolean, null, undefined, symbol
- 참조형: object(Map, WeakMap, Set, WeakSet), array, function, date, regexp
-
기본형: 할당/연산 시 복제된다
- 값이 담긴 주솟값을 바로 복제
- 불변성(immutability)를 띈다.
-
참조형: 할단/연산 시 참조된다.
- 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주솟값을 복제
2. 데이터 타입에 관한 배경지식
- 모든 데이터는 바이트 단위의 식별자, 즉 ‘메모리 주솟값’을 통해 서로 구분하고 연결할 수 있다.
- 변수: variable. 변할 수 있는 데이터 (숫자, 문자열, 객체, 배열 등)
- 식별자: 어떤 데이터를 식별하는데 사용하는 이름. (=변수명)
3. 변수 선언과 데이터 할당
변수 선언:
var a // 변할 수 있는 데이터를 만든다. 이 데이터의 식별자는 a로 한다.
- 즉, 변수는 변경 가능한 데이터가 담길 수 있는 공간 또는 그릇
- 컴퓨터가 위와 같이 변수를 선언하면 메모리 영역에서 아래와 같은 작업을 수행한다.
주소 | .. | 1002 | 1003 | 1004 | 1005 | … |
---|---|---|---|---|---|---|
데이터 | 이름: a / 값: |
데이터 할당:
var a // 변수 a 선언
a = 'abc' // 변수 에 데이터 할당
var a = 'abc' // 변수 선언과 할당을 한 문장으로 표현
- 실제로 해당 ‘a’ 변수 데이터에 문자열 ‘abc’를 직접 저장하지는 않는다. 데이터를 저장하기 위한 별도의 메모리 공간을 다시 확보해서 문자열 ‘abc’를 저장하고, 그 주소를 변수 영역에 저장한다.
변수 영역
주소 | .. | 1002 | 1003 | 1004 | 1005 | … |
---|---|---|---|---|---|---|
데이터 | 이름: a / 값: @5004 |
데이터 영역
주소 | .. | 5002 | 5003 | 5004 | 5005 | … |
---|---|---|---|---|---|---|
데이터 | ‘abc’ |
1) 변수 영역에서 빈 공간 (@1003)을 확보한다.
2) 확보한 공간의 식별자를 a로 지정한다.
3) 데이터 영역의 빈 공간(@5004)에 문자열 'abc'를 저장한다.
4) 변수 영역에서 a라는 식별자를 검색한다.(@1003).
5) 앞서 저장한 문자열의 주소(@5004)를 @1003의 공간에 대입한다.
- 변수 영역에 값을 직접 대입하지 않고, 한단계를 더 거치는 이유?
- 데이터 변환을 자유롭게 할 수 있게 함과 동시에, 메모리를 더욱 효율적으로 관리
- js는 숫자형 데이터에 8바이트(64비트)의 공간을 확보하는데, 문자열은 정해진 규격이 없다. 메모리 용량이 가변적이므로 ‘확보된 공간을 변환된 데이터 크기에 맞게 늘리는 작업’이 필요 없음.
- 결국, 효율적으로 문자열 데이터의 변환을 처리하려면 변수와 데이터를 별도의 공간에 나누어 저장해야함
4. 기본형 데이터와 참조형 데이터
불변값
- 변수(variable)과 상수(constant)를 구분하는 성질은 ‘변경 가능성’
- 불변값은 상수가 아님.
- 변수 vs 상수 구분 짓는 변경 가능성의 대상은 변수 영역의 메모리
- 불변성 여부를 구분할 때의 변경 가능성은 데이터 영역의 메모리
- 기본형 데이터(숫자, 문자열, boolean, null, undefined, Symbol) 모두 불변값
불변성 예시:
var a = 'abc' // 변수 a에 'abc' 데이터 주소 할당
a = a + 'def' // 기본 'abc'에 'def' 추가가 아니라, 새로운 문자열 'abcdef'를 만들어 a에 주소 저장
// 즉, 'abc'와 'abcdef'는 완전 별개의 데이터
var b = 5 // 변수 b에 숫자 5 할당. 데이터 영역에서 5 찾고, 없으면 데이터 공간 만들어 저장
var c = 5 // b에 할당한 숫자 5의 주솟값 재활용
b = 7 // 기존 5값을 변경하지 않고, 데이터 영역에서 7을 찾거나 생성해서 주솟값 변경
- 이처럼 문자열 값은 한번 만들면 변경할 수 없다.
- 변경은 오로지 새로 만드는 동작을 통해서만 이뤄진다.
- 한번 만들어진 값은 가비지 컬렉팅을 하지 않은 한 영원히 변하지 않는다.
가변값
- 참조형 데이터의 기본적인 성질은 가변값이 경우가 많다.
- 설정에 따라 변경 불가하게 활용할 수도 있음
참조형 데이터 할당:
var obj1 = {
a: 1,
b: 'bbb',
}
변수 영역
주소 | .. | 1002 | 1003 | 1004 | 1005 | … |
---|---|---|---|---|---|---|
데이터 | 이름: obj1, 값: @5001 |
데이터 영역
주소 | .. | 5001 | 5002 | 5003 | 5004 | … |
---|---|---|---|---|---|---|
데이터 | @7103 ~ ? | 1 | ‘bbb’ |
객체 @5001의 변수 영역
주소 | .. | 7103 | 7104 | 7105 | 7106 | … |
---|---|---|---|---|---|---|
데이터 | 이름: a, 값: @5003 | 이름: b, 값: @5004 |
1) 컴퓨터는 변수 영역의 빈 공간(@1002)를 확보하고, 그 주소 이름을 obj1으로 지정
2) 임의의 데이터 저장공간 (@5001)에 저장하려고 보니 여러개의 프로퍼티로 이뤄진 데이터 그룹임.
이 그룹 내부의 프로퍼티를 저장하기 위해 별도의 변수 영역 마련 후, 그 영역의 주소(@7103 ~?)를 @5001에 저장
3) @7103 및 @7104에 각각 a와b라는 프로퍼티 이름 지정
4) 데이터 영역에서 숫자 1 검색.
검색 결과 없으므로 임의로 @5003에 저장하고 이 주소를 @7103에 저장.
'bbb'역시 마찬가지로 작업
- 기본형 데이터와의 차이는 객체의 변수(프로퍼티) 영역이 별도로 존재한다는 점이다.
- 객체가 별도로 할애한 영역은 ‘변수 영역’일 뿐 ‘데이터 영역’은 기존의 메모리 공간 그대로 활용. 즉, 데이터 영역에 저장된 값은 불변값
- 하디만 변수에는 언제든지 다른 값 대입할 수 있다. 이 점이 가변성.
- 따라서 참조형 데이터는 불변(immutable)하지 않다고 한다.
가비지 컬렉터:
- 어떤 데이터에 대해 자신의 주소를 참조하는 변수의 갯수를 ‘참조 카운트’ 라고 한다. (ex. 주소 @5003을 참조하는 주소는 @7103 하나 이므로 참조 카운트는 1)
- 이때, @7103의 데이터 값을 @5004로 변경하면 (예: obj1 = {a: “bbb”, b: “bbb”}) 해당 데이터(@5003)의 참조 카운트는 0이 된다.
- 참조 카운트가 0인 메모리 주소는 가비지 컬렉터의 수거 대상이 된다.
- 가비지 컬렉터는 런타임 환경에 따라 특정 시점이나 메모리 사용량 포화 상태 시 자동으로 수거한다.
변수 복사 비교
var a = 10
var b = a
var obj1 = { c: 10, d: 'ddd' }
var obj2 = obj1
변수 영역
주소 | .. | 1001 | 1002 | 1003 | 1004 | … |
---|---|---|---|---|---|---|
데이터 | 이름: obj1, 값: @5001 | 이름:b, 값: @5001 | 이름: obj1, 값: @5002 | 이름: obj2, 값: @5002 |
데이터 영역
주소 | .. | 5001 | 5002 | 5003 | 5004 | … |
---|---|---|---|---|---|---|
데이터 | 10 | @7103 ~ ? | ‘ddd’ |
객체 @5002의 변수 영역
주소 | .. | 7103 | 7104 | 7105 | 7106 | … |
---|---|---|---|---|---|---|
데이터 | 이름: c, 값: @5001 | 이름: d, 값: @5003 |
- 변수를 단순히 선언, 할당, 복사하는 과정은 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보는 점에서 동일하다.
- 복사 과정은 동일하지만, 데이터 할당 과정과 복사 이후의 동작에서 차이가 발생함.
(1) 변수 복사 이후 값 변경 결과 비교 - 객체의 프로퍼티 변경 시
var a = 10
var b = a
var obj1 = { c: 10, d: 'ddd' }
var obj2 = obj1
b = 15
objc2.c = 20
위 처럼 복사된 객체의 프로퍼티를 변경하면 아래와 같은 흥미로운 결과가 나온다:
a !== b
obj1 === obj2
변수 a와 b는 서로 다른 주소를 바라보게 되었으나, 변수 obj1과 obj2는 여전히 같은 객체를 바라보고 있다.
왜 그럴까?
- 기본형 데이터를 복사한 변수 b의 값을 바꾸면, b 변수의 데이터 값(주소)만 달라진다.
- 반면, 참조형 데이터 복사 변수 obj2의 프로퍼티 값을 바꾸면, obj1, obj2의 값(주소)는 동일하면서, 해당 값(주소)에 있던 데이터가 변경된다.
- 즉, 변수 a와 b는 서로 다른 주소를 바라보게 되었으나, 변수 obj1과 obj2는 같은 객체를 바라본다.
메모리를 자세히 들여다 보면, 아래와 같이 변경이 되어있다.
변수 영역
주소 | .. | 1001 | 1002 | 1003 | 1004 | … |
---|---|---|---|---|---|---|
데이터 | 이름: obj1, 값: @5001 | 이름:b, 값: @5004 | 이름: obj1, 값: @5002 | 이름: obj2, 값: @5002 |
데이터 영역
주소 | .. | 5001 | 5002 | 5003 | 5004 | 5005 | … |
---|---|---|---|---|---|---|---|
데이터 | 10 | @7103 ~ ? | ‘ddd’ | 15 | 20 |
객체 @5002의 변수 영역
주소 | .. | 7103 | 7104 | 7105 | 7106 | … |
---|---|---|---|---|---|---|
데이터 | 이름: c, 값: @5005 | 이름: d, 값: @5003 |
-
“기본형은 값을 복사하고 참조형은 주솟값을 복사한다” 그럼 이 말은 어떤 뜻일까?
- 엄밀히 따지면 js의 모든 데이터 타입은 참조형이다
- 다만, 기본형은 주솟값을 한번만 복사하고, 참조형은 한단계 더 거치므로 차이가 생긴다
-
단, 아래 예시처럼 참조형 데이터의 속성이 아닌 참조형 데이터 객체 자체를 변경하면 값이 달라진다.
var obj1 = { c: 10, d: 'ddd' }
var obj2 = obj1
obj2 = { c: 20, d: 'ddd' }
- 즉, 참조형 데이터가 ‘가변값’일 때는 참조형 데이터 자체를 변경할 경우가 아니라, 내부의 프로퍼티를 변경할 때만 성립한다.
5. 불변 객체
불변 객체를 만드는 간단한 방법
- 불변 객체 (immutable object)는 최근 React, Vue.js, Angular 등의 라이브러리 프레임워크 뿐만 아니라, 함수형 프로그래밍, 디자인 패턴 등에서도 매우 중요함
- 객체도 데이터 차제를 변경하고자 하면 (새로운 데이터 할당) 기본형과 마찬가지로 기존 데이터는 변하지 않는다 (불변성)
- 내부 속성 변경 시 가변성
- 따라서, 내부 속성 변경시마다 매번 새로운 객체를 만들어 재할당을 하거나, 자동으로 새로운 객체를 만드는 도구를 활용하면 객체도 불변성을 확보할 수 있음
- ex) immutable.js, immmer js, immutability-helper 등의 라이브러리. ES6의 spread operator, Object.assign 메서드
왜 불변 객체가 필요할까?
- 값으로 전달받은 객체에 변경을 주더라도, 원본 객체는 변하지 않아야 하는 경우가 있음.
var user = {
name: 'Kim',
gender: 'female',
}
var changeName = function (user, newName) {
var newUser = user
newUser.name = newName
return newUser
}
var user2 = changeName(user, 'Jung')
console.log(user.name, user2.name) // Jung Jung
위 예시처럼 user2 호출 시, 내부 프로퍼티만 변경함으로써 기존 user 객체를 동시에 변경하였다. (가변성)
user와 user2가 다른 값을 가질 수 있도록 하려면 아래와 같이 새로운 객체를 반환하도록 수정하면 된다.
var user = {
name: 'Kim',
gender: 'female',
}
var changeName = function (user, newName) {
return {
name: newName,
gender: user.gender,
}
}
var user2 = changeName(user, 'Jung')
console.log(user.name, user2.name) // Kim Jung
하지만, 변경된 코드의 경우 변경할 필요가 없는 기존 객체의 ‘gender’속성도 하드코딩으로 입력했다. 객체에 정보가 많을수록 비효율적이게 되는데, 프로퍼티 갯수에 상관없이 모든 프로퍼티를 복사하는 함수를 만들 수 있다.
// 얕은 복사
var copyObject = function (target) {
var result = {}
for (var prop in target) {
result[prop] = target[prop]
}
}
- 물론 협업하는 모든 개발자들이 copyObject라는 얕은 복사를 사용하면 문제가 되지 않는다. (=user 객체가 곧 불변 객체)
- 하지만, 그 규칙을 지키지 않을 수도 있으므로 아예 프로퍼티를 변경할 수 없게 제약을 거는게 더 안전하다.
얕은 복사와 깊은 복사
-
얕은 복사(shallow copy): 바로 아래 단계의 값만 복사.
- ex) 중첩된 객체에서 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주솟값만 복사
- 즉, 원본과 사본이 동일한 참조형 데이터 주소를 가리키므로 사본을 바꾸면 원본도 변경됨
-
깊은 복사(deep copy): 내부의 모든 값들을 하나하나 찾아서 전부 복사.
- ex) 중첩된 객체에서 프로퍼티 내부의 값들까지 전부 복사. 재귀로 구현 가능
// 깊은 복사
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
}
- target이 객체인 경우에는 내부 프로퍼티를 순회하며 copyObjectDeep 함수를 재귀적으로 호출하고, 객체가 아닌경우에는 target을 그대로 지정한다.
- 이 함수는 원본과 사본이 서로 완전히 다른 객체를 참조하게 하여 어느쪽의 프로퍼티를 변경하더라도 다른 쪽에 영향을 주지 않는다.
6. undefined와 null
undefined
와null
은 자바스크립트에서 모두 ‘없음’을 나타낸다. 하지만 미세하게 차이점이 있다.
1. undefined
- 자바스크립트 엔딘은 사용자가 어떤 값을 지정할 것이라고 예상되는 상황임에도 실제로 그렇게 하지 않았을 때 undefined를 반환한다.
-
- 값을 대입하지 않은 변수. 즉, 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때
-
- 객체 내부의 존재하지 않는 프로퍼티에 접근하려고 할 때
-
- return 문이 없거나 호출되지 않는 함수의 실행 결과
-
2. null
- null은 ‘비어있음’을 명시적으로 나타내고 싶을 때 사용한다.
- typeof null은 object라고 반환되는데, 이는 자바스크립트 자체 버그이다. 따라서 변수의 값이 null인지 확인하려면 일치 연산자로 확인을 해줘야 한다.
undefined와 null을 구분하기 위해서는 동등 연산자 대신, 일치 연산자(===)를 사용해야 한다.
var n = null
console.log(typeof n) // object
console.log(n == undefined) // true
console.log(n == null) // true
console.log(n === undefined) // false
console.log(n === null) // true
참고
- 정재남, 『코어 자바스크립트』, 위키북스(2019), p1-35.