본문 바로가기
js 이론

자바스크립트 변수

by 김홍중 2021. 9. 4.

값의 재할당 과정

var temp
temp = 100
temp = 200

위와 같은 코드의 재할당 과정은 어떻게 될까요?

변수 선언시 이전에 남아 있는 값이 그대로 있는 주소값을 가지고 그 주소값에 100의 값을 할당할까요? 재할당시에도 그 주소값에 200이 할당할까요?

이런식으로 생각하셨다면 C언어를 공부하셨을것 같습니다. 아니면 그 외의 저수준 언어일것 같습니다. 

 

하지만 자바스크립트는 다른 재할당 과정을 거칩니다.

변수 선언단계에서 변수 이름을 등록합니다. 이는 실행컨텍스트에 등록됩니다. 자바스크립트 엔진은 실행 컨텍스트를 이용하여 식별자와 스코프를 관리합니다. 그렇다면 변수 이름과 값은 어떤 형식으로 등록될까요? 키,값 형식인 객체로 등록되어 관리됩니다.

아직 값을 할당하지 않았기에 메모리 공간이 비어있는것이 아닙니다. 확보된 메모리 공간은 자바스크립트 엔진이 undefined라는 값을 암묵적으로 할당하여 초기화합니다.

값의 할당에서는 undefined가 있는 메모리가 아니라 새로운 메모리 공간을 확보하고 값을 할당합니다.

재할당 단계에서도 또다른 메모리 공간을 확보하여 재할당합니다.

 

"그렇다면 자바스크립트 엔진에 의해 undefined라는 값이 초기화 되기전에는 아예 비어있을까?" 라는 의문이 들었습니다. 그래서 메모리 생명주기를 정리해보았습니다.

 

메모리 생명 주기

1. allocate(할당) : 프로그램이 메모리를 이용할 수 있도록 운영체제가 부여

2. use(사용) : 코드에서 변수를 사용하여 읽기 및 쓰기 작업

3. release(해제) : 프로그램에서 더 이상 필요하지 않은 메모리를 운영체제에게 돌려줌.

 

javascript에서는 1단계에서 자바스크립트 엔진, 3단계에서 가비지컬렉션이 처리하기 때문에 개발자가 신경쓰지 않아도 됩니다. 고수준 언어인 javascript와 반대로 저수준 언어(ex. C언어)는 개발자가 명시적으로 처리해야합니다.

 

대부분 더 이상 필요하지 않은 메모리는 가비지컬렉터가 회수한다고 하는데 이것이 운영체제에게 돌려주는 것을 의미한다고 생각합니다. 그렇다면 돌려주고 메모리는 어떤 상태인지에 대한 의문은 풀리지 않았습니다. 그래서 제가 C언어를 공부할때 메모리에 남아있는 이전값을 참조하지 않도록 null로 초기화 하던것을 떠올려 자바스크립트의 null타입을 살펴보았습니다.

null타입은 변수에 값은 변수가 이전에 참조하던 값을 더 이상 참조하지 않겠다는 것을 명시하는 것입니다. 그리고 자바스크립트 엔진은 사용하지않는 메모리 공간에 대해 자바스크립트 엔진 내의 가비지콜렉터가 회수할 것입니다. 이것도 가비지콜렉터의 회수 직전의 단계여서 회수 이후에는 비어있거나 다른 상태일것으로 예상합니다.

 

그래서 가비지 컬렉션에 대하여 찾아보니 남아있는 상태의 포인트가 아닌 도달가능한 값이냐가 포인트인것 같습니다.

 

어떤것이 도달 가능한 값일까요?

- 현재 함수의 지역 변수와 매개변수

- 중첩 함수의 체인에 있는 함수에서 사용하는 변수, 매개변수

- 전역 변수

- 기타

이러한 값을 루트라고 부르고 루트가 참조하는 값은 도달 가능한 값이라고 합니다.

 

메모리 누수(Memory Leak)

가비지컬렉션이 메모리를 관리하겠지만 그럼에도 메모리 누수가 나는 경우는 어떤 경우일까요?

 

- 전역 변수를 많이 선언한 경우

선언되지 않은 변수를 참조하면 이는 자동적으로 전역 변수가 됩니다. 엄격모드를 사용하면 전역변수의 사용을 예방할 수 있습니다. 명시적으로 사용된 전역변수는 회수되지 않습니다.

 

