들어가며
많은 기업들이 타입스크립트와 nest.js를 활용해서 서버 개발을 하곤 합니다. 취업을 하려면, 타입스크립트와 nest.js를 공부해서 실무를 익히는 것이 중요할 것입니다. 하지만 아직 자바스크립트의 기초도 없는 상태에서 타입스크립트와 nest.js를 공부하는 것이 맞을까 하는 생각이 들었습니다. 빠르게 기술변화를 적응하고, 러닝 커브를 줄이기 위해 빠르게 공부해야 하는 것도 맞겠지만, 그전에 언어의 기반이 되는 자바스크립트부터 제대로 알아야 하지 않을까 하는 생각이 들었습니다. 이 기회에 자바스크립트의 기본에 대해 정리해보고자 합니다.
불변값
기본형 데이터의 숫자, 문자열, boolean, null, undefined, symbol은 모두 불변값입니다. 그중 숫자와 문자열을 예로 들어 불변성의 개념을 알아보겠습니다.
var a = 'abc';
a = a + 'def';
var b = 5;
var c = 5;
b = 7;
위의 코드를 바탕으로 메모리가 어떻게 작동하는지 함께 살펴보겠습니다.
우선 위 예제의 1~2번째 줄을 보면, 변수 a에 문자열 'abc'를 할당하고, 뒤에 'def'를 추가했습니다. 그러면, 기존의 'abc'가 'abcdef'로 바뀌는 것이 아니라 메모리의 데이터 영역에서 새로운 문자열 'abcdef'를 만들어 영역의 주소를 변수 a에 저장합니다.
그리고 4번째 줄에는 변수 b에 숫자 5를 할당합니다. 그러면 컴퓨터는 일단 데이터 영역에서 5를 찾고, 없으면 데이터 영역에 공간을 하나 만들어 저장합니다. 그 후 데이터 영역의 주소를 b에 저장합니다.
5번째 줄에서는 변수 c에 b와 같은 5를 할당하려고 합니다. 값을 할당하기 위해 컴퓨터는 데이터 영역에서 5를 찾습니다. 4번째 줄에서 이미 만들어놓은 값이 있으니 그 주소 값을 변수 c에도 할당해줍니다.
그 후, 6번째 줄에서는 변수 b의 값을 7로 바꾸고자 합니다. 그러면 기존에 데이터 영역에 저장된 5를 7로 바꾸는 것이 아니라 기존에 저장했던 7을 찾아서 있으면 재활용하고, 없으면 새로 만들어서 위치를 b에 저장합니다.
이처럼 문자열과 숫자 값은 한 번 만든 값을 바꿀 수 없고, 만약 변수 영역에 저장된 값을 변경하고 싶다면, 데이터 영역에서 새로 만드는 곳을 연결해서 이뤄집니다. 이것이 바로 불변값의 성질입니다. 한 번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않습니다.
가변값
기본형 데이터는 모두 불변값입니다. 그렇다면 참조형 데이터는 어떤 값일까요? 설정에 따라 변경 불가능한 경우도 있고, 아예 불변값으로 활용하는 방안도 있습니다. 우선 참조형 데이터를 변수에 할당하는 과정을 살펴보고, 가변값에 대해 알아보겠습니다.
var obj1 = {
a: 1,
b: 'bbb'
};
변수 영역
주소 | 1001 | 1002 | 1003 | 1004 | ... | ... |
데이터 | 이름 : 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' 역시 임의로 @5004에 저장하고, 이 주소를 @7104에 저장합니다.
참조형 데이터과 기본형 데이터와의 차이는 '객체의 변수(프로퍼티) 영역'이 별도로 존재한다는 점입니다. 위의 표를 자세히 보면 객체가 별도로 할애한 영역은 변수 영역일 뿐, '데이터 영역'은 기존의 메모리 공간을 그대로 활용하고 있습니다. 데이터 영역에 저장된 값은 모두 불변값입니다. 그러나 변수에는 다른 값을 얼마든지 대입할 수 있습니다. 바로 이 부분 때문에 흔히 참조형 데이터는 불변하지 않다(가변값이다)라고 하는 것입니다. 예제를 확인해 봅시다.
var obj1 = {
a: 1,
b: 'bbb'
};
obj1.a = 2;
5번째 줄에서 obj1의 a 프로퍼티에 숫자 2를 할당하려고 합니다. 데이터 영역에서 숫자 2를 검색합니다. 검색 결과가 없으므로 빈 공간인 @5005에 저장하고, 이 주소를 @7103에 저장합니다. 4번째 줄의 명령 전과 후에 변수 obj1이 바라보고 있는 주소는 @5001로 변하지 않았습니다. 즉 '새로운 객체'가 만들어진 것이 아니라 기존의 객체 내부의 값만 바뀐 것입니다.
변수 영역
주소 | 1001 | 1002 | 1003 | 1004 | ... | ... |
데이터 | 이름 : obj1 값 : @5001 |
데이터 영역
주소 | 5001 | 5002 | 5003 | 5004 | 5005 | ... |
데이터 | @7103 ~ ? | 1 | 'bbb' | 2 |
객체 @5001의 변수 영역
주소 | 7103 | 7104 | 7105 | 7106 | ... | ... |
데이터 | 이름 : a 값 : @5005 |
이름 : b 값 : @5004 |
중첩 객체
이번에는 참조형 데이터의 프로퍼티에 다시 참조형 데이터를 할당하는 경우를 살펴보겠습니다. 참고로 이런 경우를 일컬어 중첩 객체라고 합니다. 중첩 객체는 어떻게 메모리에 값들이 할당되는지 살펴보겠습니다.
var obj = {
x: 3,
arr: [3, 4, 5]
};
1. 컴퓨터는 우선 변수 영역의 빈 공간(@1002)을 확보하고, 그 주소의 이름을 obj로 지정합니다.
2. 임의의 데이터 저장공간(@5001)에 데이터를 저장하려는데, 이 데이터는 여러 개의 변수와 값들을 모아놓은 그룹(객체)입니다. 이 그룹의 각 변수(프로퍼티)들을 저장하기 위해 별도의 변수 영역을 마련하고(@7103~?), 그 영역의 주소를 @5001에 저장합니다.
3. @7103에 이름 x를 @7104에 이름 arr를 지정합니다.
4. 데이터 영역에서 숫자 3을 검색합니다. 없으므로 임의로 @5002에 저장하고, 이 주소를 @7103에 저장합니다.
5. @7104에 저장할 값은 배열로서 역시 데이터 그룹입니다. 이 그룹 내부의 프로퍼티들을 저장하기 위해 별도의 변수 영역을 마련하고(@7104~?), 그 영역의 주소를 @7104에 저장합니다.
6. 배열의 요소가 총 3개이므로 3개의 변수 공간을 확보하고 각각 인덱스를 부여합니다(0, 1, 2).
7. 데이터 영역에서 숫자 3을 검색해서(@5002) 그 주소를 @8104에 저장합니다.
8. 데이터 영역에 숫자 4가 없으므로 @5004에 저장하고, 이 주소를 @8105에 저장합니다.
9. 데이터 영역에 숫자 5가 없으므로 @5005에 저장하고, 이 주소를 @8106에 저장합니다.
변수 영역
주소 | 1001 | 1002 | 1003 | 1004 | ... | ... |
데이터 | 이름 : obj 값 : @5001 |
데이터 영역
주소 | 5001 | 5002 | 5003 | 5004 | 5005 | ... |
데이터 | @7103 ~ ? | 3 | @8104 ~ ? | 4 | 5 |
객체 @5001의 변수 영역
주소 | 7103 | 7104 | 1003 |
데이터 | 이름 : x 값 : @5002 |
이름 : arr 값 : @5003 |
배열 @5003의 변수 영역
주소 | 8104 | 8105 | 8106 |
데이터 | 이름 : 0 값 : @5002 |
이름 : 1 값 : @5004 |
이름: 2 값 : @5005 |
이제 obj.arr[1]을 검색하고자 하면 메모리에서는 다음과 같은 검색 과정을 거칩니다.
1. obj 검색 1 : obj라는 식별자를 가진 주소를 찾습니다(@1002).
2. obj 검색 2 : 값이 주소이므로 그 주소로 이동합니다(@5001).
3. obj 검색 3 : 값이 주소이므로 그 주소로 이동합니다(@7103 ~ ?).
4. obj.arr 검색 1 : arr이라는 식별자를 가진 주소를 찾습니다. (@7104).
5. obj.arr 검색 2 : 값이 주소이므로 그 주소로 이동합니다. (@5003).
6. obj.arr 검색 3 : 값이 주소이므로 그 주소로 이동합니다 (@8104 ~ ?)
7. obj.arr[1] 검색 1 : 인덱스 1에 해당하는 주소를 찾습니다 (@8105).
8. obj.arr[1] 검색 2 : 값이 주소이므로 그 주소로 이동합니다 (@5004).
9. obj.arr[1] 검색 3 : 값이 숫자형 데이터이므로 4를 반환합니다.
@1002 -> @5001 -> (@7103 ~ ?) -> @7104 -> @5003 -> (@8104 ~ ?) -> @8105 -> @5004 -> 4 반환
중첩 객체 재할당
만약 이 상태에서 다음과 같이 재할당 명령을 내리면 어떻게 될까요?
var obj = {
x: 3,
arr: [3, 4, 5]
};
obj.arr = 'str';
변수 영역
주소 | 1001 | 1002 | 1003 | 1004 | ... | ... |
데이터 | 이름 : obj 값 : @5001 |
데이터 영역
주소 | 5001 | 5002 | 5003 | 5004 | 5005 | 5006 |
데이터 | @7103 ~ ? | 3 | 4 | 5 | 'str' |
객체 @5001의 변수 영역
주소 | 7103 | 7104 | 1003 |
데이터 | 이름 : x 값 : @5002 |
이름 : arr 값 : @5006 |
배열 @5003의 변수 영역
주소 | |||
데이터 |
@5006에 문자열 'str'을 저장하고, 그 주소를 @7104에 저장합니다. 그러면 @5003은 더 이상 자신의 주소를 참조하는 변수가 하나도 없게 됩니다. 어떤 데이터에 대해 자신의 주소를 참조하는 변수의 개수를 참조 카운트라고 합니다.
@5003의 참조 카운트는 @7104에 @5003이 저장돼 있던 시점까지는 1이었다가 @7104에 @5006이 저장되는 순간 0이 됩니다. 참조 카운트가 0인 메모리 주소는 가비지 컬렉터의 수거 대상이 됩니다. 가비지 컬렉터는 런타임 환경에 따라 특정 시점이나 메모리 사용량이 포화 상태에 임박할 때마다 자동으로 수거 대상들을 수거합니다. 수거된 메모리는 다시 새로운 값을 할당할 수 있는 빈 공간이 됩니다.
즉, @5003은 참조 카운트가 0이 됨에 따라 GC 대상이 되고, 이후 언젠가 담겨 있던 데이터인 "@8104 ~?"라는 값이 사라집니다. 이 과정에서 연쇄적으로 @8104 ~? 의 각 데이터들의 참조 카운트가 0이 되고, 이들 역시 GC의 대상이 되어 함께 사라질 것입니다.
변수 복사 비교
지금까지는 메모리에서 어떻게 동작하는지에 대해 살펴봤다면, 본격적으로 기본형과 참조형 데이터의 차이를 살펴보겠습니다. 먼저 변수를 복사할 때 어떤 변화가 일어나는지 살펴보겠습니다.
var a = 10;
var b = a;
var obj1 = {c: 10, d: 'ddd'};
var obj2 = obj1;
변수 영역
주소 | 1001 | 1002 | 1003 | 1004 | ... | ... |
데이터 | 이름 : a 값 : @5001 |
이름 : b 값 : @5001 |
이름 : obj1 값 : @5002 |
이름 : obj2 값 : @5002 |
데이터 영역
주소 | 5001 | 5002 | 5003 | 5004 | ||
데이터 | 10 | @7103 ~ ? | 'ddd' |
객체 @5002의 변수 영역
주소 | 7103 | 7104 | |
데이터 | 이름 : c 값 : @5001 |
이름 : d 값 : @5003 |
우선 기본형 데이터 먼저 살펴본다면, 1번째 줄에서 변수 영역의 빈 공간 @1001을 확보하고 식별자를 a로 지정합니다. 숫자 10을 데이터 영역에서 검색하고 없으므로 데이터 영역의 빈 공간 @5001에 저장한 다음, 이 주소를 @1001에 넣습니다. 이를 통해 기본형 데이터의 변수 선언 및 할당이 종료됩니다.
이제 복사를 할 차례입니다. 변수 영역의 빈 공간 @1002을 확보하고 식별자를 b로 지정합니다. 이제 식별자 a를 검색해 그 값을 찾아와야 합니다. @1001에 저장된 값인 @5001을 들고 좀 전에 확보해둔 @1002에 값으로 대입합니다.
다음은 참조형 데이터를 살펴보겠습니다. 3번째 줄에서는 변수 영역의 빈 공간 @1003를 확보해 식별자를 obj1로 지정합니다. 데이터 영역의 빈 공간 @5002을 확보하고, 데이터 그룹이 담겨야 하기 때문에 별도의 변수 영역 @7103~을 확보해 그 주소를 저장합니다. @7103에는 식별자 c를, @7104에는 식별자 d를 입력한 다음, c에 대입할 값 10을 데이터 영역에서 검색합니다. @5001에 이미 저장돼 있으므로 이 주소를 @7103에 연결하고, 문자열인 'ddd'는 데이터 영역의 빈 공간에 새로 만들어서 @7104에 연결합니다. 여기까지가 참조형 데이터인 객체에 대한 변수 선언 및 할당 과정입니다.
그리고 4번째 줄에서는 변수 영역의 빈 공간 @1004를 확보하고 식별자를 obj2로 지정합니다. 이제 식별자 obj1을 검색해(@1003) 그 값인 @5002를 들고, @1004에 값으로 대입합니다.
변수를 복사하는 과정은 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보게 되는 점에서 동일합니다. @1001과 @1002는 모두 값이 @5001이 됐고, @1003과 @1004에는 모두 값이 @5002가 됐습니다. 복사 과정은 동일하지만 데이터 할당 과정에서 이미 차이가 있기 때문에 변수 복사 이후의 동작에도 큰 차이가 발생합니다.
var a = 10;
var b = a;
var obj1 = {c: 10, d: 'ddd'};
var obj2 = obj1;
b = 15;
obj2.c = 20;
이번에는 먼저 그림 없이 코드를 바탕으로 흐름을 따라가 봅시다. 1번째 불부터 4번째 줄까지는 위에서 살펴본 것과 같습니다.
6번째 줄에서는 데이터 영역에 아직 15가 없으므로 새로운 공간 @5004에 저장하고, 그 주소를 든 채로 변수 영역에서 식별자가 b인 주소를 찾습니다. @1002의 값이 @5004가 됩니다.
7번째 줄에서는 데이터 영역에 아직 20이 없으므로 새로운 공간 @5005에 저장하고, 그 주소를 든 채로 변수 영역에서 obj2를 찾고(@1004), obj2의 값인 @5002가 가리키는 변수 영역에서 다시 c를 찾아(@7103) 그곳에 @5005를 대입합니다.
변수 영역
주소 | 1001 | 1002 | 1003 | 1004 | ... | ... |
데이터 | 이름 : a 값 : @5001 |
이름 : b 값 : @5004 |
이름 : obj1 값 : @5002 |
이름 : obj2 값 : @5002 |
데이터 영역
주소 | 5001 | 5002 | 5003 | 5004 | 5005 | |
데이터 | 10 | @7103 ~ ? | 'ddd' | 15 | 20 |
객체 @5002의 변수 영역
주소 | 7103 | 7104 | |
데이터 | 이름 : c 값 : @5005 |
이름 : d 값 : @5003 |
기본형 데이터를 복사한 변수 b의 값을 바꿨더니 @1002의 값이 달라진 반면, 참조형 데이터를 복사한 변수 obj2의 프로퍼티의 값을 바꿨더니 @1004의 값은 달라지지 않았습니다. 즉, 변수 a와 b는 서로 다른 주소를 바라보게 됐으나 변수 obj1과 obj2는 여전히 같은 객체를 바라보고 있는 상태입니다. 이를 코드로 표현하면 다음과 같습니다.
a !== b
obj1 === obj2
이 결과가 바로 기본형과 참조형 데이터의 가장 큰 차이점입니다. 대부분의 자바스크립트 책에서 '기본형은 값을 복사하고, 참조형은 주솟값을 복사한다'고 설명하고 있지만, 사실은 어떤 데이터 타입이든 변수에 할당하기 위해서는 주솟값을 복사해야 하기 때문에, 엄밀히 따지면 자바스크립트의 모든 데이터 타입은 참조형 데이터일 수밖에 없습니다. 다만 기본형은 주솟값을 복사하는 과정이 한 번만 이뤄지고, 참조형은 한 단계를 더 거치게 된다는 차이가 있습니다.
또 여기서 한 가지 더 짚고 넘어갈 내용이 있습니다. 변수의 값(b)을 직접 변경할 때와 값이 아닌 내부 프로퍼티(obj2.c)를 변경할 때의 결과를 비교한 것입니다. 가만 보면 애초에 비교 대상의 조건 자체가 서로 다르니 동작도 다른 게 당연할 수밖에 없습니다. 같은 조건인 상태에서 비교하면 어떨지도 확인해봅시다.
var a = 10;
var b = a;
var obj1 = {c: 10, d: 'ddd'};
var obj2 = obj1;
b = 15;
obj2 = {c: 20, d: 'ddd'};
이번에는 b의 경우와 마찬가지로 obj2에도 새로운 객체를 할당함으로써 값을 직접 변경했습니다. 그러면 메모리의 데이터 영역의 새로운 공간에 새 객체가 저장되고 그 주소를 변수 영역의 obj2 위치에 저장합니다. 객체에 대한 변경임에도 값이 달라졌습니다!
변수 영역
주소 | 1001 | 1002 | 1003 | 1004 | ... | ... |
데이터 | 이름 : a 값 : @5001 |
이름 : b 값 : @5004 |
이름 : obj1 값 : @5002 |
이름 : obj2 값 : @5006 |
데이터 영역
주소 | 5001 | 5002 | 5003 | 5004 | 5005 | 5006 |
데이터 | 10 | @7103 ~ ? | 'ddd' | 15 | 20 | @8204 ~ ? |
객체 @5002의 변수 영역
주소 | 7103 | 7104 | |
데이터 | 이름 : c 값 : @5001 |
이름 : d 값 : @5003 |
객체 @5006의 변수 영역
주소 | 8204 | 8205 | |
데이터 | 이름 : c 값 : @5005 |
이름 : d 값 : @5003 |
즉, 참조형 데이터가 '가변값'이라고 설명할 때의 '가변'은 참조형 데이터 자체를 변경할 경우가 아니라 그 내부의 프로퍼티를 변경할 때만 성립합니다.
정리해보면, 변수는 변경 가능한 데이터가 담길 수 있는 공간이며, 식별자는 그 변수의 이름을 말합니다. 변수를 선언하면 컴퓨터는 메모리의 빈 공간에 식별자를 저장하고, 공간의 값에 기본형 데이터를 할당하려도 하면, 데이터 영역에 데이터를 저장하고, 그 공간의 주소를 값 영역에 할당합니다.
참조형 데이터를 할당한다면, 컴퓨터는 참조형 데이터 내부 프로퍼티들을 위한 변수 영역을 별도로 확보합니다. 그리고 확보된 주소에 각 프로퍼티의 식별자를 저장합니다. 또한 데이터 영역에서 가져온 주소값을 식별자들과 매칭합니다.
마치며
자바스크립트의 기본에 대해 공부하면서, 기본도 정확하게 알지 못하고, 앞으로 나아가려 했구나 하는 생각이 들었습니다. 기초이기에 지금 공부하는 내용을 더 잘 이해해야겠다고 생각했습니다. 기본에 충실할 수 있는 개발자가 되고 싶습니다.
출처
'JavaScript > 코어 자바스크립트' 카테고리의 다른 글
[자바스크립트] 실행 컨텍스트란? (feat 코어 자바스크립트) (1) | 2021.09.24 |
---|---|
[자바스크립트] undefined와 null (feat 코어 자바스크립트) (0) | 2021.09.16 |
[자바스크립트] 불변 객체 (feat 코어 자바스크립트) (0) | 2021.09.13 |
[자바스크립트] 변수 선언과 데이터 할당 (feat 코어 자바스크립트) (1) | 2021.09.01 |
[자바스크립트] 데이터 타입 종류와 배경지식 (feat 코어 자바스크립트) (0) | 2021.09.01 |