📖 이 글은 코어자바스크립트를 읽고 책을 바탕으로 이해한 내용을 작성한 글입니다.
세 건의 프로젝트를 통해 자바스크립트를 이용한 웹 프론트엔드 개발 경험을 쌓으며 지금 당장 자바스크립트에 대해 무엇을 공부해야할지 알 수 있었습니다. 하지만 프로젝트 마감 일정 등이 빡빡한 탓에.. 이론적인 부분은 나중으로 미루며 우선 프로젝트를 잘 마무리하는 데에 집중했었어요.
이제는 세 건의 프로젝트가 모두 종료된 만큼 (리팩토링이 진행 중이긴 하지만..) To Do 리스트로만 쌓여져 있던 ‘공부할 자바스크립트 이론들’을 하나씩 공부하고 있습니다. 그 일환으로 코어자바스크립트 책을 다시 읽으면서 이해한 것을 정리해보려고 합니다. 1장, 데이터 타입을 읽고 해소할 수 있었던 의문점들은 다음과 같습니다.
- undefined가 아니라 null을 이용해야 하는 이유
- 리액트 등 spa 프레임워크에서 state의 불변성을 유지해야하는 이유
- 얕은 복사와 깊은 복사
- 참조형 데이터란 무엇인가
자바스크립트의 데이터 타입
자바스크립트를 처음 공부할 때 자바스크립트에는 두 가지의 데이터 타입이 있다고 배웠었습니다. 첫 번째가 기본형(원시형, Primitive type)이고 두 번째가 참조형(Reference type)이예요.
- 기본형 : number, string, null, undefined, boolean, symbol
- 참조형 : object, array, function, date, regexp, map, set 등
그때는 그냥 그런가보다~
하고 넘어갔었지만, 이제는 저게 뭔지 좀 더 깊이 알아야 궁금했던 부분들을 해결할 수 있었습니다. 코어자바스크립트에 의하면 엄밀하게 자바스크립트의 모든 데이터는 참조형이라고 할 수 있는데, 이게 무슨 말인지는 뒤에서 더 이야기 해보겠습니다.
기본형과 참조형을 나누는 기준으로 흔히 두 가지를 이야기해요. 첫째가 ’불변성‘이고 둘째가 ‘실제값’들로 연결시켜주는 ‘주소값’들이 모여있는 주소를 복제하는지, ‘실제값’이 담긴 주소를 바로 복제하는지입니다.
이게 무슨 말인지 이해하려면 먼저 메모리와 데이터에 대해 간단하게나마 이해해야 하고, 식별자와 변수를 구분할 수 있어야 합니다. 하나씩 되짚어보겠습니다.
선행 개념 1. 메모리와 데이터
컴퓨터의 메모리는 매우 많은 비트들로 구성되어 있어요. 비트란 0 또는 1로만 표현이 가능한 메모리 조각입니다. 그리고 고유한 식별자를 통해 비트의 위치를 찾아갈 수 있습니다. 그렇지만 이건 매우 비효율적인 일입니다. 생각해보면, 비트는 0 또는 1로만, 즉 두 가지의 경우로만 표현이 가능하다 했습니다. 이렇게 한정적인 표현으로 수 많은 데이터의 위치를 표현하기엔 너무 빡쎄단 생각이 들죠.
모든 기술은 이전 기술의 불편함 또는 문제를 해결하기 위해 등장한다 했습니다. 이런 비트의 문제를 해결하기 위해서 비트 여러개를 묶어서 하나의 단위로 여기기 시작했어요. 그러면서 표현할 수 있는 값이 증가하고, 검색 시간이 감소했어요. 하지만 장점만 있지는 않았습니다. 낭비되는 비트가 생기게 된 것입니다.
따라서, 바이트가 등장했습니다.
표현 가능한 개수에 어느 정도 제약을 걸더라도, 비트의 낭비를 최소화하기로 한 거예요. 그 적정선이 바로 8개의 비트를 묶어서 256개의 값을 표현할 수 있는 바이트였습니다. 바이트 역시 비트의 식별자로 위치를 파악합니다. 이 비트의 식별자란, 메모리 주소값을 의미합니다. 결국 모든 데이터는 바이트 단위의 식별자로 서로 구분하고 연결할 수 있게 됩니다.
그런데, 애초에 저런 고민은 메모리가 넉넉하지 않았으니까 했을거예요. 하지만 자바스크립트가 등장하던 시점에는 이전에 비해 메모리가 많이 넉넉해졌습니다. 따라서 자바스크립트는 상대적으로 메모리 관리 압박에서 자유로워져서, 넉넉한 메모리를 할당하게 됩니다.
선행 개념 2. 식별자와 변수
모든 데이터는 바이트 단위의 식별자로 서로 구분하고 연결한다고 했습니다. 식별자란 메모리 주소값을 의미한다고 했고요. 그러면 식별자와 변수는 같은 걸까요 다른 걸까요?
식별자와 변수는 쉽게 혼용되지만 실제로 다른 개념입니다. 변수가 변할 수 있는 수, 변할 수 있는 데이터를 의미한다면 식별자는 어떤 데이터를 식별하는 데 사용되는 이름입니다. 즉, 변수의 이름 (변수명)인거죠. 변수가 데이터를 저장하는 곳이라면, 식별자(변수명)는 데이터를 저장하는 곳의 이름입니다. 데이터를 저장한 곳을 찾아갈 수 있게 해주는 이름이요.
- 변수 : 데이터를 저장하는 곳. 변할 수 있는 데이터를 의미
- 식별자 : 데이터를 저장한 곳의 이름. 데이터를 저장한 곳을 찾아갈 수 있게 (=데이터를 식별할 수 있게) 함.
둘이 다르다는 것은 이제 알겠습니다. 그러면 실제로 어떻게 쓰이고 있을까요 ? 변수의 선언 과정을 살펴보면 알 수 있습니다.
변수의 선언 과정
var a = 3
이라는 식은 선언과 할당이 동시에 이루어지는 식입니다. 이 식을 입력하면 자바스크립트는 메모리에서 임의의 주소값에 공간을 찜합니다. 그리고 공간에 변수의 이름과 데이터를 저장합니다. 다음과 같은 구조를 띈다고 할 수 있어요. 데이터 부분은 사실 저게 아니지만, 지금은 간단하게 표현하려고 저렇게 썼다고 봐주세요.
이후 메모리에서 a이름을 가진 주소값을 검색합니다. 여기서 식별자가 변수를 찾을 수 있는 이름이라고 했던 게 무슨 말인지 알 수 있어요. 그리고 해당 주소값의 공간에 담긴 데이터를 반환합니다.
변수의 할당 과정
그럼 이번에는 변수의 할당 과정만 살펴보겠습니다. 위에서 선언했던 var a = 3
을, a = 5
라고 재할당합니다. 그러면 먼저 a라는 이름을 가진 주소값을 찾을 거라는 건 이제 알고 있습니다. 그리고, 그 공간에 데이터 5를 저장할 거라고 생각할 수 있지만 실제로 그렇게 직접 저장하지 않습니다.
직접 저장하는 것이 아니라 별도의 메모리 공간을 다시 확보하고, 새 공간에 데이터를 저장한 후, 그 새 공간의 주소를 변수 영역의 데이터에 저장하게 됩니다.
이렇게 굳이 한 단계를 더 거치는 이유는 뭘까요? 그냥 원래 있던 변수 영역의 데이터에 덮어씌우기 하면 편할 것 같은데요. 그 이유는 두 가지입니다.
- 데이터 변환을 자유롭게 하기 위해서
- 메모리 관리를 더 효율적으로 하기 위해서 (중복 데이터에 대한 처리 효율 증가)
이게 무슨 말일까요?
string 값을 할당했다고 해보겠습니다. 처음 문자열의 길이는 짧았고 자바스크립트는 그 크기에 맞춰 메모리의 영역을 찜해놨어요. 그리고 그 이후에 다른 데이터들이 줄줄이 뒤에 저장되었죠. 그런데 이제와서 string 값의 길이가 늘어나게 된거예요. 이러면 공간을 더 늘려야 합니다. 늘려야 하는데 이미 뒤에가 꽉 찼어요. 뒤에 있는 데이터들을 늘어난 길이만큼 뒤로 밀거나 해야겠죠. 이 과정에서 연산이 많이 발생하게 됩니다.
이런 문제를 해결하기 위해서, 효율적으로 메모리를 관리하기 위해서 변수와 데이터를 분리하여 별도의 공간에 저장하는 것입니다. 얼마나 효율적일지 알아보기 위해 책의 예시를 인용하겠습니다.
변수와 데이터가 분리되어있지 않는다면 500개의 공간에 숫자를 다 넣어줘야됩니다. 숫자 5가 8바이트라 할때, 4000바이트의 메모리 소모가 발생합니다.
반면 변수와 데이터가 분리되어 있다면 500개의 공간을 찜하는 것은 똑같지만 그 공간들에 숫자를 일일이 다 넣어주지 않아도 됩니다. 숫자 5가 저장된 데이터 영역의 주소값만 넣어줍니다. 이미 존재하는 5를 재활용하는거죠. 변수와 데이터가 분리되어 있기에 가능한 일입니다.
그래서 주소값이 2바이트라고 하면, 8바이트의 숫자 5를 한 번 저장하는 것까지 해서 총 1008바이트의 메모리 소모가 발생합니다. 차이가 크다는 것을 바로 알 수 있습니다.
결론
변수에 데이터를 재할당할 때에는 원래 자리에 값을 덮어씌우는 것이 아니라 무조건 새로 만들어서 별도의 공간에 저장한다 !
불변값
이제 우리는 변수와 데이터를 각각의 영역으로 분리하여 관리하고, 변수 영역에는 데이터 영역의 주소값이 변수명(식별자)과 함께 저장된다는 사실을 알았습니다.
그럼 이제 궁금한 게 하나 생겨요. 한 번 데이터 할당이 이루어진 변수 영역에, 다른 데이터를 재할당 할 수 있을까요? 그러니까, 1003번 주소값에 a라는 이름과 5005번 데이터가 할당되어 있을 때 5005번을 5008번으로 바꿀 수 있을까요?
이게 가능한 것을 변수라고 하고, 불가능한 것을 상수라고 합니다. 즉, 변수와 상수를 구분하는 것은 변수 영역의 메모리입니다. 변수 영역에 다른 데이터를 재할당할 수 있냐는거죠.
변수와 상수의 뜻을 알고나니 상수가 불변값과 같은 말로 보이기도 하는 것 같지만, 둘은 다릅니다. 불변값의 변경 가능성은 상수와 달리 데이터 영역을 기준으로 하기 때문입니다.
예시를 보겠습니다.
var a = 'abc';
a = a + 'def';
이런 식이 있을 때, 1003번 주소의 영역에 이름은 a, 데이터는 5001번 주소값이 할당됩니다. 그리고 5001번 주소의 영역에는 ‘abc’가 저장됩니다. 이 ‘abc’를 변경할 수 있냐 없냐가 바로 불변값이냐 아니냐를 판단하는 기준이 됩니다. 상수는 5001번을 변경할 수 있냐 없냐를 기준으로 하는 거였죠.
우리는 위에서, 변수에 데이터를 재할당 할 때에는 무조건 새로 만들어서 별도의 공간에 저장한다고 했습니다. 따라서 5001번의 데이터 영역은 바뀌지 않고 그대로고, 5002번 데이터 영역이 새로 생겨 ‘abcdef’가 저장될 것임을 알 수 있습니다. 실제로 구조를 간단히 그려보면 다음과 같습니다.
결론적으로 5001번이 바뀌지 않았죠? 5002번과 완전히 다른 데이터입니다. 이를 불변값이라고 합니다.
즉, 불변값의 특징은 다음과 같습니다.
- 변경은 새로 만드는 동작을 통해서만 이루어진다
- 한번 만들어진 것은 가비지 컬렉팅을 제외하면 영원히 변하지 않는다.
이러니까 또 하나 궁금한 게 생깁니다. ‘그럼 만들어졌는데 아무데서도 안 쓰이는 경우는 낭비아닌가?’ 이 궁금증은 가비지 컬렉팅이 해결해줍니다. 참조하는 곳이 없다면 (= 참조카운트가 0이라면) 가비지 컬렉팅이 수거해갑니다. 이렇게 낭비되는 메모리가 없도록 관리를 해요.
어쨌든 다시 돌아와서, 이제 불변값이 뭔지 알았습니다. 그럼 떠오르는 게 하나 있죠. 맨 처음에 우리는 기본형과 참조형의 차이 중 하나가 ‘불변성’이라고 했었습니다. 기본형은 불변값이지만 참조형은 불변값이 아니고 가변값이라고요. 참조형은 뭐 어떻게 돌아가길래 불변값이 아닌 걸까요?
참조형 데이터의 할당 과정
참조형 데이터의 할당 과정도 기본형 데이터와 크게 다르지 않습니다. 딱 하나 결정적인 차이는, 객체의 프로퍼티가 저장되는 영역이 별도로 존재한다는 겁니다.
객체의 프로퍼티에 각각의 값이 할당된 것이므로, 프로퍼티를 변수로 취급하여 변수영역에 저장하고 그 값은 데이터 영역에서 관리합니다. 따라서 프로퍼티에 할당된 값도 예외없이 이미 데이터 영역에 존재하는지를 확인함으로써 효율적으로 메모리를 관리하죠.
그리고 프로퍼티가 일종의 변수'
영역으로 관리되기 때문에 역시 주소값이 저장되는 데이터가 변할 수 있는, 가변값임을 알 수 있습니다. 이게 참조형 데이터는 불변하지 않다고 이야기하는 이유입니다.
즉, 다음과 같은 식은 이렇게 구조를 그려 이해해볼 수 있습니다. obj.a = 2
라고 하면 5007 대신 5009 주소가 연결됩니다.
var obj = {
a: 1,
b: "abc",
};
var obj.a = 2;
그런데 참조형은 언제나 가변값일까요? 정답은 ‘아니오’입니다. 참조형 데이터가 가변값이라고 하는 것은 참조형 데이터 자체에 새 것을 할당하는 게 아니라, 그 내부 프로퍼티를 변경하려 할 때 해당하는 이야기입니다. 다음의 예시를 통해서 확인할 수 있습니다.
var obj1 = {
a: 10,
b: 'ddd',
};
var obj2 = obj1;
obj2 = {
a: 20,
b: 'ddd',
};
이렇게 객체 자체를 재할당한다면 원본 객체는 그대로 유지되고, 메모리에는 새 객체가 저장됩니다. 이 부분을 이용해 참조형 데이터를 불변 객체로 만들 수 있습니다.
SPA 프레임워크를 이용할 때 state 객체의 불변성을 유지하는 것이 중요하다라는 이야기를 눈 따갑게 보셨을 텐데요, 그 이유는 효율적인 렌더링을 위해 이전 state와 이후 state를 비교해야 하는데, 불변성을 지키지 않으면 이전 state를 추적할 수 없기 때문입니다. 이전 객체까지 같이 변해버리니까요.
이처럼 불변객체는 값으로 전달받은 객체에 변경을 가하더라도 원본 객체는 변하지 않아야 할 때 사용됩니다.
얕은 복사와 깊은 복사
이제 불변값이 무슨 의미인지 알고, 참조형과 기본형의 차이도 명확히 알게 되었습니다. 그럼 더 나아가서, 우리가 그동안 눈 따갑게 보았었던 참조형을 복사할 때의 주의점의 의미를 다시 생각해 볼 수 있습니다. 기본형은 그냥 복사할 수 있지만, 참조형은 복사할 때 불변성을 유지하지 않으면 수정시 원래의 값까지 같이 변한다는 그 주의점이요.
변수를 복사할 때, 기본형과 참조형 모두 같은 주소를 바라보게 된다는 점에서는 동일합니다. 그래서 엄밀히 말하자면 자바스크립트의 모든 데이터 타입은 참조형 데이터일 수밖에 없다고 코어자바스크립트의 저자가 이야기하는 것이구요.
하지만 두 타입은 이미 할당 과정에서 큰 차이가 있었습니다. 기본형은 주소값을 복사하는 과정이 한 번만 일어나지만 참조형은 한 단계를 더 거치죠. 그러므로, 복사 이후의 동작에서도 큰 차이가 발생하게 됩니다.
var obj1 = {
a: '1',
b: 'abc',
};
var obj2 = obj1;
obj2.a = '2';
console.log(obj1.a); // 2
이런 불상사가 일어나는 것을 막기 위해 참조형 데이터는 이렇게 복사해야 한다! 하면서 얕은 복사와 깊은 복사가 소개됩니다.
- 얕은 복사 : 바로 아래 단계 값만 복사. 참조형 데이터 프로퍼티 복사시 그 주소값만 복사하는 것.
- 깊은 복사 : 내부의 모든 단계를 하나하나 찾아서 전부 복사. 불변 객체를 만드는 과정을 재귀적으로 수행해 내부 프로퍼티들도 전부 불변 객체로 만든다.
undefined와 null
마지막으로, 너무나도 비슷해보이는 두 데이터 타입의 차이에 대해 알아봅니다. vue 2.x
를 이용해 프로젝트를 할 때, 상위 컴포넌트에서 전달받은 prop의 데이터 타입을 확인하는 식을 작성할 일이 있었습니다. 그런데, 애초에 값이 없는 채로 넘어오는 prop의 경우 그 타입을 undefined
로 해야할지 null
로 해야할지 고민이 됐었어요. 왜냐면 둘이 비슷한 의미니까요. 결론적으로는 null
을 이용하는 것이 권장되었습니다.
그 이유는 undefined
에는 두 가지 종류가 있기 때문이예요.
- 자바스크립트 엔진이 자동으로 지정한
undefined
- 사용자가 의도적으로 입력한
undefined
같은 undefined
같지만 둘은 실제로 다르고, 구분됩니다. 장난하냐 싶지만 실제로 달라요. 1번은 진짜로 값 없음 ;;;
이고, 2번은 없긴 한데 실존하는 데이터임ㅎ
이예요. 둘의 차이는 객체를 순회할 때 아주 명확해집니다.
객체의 순회에서, 1번의 경우에는 프로퍼티 또는 배열의 인덱스 자체가 존재하지 않음을 의미하는 것이기 때문에 에러가 발생하지만 2번의 경우는 어쨌건 할당은 된 것이라 프로퍼티 또는 배열의 인덱스가 존재하므로, 객체의 순회가 가능합니다.
이처럼 둘은 혼란을 야기하기 때문에, null
이 등장해서 2번을 대체합니다. 이러면 undefined
는 1의 의미로만 쓰이기 때문에 혼란을 막을 수 있습니다. ‘값을 대입하지 않은 변수에 접근할 때 자바스크립트 엔진이 알아서 반환해주는 값’ 으로만 존재할 수 있는 거죠. 왜 undefined가 아니라 null을 써야 했는지, 이제 이해가 됩니다.