본문 바로가기
Javascript&Typescript

Call Stack과 Memory Heap으로 알아보는 불변성을 지켜야 하는 이유

by Robinkim93 2023. 2. 2.

자바스크립트 엔진은 자바스크립트를 실행할 때 원시타입 및 참조타입을 저장할 때 Call Stack과 Heap 이라는 두 가지 메모리에 저장한다.

 

# Call Stack (콜 스택)

Call Stack에는 원시타입 값과 함수 호출을 하는 실행 컨텍스트를 저장한다.

 

#Memory Heap(메모리 힙)

Memory Heap에는 객체, 배열, 함수와 같이 크기가 동적으로 변할 수 있는 참조타입 값을 저장한다.

 

이렇게만 알면 복잡해보이기 때문에 동작원리를 알아보기 위해 예제문과 그 순서를 작성했다.

 

let a = 10;
let b = 35;
let arr = [];
function func() {
	const c = a + b;
    const obj = { "d" : c };
    return obj;
}
let o = func();

# 전역 실행 컨텍스트(GEC) 생성 후 원시타입 값은 Call Stack에 참조타입 값은 Memory Heap에 저장된다.

 

원시타입으로 선언된 a와 b는 값에 접근할 때 할당된 메모리 주소로 바로 접근이 가능하다.

 

하지만 참조타입인 배열과 함수는 내부의 값이 Memory Heap에 저장이 되고, 저장된 값의 메모리 주소를 해당 배열과 함수를 실행한 선언문의 값으로 가진다.또한, 함수의 결과값을 담은 o는 아직 함수가 진행되지 않았기 때문에 메모리만 할당된 상태로 값을 가지지 않는다.

 

# func 함수가 실행되면 함수 안에서 함수 실행 컨텍스트(FEC)가 실행되고 함수 내부에서도 원시타입은 Call Stack에 참조타입은 Memory Heap에 저장된다.

 

함수 내부에서 c = a + b로 선언되었고 연산하였을 때 원시타입(number)이 되기 때문에 마찬가지로 Call Stack에 저장된다. 

 

함수 내부의 객체도 마찬가지로 객체를 선언한 선언문은 Call Stack에 저장되는 것은 같지만, 그 내부의 값은 Memory Heap에 저장하고, 그 저장된 값의 메모리를 값으로써 참조한다.

 

# 함수 내부의 진행이 종료되면 함수 실행 컨텍스트(FEC)는 사라지고 return한 값을 참조한다.

 

func 함수는 결과값으로 00CT78401의 메모리주소를 가진 객체(참조타입)를 return 했다.

func 함수의 결과값을 담아서 선언한 변수 o는 자연스럽게 참조타입의 값을 가지게 되고, 마찬가지로 메모리주소가 할당되고 그 내부의 값은 Memory Heap이 가지고 있고, 그 값에 대한 메모리주소를 값으로써 참조하게 된다.

 

# 코드 종료 후 전역 실행 컨텍스트(GEC)도 사라진다.

 

전역 실행 컨텍스트(GEC)가 사라짐에 따라서 Call Stack에 있는 값들이 사라지기 때문에 자연스럽게 Memory Heap이 가지고 있던 내용은 자바스크립트의 가비지 컬렉터에 의해서 제거된다.

 

더보기
Garbage Collector(가비지 컬렉터) : 자바스크립트 엔진 내에서 메모리 관리를 수행한다.

 

# 불변성을 지켜야 하는 것과의 연관성

let v = 1;
let v = 2;

let arr = [1, 2, 3];
let arr = [4, 5, 6];

이런 식의 코드를 진행할 때, 위의 Call Stack과 Memory Heap으로 대입해보면, v는 특정 메모리주소를 할당받고 1이라는 원시타입을 값으로 가진다. 후행에서 v를 다시 2로 재선언했을 때는 1이 2로 대체된 것이 아니라 이미 선언된 두 번째 v라는 선언문의 메모리주소와 값으로 대체된 것이다. 바꿔말하면 원시타입의 값이 변경될 때는 메모리주소와 값이 통째로 바뀌기 때문에 서로 값이 꼬일 일이 없다. 이것은 곧 불변성을 가지고 있다고 할 수 있다.

 

하지만 하단에서 배열이라는 참조타입을 선언하고, 하단에 다시 같은 방식으로 재선언을 하게 되면 그 내부의 값이 변경된 것을 알 수 없다. 왜냐면 값은 Memory Heap이 가지고 있고, 값이 변경되더라도 Call Stack에서는 값에 대한 주소만을 참조하고 있기 때문에 Heap에서 값이 변경되어도 알 수 있는 방법이 없다.

 

그렇기 때문에 우리는 참조타입은 불변성이 유지되지 않는다는 것을 인지하고, 따로 신경써서 불변성을 관리해줘야 한다.

 

# 불변성을 지킬 수 있는 방법

const array = [1, 2, 3, 4];
const sameArray = array;
sameArray.push(5);

console.log(array === sameArray); // true

const array = [1, 2, 3, 4];
const differentArray = [...array, 5];

console.log(array === differentArray) // false

위의 코드는 1,2,3,4라는 값을 가진 배열에 5라는 값을 추가하는 두 가지 방법이다.

 

첫 번째는 배열 자체를 다른 변수에 그대로 담아 선언했고, push 메서드를 통해 5를 추가했다. 이 때, 원본배열과 새로 선언한 배열이 동일해진다. 그것은 곧 원본배열의 불변성을 지키지 않았다고 볼 수 있다. (다른 코드에서 원본의 값을 변하게 만들었기 때문)

 

두 번째는 배열 내부의 값을 확장 연산자를 통해 새로운 배열에 담고, 그 뒤에 5를 추가했다. 이 때, 원본배열과 새로운 배열을 비교했을 때는 같지 않다고 나오는데, 원본배열은 그대로 둔 상태에서 새로운 배열에 원본배열의 값만을 넣어 재선언했기 때문이다.

 

추가적으로 같은 맥락에서 새로운 참조타입을 반환하는 메소드를 사용하여 원본 데이터의 불변성을 보장받을 수 있는데, 새로운 참조타입을 반환하는 메소드에는 map, filter, slice, reduce 등이 있다.