JavaScript에서 원시 값(primitive, 또는 원시 자료형)이란 객체가 아니면서 메서드도 가지지 않는 데이터입니다. 원시 값에는 7종류, string, number (en-US), bigint (en-US), boolean, undefined, symbol, 그리고 null이 존재합니다.
MDN 공식문서
원시타입은 말 그대로 굉장히 원시적인 형태의 자료이다. 말 그대로 아주 단순한데, 식별자의 이름에 메모리 주소와 값이 할당된다.
원시타입은 콜 스택에만 저장된다는 말이 있다. 콜 스택은 순차적으로 코드를 실행하며 쌓이는 실행 컨텍스트(Execution Context)를 저장하는 곳이다. 즉 콜 스택은 코드를 실행순서를 정하고 해당 실행 컨텍스트 내의 변수를 저장하는 공간이라고 생각하면 편하다.
실행 컨텍스트는 또 생성/실행으로 나뉘어 지는데, 생성(Creation) 과정에서 호이스팅이 일어난다.
이 실행 컨텍스트는 글로벌 실행 컨텍스트와 함수 실행 컨텍스트로 이루어져 있다. 사실상 글로벌 실행 컨텍스트도 모든 스크립트를 실행하게 만드는 글로벌 스코프 함수에 의해 실행되므로, 글로벌 실행 컨텍스트도 함수 실행 컨텍스트의 일종이라고 볼 수 있겠다.
예를 들어 다음 코드의 작동 과정을 봐보자.
var a = "abc";
let b = 123;
const c = false
지금은 글로벌 스코프 함수에 의해 실행되어 나타난 글로벌 실행 컨텍스트에 다른 함수 실행 컨텍스트가 없이 변수 3개만이 있다. 처음 글로벌 실행 컨텍스트에서 Creation Phrase가 일어나 변수들을 호이스팅하게 된다. 이후 콜스택에 담은 실행 컨텍스트들을 하나씩 처리하면서 Execution Phrase가 일어나게 되면서 변수에 값이 할당되게 된다. 변수의 선언, 초기화, 할당의 구조는 여기서 확인하기를 바란다.
하튼 원시타입에서 이렇게 구구절절히 메모리 구동 방식까지 보여준 이유는 결국 당신이 배우게 될 내용이라서? 그러하다 ㅋㅋㅋㅋ
결국 본론으로 돌아와서, 이 Global Lexcial Environment의 환경레코드에 지역변수들이 저장되게 된다.
이후 이 환경레코드 안에 있는 변수들은 다음과 같이 메모리에 저장되게 되는데, 원시타입은 다음과 같이 콜 스택 내에서만 저장되는 특징이 있다.
또한 원시타입은 '불변성'을 가지는데, 즉 주소는 바뀔지언정 안의 '값'은 바뀔 수 없다는 것이다.
예를 들어 변수를 재할당한다고 생각해보자.
var a = "abc";
let b = 123;
const c = false
a = "def"
다음 코드는
다음과 같이 "def"라는 값을 새로운 주소에 할당한 후, 해당 주소를 가르키게 된다. 그러므로 본래 있던 주소의 값은 변경되는 것이 아니기에 불변하다는 이야기이다.
그렇다면 "abc"라는 값은 어떻게 되는거냐고? 이렇게 더이상 참조되지 않는 값들은 자바스크립트의 가비지 컬렉터에 의해 적절한 시점에 자동적으로 제거되게 된다.
여기서 b의 값을 a로 바꾼다면,
b = a;
다음과 같이 a의 주소를 참조하게 된다. 하지만 여기서 a의 값을 c로 바꾼다면?
a = c;
d가 참조하는 주소로 바뀌면서 a의 값은 false가 된다. 그렇다면 b는 a가 가르키는 주소를 따라가는 걸까? 아니다. b는 a가 바뀌기 이전의 주소가 본인의 주소가 되었으므로, a가 추후에 바뀌더라도 함께 바뀌지 않는다.
이렇듯, 원시타입의 데이터 타입은 불변성과 함께 콜 스택 상에서만 데이터가 저장되게 된다.
사실 mdn 레퍼런스에는 reference type이라고 되어 있지 않고 'Object', 객체라고 되어 있다. 즉 객체를 담는 데이터 타입을 원시타입과 대비되게 만들기 위해 개발자들이 붙이는 얘기인 듯 하다. 즉 위에서 열거한 원시타입 이외의 모든 타입을 참조타입이라고 생각하면 된다.
이 참조타입은 불변성을 가지지 않는다. 왜냐하면 사실상 이런 객체는 수시로(동적으로) 데이터가 변하기 때문이다. 배열만 생각해봐도 push, pop 할 때마다 콜스택에 각기 다른 데이터가 생기면 매우 비효율적일 것이다. 그러므로 이런 참조타입의 데이터 형태들은 '힙(Heap) 메모리'라는 자바스크립트 엔진의 특성을 가져온다.
위에서 할당한 변수들을 다음과 같이 재할당 해보겠다.
var a = ["abc","def","ghi"];
let b = { "name" : "솔방울", "기분" : "피곤함"};
const c = new Set([1,2,3,4,5]);
다음과 같이 참조타입은 콜 스택에 곧바로 값을 저장하는 것이 아니라, '힙 메모리'라는 영역을 추가로 만든다. 그러므로 콜 스택에서 각 변수가 참조하는 변수의 값은 힙 메모리의 주소값이고, 힙 메모리의 주소에 해당 값이 할당되는 것이다.
그렇다면 여기서 a의 값을 완전히 다른 객체로 바꿔보겠다.
var a = ["abc","def","ghi"];
let b = { "name" : "솔방울", "기분" : "피곤함"};
const c = new Set([1,2,3,4,5]);
a = ["jkl", "mno", "pqr"];
해당 과정은 원시타입이랑 비슷하게, 새로운 값이 재할당되는 것이므로 힙 메모리에 새로운 값에 대한 힙 메모리 주소가 생기고, 이를 콜 스택의 주소값으로 갖게 된다. 그러면 불변성이 생기는 거 아니냐고? 해당 예를 봐보자.
var a = ["abc","def","ghi"];
let b = { "name" : "솔방울", "기분" : "피곤함"};
const c = {1,2,3,4,5};
b["공부"] = "하기싫음";
b 객체에 새로운 데이터를 넣었음에도, 힙 메모리 영역에 새로운 주소가 할당되는 것이 아니라, 같은 주소에 값만 바뀌었다. 즉, 참조타입의 경우 데이터를 추가, 변경, 삭제하는 것은 힙 메모리 영역의 데이터를 변경하는 것이지, 해당 이벤트가 발생될 때마다 새로운 데이터에 대한 힙 메모리를 생성하는 것이 아니다. 즉, 불변성이 깨진다.
그러한 맥락에서 다음 코드도 정상적으로 작동한다.
var a = ["abc","def","ghi"];
let b = { "name" : "솔방울", "기분" : "피곤함"};
const c = {1,2,3,4,5};
c.add(6);
const는 값이 바뀌지 않는데 왜 되냐구요? 정확히 말하자면
let은 메모리 주소값 변경을 허용하고, const는 허용하지 않습니다.
이는 즉슨, 메모리 주소값 자체가 바뀌는 '재할당'은 문제가 생기지만, 참조하는 힙 메모리의 값이 바뀌는 것은 상관이 없다는 것이다.
현 게시물에는 원시타입, 참조타입만을 정리한 것이 아니라 이 얘기에 필요한 다양한 개념에 대해서도 설명을 진행했다. (콜 스택 내 실행 컨텍스트 등...) 해당 개념에 대해서는 나 또한 다시 한 번 정리해서 올려보도록 하겠다.
참조 게시물들
https://velog.io/@code-bebop/JS-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B5%AC%EC%A1%B0
https://meetup.toast.com/posts/129
https://jeonghwan-kim.github.io/2017/10/22/js-context-binding.html
https://curryyou.tistory.com/276
나중에 쓸 주제 : https://www.atatus.com/blog/javascript-execution-context/