프로젝트를 진행하면서 에러창을 여러 번 마주했다. 어떤 에러는 구글과 스택오버플로우를 찾아보며 금새 해결하고 원인을 찾아 같은 에러가 발생하지 않도록 개선했지만, 어떤 에러는 며칠을 투자해 겨우 해결하고도 에러 발생 원인을 찾지 못해 계속 답답한 감정을 가지게 했다.
컴퓨터와 자바스크립트에 대한 이해를 보다 높여서 이런 답답함을 해소하고 에러 발생률을 낮추고자 정재남님의 "코어 자바스크립트" 책을 샀다. 완독은 했으나... 어떤 챕터는 단 한 문단도 이해가 가지 않았다. 그래서 블로깅을 하면서 차근차근 다시 책을 읽어보려 한다.
챕터 목표:
자바스크립트가 데이터를 처리하는 과정을 통해 기본형 타입과 참조형 타입이 다르게 동작하는 이유를 이해하고 활용한다.
기본형(원시형, primitive type):
값이 담긴 주솟값을 바로 복제함. 모두 불변값임.
- number
- string
- boolean
- null
- undefined
- symbol
참조형(reference type):
값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주솟값을 복제함. 가변값인 경우가 대부분이나, 설정에 따라 변경이 불가능하거나 불변값으로 활용할 수 있음.
- object
- array
- function
- Date
- RegExp(정규표현식)
- Map, WeakMap
- Set, WeakSet
bit < byte < memory
각 bit는 고유한 식별자(unique identifier)를 통해 위치를 확인할 수 있다.
각 byte는 시작하는 bit의 식별자로 위치를 확인할 수 있다.
즉, 모든 데이터는 메모리 주솟값(memory address)을 통해 서로 구분하고 연결할 수 있다.
자바스크립트에서 숫자(정수형, 부동소수형 구분 없이 모두)는 8byte(64bit)를 확보한다. 이중 1bit는 +/- 부호, 11bit는 지수부, 52bit는 가수부로 사용한다. 반면, 문자는 정해진 규격 없이 가변적으로 메모리를 사용한다.
변수(variable): 변할 수 있는 데이터. 변경 가능한 데이터가 담길 수 있는 공간.
식별자(identifier): 변수명. 어떤 데이터를 식별하는데 사용하는 이름.
var a; // === "변할 수 있는 데이터를 만든다. 이 데이터의 식별자는 a로 한다."
위와 같이 변수가 선언되고 나면, 컴퓨터는 메모리에서 비어있는 공간 하나를 확보하여 공간의 이름(식별자)을 a로 지정한다. 이후 a에 접근할 때는 컴퓨터가 메모리에서 a가 이름인 주소를 검색하여 같은 공간의 데이터를 반환한다.
var a; // 변수 선언
a = 'abc'; // 데이터 할당
var a = 'abc'; // 변수 선언과 데이터 할당
위와 같이 데이터가 할당되고 나면, 컴퓨터는 메모리에서 비어있는 공간 하나를 확보하여 문자열 'abc'를 저장한다. 그 메모리주소를 a가 이름인 공간의 값에 저장한다.
즉, 변수 선언과 데이터 할당의 메모리 저장은 각각 이루어지고, 변수공간에 식별자와 데이터의 메모리 주소가 저장된다.
이렇게 저장하는 데는 다음의 이점이 있다.
1. 데이터 변환을 자유롭게 할 수 있음.
2. 메모리를 더욱 효율적으로 관리할 수 있음.
3. 중복된 데이터는 메모리에 한 번만 저장하고 재사용할 수 있기 때문에 처리 효율이 높음.
이렇게 저장하지 않는다면 다음과 같이 비효율적인 면이 있다.
1. 확보된 공간을 변환된 데이터 크기에 맞게 늘리는 작업 필요. (처리할 연산이 많아짐.)
1-1. 해당 공간보다 뒤에 저장된 데이터들을 모두 뒤쪽으로 옮김.
1-2. 이동시킨 주소를 각 식별자에 다시 연결함.
그렇기 때문에 기존의 데이터에 문자열을 추가하거나 일부 문자열을 삭제한다면, 모두 새로 만들어 별도의 공간에 저장하고 그 주소를 식별자 메모리에 다시 연결한다.
데이터 영역에 저장된 값은 변경될 수 없으나, 변수 영역에 저장된 값(주솟값)은 변경될 수 있다.
즉, 데이터 영역은 불변값, 변수 영역은 가변값이다.
데이터 영역에 저장되는 모든 값.
변수와 상수는 '변경 가능성'에 의해 구분된다. (변수: 바꿀 수 있음 / 상수: 바꿀 수 없음)
변수와 상수를 구분 짓는 변경 가능성의 대상은 변수 영역(식별자와 데이터의 주소가 담긴 공간) 메모리이다.
불변성 여부를 구분 짓는 변경 가능성의 대상은 데이터 영역(데이터가 담긴 공간) 메모리이다.
불변값의 성질은 '변경은 새로 만드는 동작을 통해서만 이루어진다'는 점이다.
변수 영역에 저장되는 모든 값
var obj1 = { // 변수 영역에 식별자와 데이터의 주솟값 담김. 주솟값은 '객체의 변수 영역' 주솟값임.
a: 1, // 객체의 변수 영역에 식별자와 데이터의 주솟값 담김.
b: 'bbb'
}
var obj1 = { // 중첩 객체
x: 3,
arr: [ 3, 4, 5 ] // 배열의 변수 영역에 식별자(인덱스값)와 데이터의 주솟값 담김.
}
참조형 데이터는 기본형 데이터와 다르게 '데이터(객체, 배열 등) 변수 영역'이 존재한다.
만약, 데이터 영역에 저장된 데이터의 메모리 주소를 참조하는 변수가 하나도 없으면 garbage collector의 수거 대상이 되어 삭제되고, 그 공간은 빈 메모리가 된다.
(garbage collector는 특정 시점이나 메모리 사용량이 포화 상태에 가까울 때 수거 대상을 수거한다.)
var a = 10;
var b = a;
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
변수 a와 b, obj1와 obj2는 각각 같은 주솟값을 값으로 가지게 된다. 즉, 변수를 복사하면 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라본다.
var a = 10;
var b = a;
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
b = 15;
obj2.c = 20;
변수 a와 b는 다른 주솟값을 바라본다. 그러나 obj1와 obj2는 여전히 같은 주솟값을 바라보고 있다. 왜냐하면 '객체의 변수 영역'의 값이 변경된 것이지 obj1와 obj2의 변수 영역 값이 변경된 것이 아니기 때문이다.
이는 다음과 같다.
a !== b
obj1 === obj2
다만, 아래와 같이 복사한 객체 obj2에 새로운 객체를 할당한다면 obj1와 obj2는 다른 주솟값을 바라본다.
var a = 10;
var b = a;
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
b = 15;
obj2 = { c: 20, d: 'ddd' };
정리하자면,
1. 자바스크립트의 모든 데이터 타입은 주솟값을 참조한다. 기본형은 주솟값 복사 과정이 1회이고, 참조형은 주솟값 복사 과정이 n회이다.
2. 참조형 데이터 내부의 프로퍼티를 변경할 때는 참조형 데이터가 가변값이고, 참조형 데이터 자체를 변경할 때는 참조형 데이터가 불변값으로 활용된다.
값으로 전달받은 객체의 프로퍼티를 변경하더라도 원본 객체는 변하지 않아야 하는 경우, 불변 객체가 필요하다.
전달받은 객체의 모든 프로퍼티를 복사(얕은 복사)하여 새로운 객체를 반환하는 함수를 통해 불변 객체를 만들 수 있다.
var copyObject = function (target) {
var result = {};
for (var prop in target) {
result[prop] = target[prop];
}
return result;
}
위 예제의 copyObject는 for in 문법을 이용해 result 객체에 target 객체 프로퍼티를 복사하는 함수이다. (프로토타입 체이닝 상의 모든 프로퍼티를 복사하는 점, getter/setter는 복사하지 않는 점, 얕은 복사만을 수행한다는 점이 아쉽다.)
이 함수를 사용해 객체를 복사하여 새 객체를 만들고, 새 객체의 프로퍼티를 변경하는 방법으로 불변객체를 만들 수 있다.
그러나 객체 내부의 변경이 필요할 때 copyObject 함수를 사용하지 않는 경우에는 불변 객체로 존재하지 않을 수 있기 때문에 immutable.js, baobab.js 등의 라이브러리를 통해 시스템 적으로 제약을 거는 것이 더욱 안전한 방법일 것 같다.
얕은 복사에서는 객체의 한 단계 아래 프로퍼티까지는 복사해서 새로운 데이터를 만든다. 그러나 그보다 한 단계 더 아래인 프로퍼티는 기존 데이터를 그대로 참조한다. 따라서 전달받은 객체의 프로퍼티 밸류의 프로퍼티를 변경하면, 원본 객체도 함께 변경된다.
따라서, 얕은 복사를 반복하는 방법으로 깊은 복사를 할 수 있다. (어딘가 허술하다는 느낌이 계속 든다.)
// 객체의 깊은 복사를 수행하는 재귀 함수
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;
}
혹은 객체를 JSON 문자열 -> JSON 객체로 두 번 전환하는 방법으로 깊은 복사를 처리할 수도 있다.
이 방법은 함수, 메서드, __proto__
, getter/setter 등과 같이 JSON으로 변경할 수 없는 프로퍼티는 모두 무시하기 때문에 순수한 정보만 다룰 때 사용할 수 있다.
var copyObjectViaJSON = function (target) {
return JSON.parse(JSON.stringify(target));
}
둘 모두 '없다'는 의미를 가진 데이터다. 그러나 서로 다른 의미도 함께 가지고 있고, 사용하는 목적도 다르다.
사용자가 명시적으로 지정할 수 있다.
값이 존재하지 않을 때 자바스크립트 엔진이 자동으로 부여한다. 사용자가 어떤 값을 지정할 것이라고 예상되는 상황이나 실제로 값을 지정하지 않았을 때, 자바스크립트 엔진은 undefined를 반환한다.
1) 값을 대입하지 않은 변수, 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때.
2) 객체 내부의 존재하지 않는 프로퍼티에 접근하려고 할 때
3) return 문이 없거나 호출되지 않는 함수의 실행 결과 (return 값이 없으면 undefined를 반환한 것으로 간주한다.)
var arr1 = [];
arr1.length = 3;
console.log(arr1); // [empty × 3]
var arr2 = new Array(3);
console.log(arr2); // [empty × 3]
var arr3 = [undefined, undefined, undefined];
console.log(arr3); // [undefined, undefined, undefined]
비어있는 요소 empty는 순회와 관련한 많은 배열 메서드의 순회 대상에서 제외된다. 따라서 empty에 forEach, map, filter, reduce와 같은 메서드를 적용하면 어떠한 처리도 하지 않고 건너뛰게 된다. 값이 지정되지 않은 인덱스는 '아직은 존재하지 않는 프로퍼티'이기 때문이다.
전자는 '값'의 의미를 가지기 때문에 프로퍼티나 배열의 요소로서 실존하는 데이터이다. 따라서 메서드 순회의 대상이 된다.
후자는 '존재하지 않음'의 의미를 가지기 때문에 프로퍼티 자격이나 배열의 인덱스가 존재하지 않는다. 따라서 메서드 순회 대상이 아니다.
이처럼 같은 undefined라도 서로 다른 의미를 가지기 때문에 혼란을 방지하기 위해서는 명시적으로 undefined를 부여하지 말자. '값이 없음'을 나타내야할 때는 null을 사용하자.
typeof null === object // true. 자바스크립트 자체 버그이다.
console.log( 어떤값 === null ); // 어떤 값이 null인지 알고 싶을 때 이렇게 확인하자.
즉, undefined는 어떤 변수에 값이 존재하지 않을 경우를 의미하고, null은 사용자가 명시적으로 '없음'을 표현하기 위해 대입한 데이터이다.
오.. 은진님 자바스크립트 처음부터 다시 공부하시는군요!