엄격모드 예시)

다음과 같이 오류가 발생합니다.

 

- 타이머 혹은 이벤트리스너를 클린업 하지 않은 경우

setInterval의 경우 클린업 하지 않으면 사용중인것으로 간주하여 메모리 공간에 남아있습니다. 이벤트 리스너의 경우 removeEventListener를 해주지 않으면 DOM요소의 수명보다 오래 남게 됩니다.

 

- 클로저를 사용한 경우

외부 함수의 실행이 끝나고도 사라지지 않고 상위 렉시컬 환경을 기억하는 클로저는 이 기억을 위해 메모리가 필요합니다. 이 메모리는 참조를 제거하지 않는 한 가비지컬렉터에 의해 회수되지 않습니다.

 

- DOM을 벗어난 참조를 한 경우 

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};

function doStuff() {
    image.src = 'http://test.url/image';
    button.click();
}

function removeButton() {
    document.body.removeChild(document.getElementById('button'));
    
}

removeButton함수내의 하단 공간에서 여전히 button을 전역 요소로 참조할 수 있습니다.

 

이러한 4가지 경우가 가비지 컬렉터에 의해 제거 되지 않기 때문에 명시적으로 null을 할당하여 가비지 걸렉션이 회수 할 수 있도록 하는것이 대안이 될것이라고 생각합니다.

그림으로보면 더 쉽게 이해할 수 있는데 빨간색 영역이 global에서 아예 도달할 수 없는 영역입니다. 가비지 컬렉터는 모든 객체를 모니터링하고 도달할 수 없는 객체를 삭제합니다.

 

hoisting

호이스팅에 대한 오해

변수나 함수의 호출코드가 선언코드 아래에 있더라도 선언코드가 끌어 올려지는것으로 오해할 수 있습니다. 하지만 이는 실행 컨텍스트의 개념을 생각해보면 자연스럽게 해결됩니다.

 

변수 호이스팅

console.log('맛 : ', flavor);
var flavor = "딸기";
console.log('맛 : ', flavor);

출력

맛 : undefined
맛 : 딸기

코드 실행단계에서 코드가 로드되면 전역 컨텍스트가 생성되는데 이때에 변수는 undefined됩니다. 자바스크립트 엔진에 의해 암묵적으로 할당되어 초기화 되는것입니다.

코드에서 변수 식별자가 나오면 해당 값을 할당하게 됩니다. 이때에 undefined와 다른 새로운 메모리에 할당합니다.

var flavor 선언 및 할당하는 코드가 위로 끌어올려지는것이 아니라 실행컨텍스트의 흐름대로 값이 할당되는 것입니다.

 

함수 호이스팅

함수 호이스팅은 함수 선언문 형태와 함수 표현식 형태가 다르게 동작합니다.

 

함수 선언문 형태

printFlavor("딸기");
 
function printFlavor(flavor) {
  console.log('맛 : ', flavor);
}

출력

맛 : 딸기

함수 선언문의 경우 함수 선언, 초기화, 사용자의 값 할당이 모두 변수 객체가 만들어지는 과정에서 완료됩니다.

그래서 오류 없이 출력됩니다.

 

함수 표현식 형태

printAge(20);
 
var printPrice = (price) => {
  console.log('가격 : ', price);
}

출력

(Uncaught ReferenceError: is not defined error)

하지만 함수 표현식인 경우는 익명함수를 생성한 뒤에 이 익명함수를 변수에 할당합니다. 

실행 컨텍스트에서 코드가 로드되어있을때 함수 선언 부분에서 함수 코드가 저장되는데 그보다 먼저 함수 호출을 하여서 오류가 발생합니다. 

 

 

- 책 모던자바스크립트 deep dive

참고자료

- 책 모던자바스크립트 deep dive

- 가비지 컬렉션

- 메모리 생명주기, 메모리 누수

'js 이론' 카테고리의 다른 글

[js] 전역 변수의 사용을 억제하는 방법 - 즉시 실행 함수, ES6모듈  (0) 2021.10.09
객체 리터럴  (0) 2021.09.17
자바스크립트 연산자  (0) 2021.09.11
call, apply, bind  (0) 2021.08.25

댓글