변수와 스코프 메모리
자바스크립트는 느슨한 타입(loosly typed) 언어 혹은 동적(dynamic) 언어로, 변수가 가져야 할 데이터 타입을 미리 선언할 필요가 없고, 변수의 타입이 실행 중에 바뀔 수 있는 언어이다.
var foo = 42; // Number
var foo = 'bar'; // String
var foo = true; // Boolean
var foo = 42; // Number
var foo = 'bar'; // String
var foo = true; // Boolean
원시 값과 참조 값
자바스크립트에는 기본 타입(Primitive, 또는 원시 자료형)과 참조 타입(Reference)의데이터를 저장할 수 있다.
원시 타입 : Number, String, Boolean, Undefined, Null, Symbol
'값으로' 접근
변수에 저장된 실제 값을 조작
참조 타입 : Object
'참조로' 접근
객체 자체가 아니라 해당 객체에 대한 '참조'를 조작
☝️ 원시 값 vs 참조 값의 차이를 머리 속에 박아두자.
Explaining Value vs. Reference in JavaScript
원시 값
var x = 10;
var y = 'abc';
var z = null;
var x = 10;
var y = 'abc';
var z = null;
Variables | Values |
---|---|
x | 10 |
y | 'abc' |
z | null |
참조 값
var arr = [];
arr.push(1);
var arr = [];
arr.push(1);
Variables | Values | Addresses | Objects |
---|---|---|---|
arr | <#001> | #001 | [] |
Variables | Values | Addresses | Objects |
---|---|---|---|
arr | <#001> | #001 | [1] |
동적 프로퍼티
참조 값에는 언제든 프로퍼티와 메서드의 추가, 수정, 삭제가 가능하다.
var person = new Object();
person.name = 'Nicholas';
alert(person.name); // "Nicholas"
var person = new Object();
person.name = 'Nicholas';
alert(person.name); // "Nicholas"
원시 값에는 프로퍼티를 추가할 수 없다. (에러가 발생하는 것은 아니다.)
var name = 'Nicholas';
name.age = 27;
alert(name.age); // undefined
var name = 'Nicholas';
name.age = 27;
alert(name.age); // undefined
값 복사
원시 값을 다른 변수로 복사할 때는 현재 저장된 값을 새로 생성한 다음 새로운 변수에 복사한다. 두 변수는 완전히 분리되어 있다.
var num1 = 5;
var num2 = num1;
var num1 = 5;
var num2 = num1;
참조 값 복사 역시 원래 변수에 들어 있던 값이 다른 변수로 복사되기는 마찬가지인데 , 이 때 복사되는 값은 객체 자체가 아니라 객체를 가리키는 포인터라는 것이다.
var obj1 = new Object();
var obj2 = obj1;
obj1.name = 'Nicholas';
alert(obj2.name); // "Nicholas"
var obj1 = new Object();
var obj2 = obj1;
obj1.name = 'Nicholas';
alert(obj2.name); // "Nicholas"
두 변수가 같은 객체를 가리키기 때문에 객체의 프로퍼티를 변경하면, 두 값이 모두변경된다.
☝️ 오른쪽에 있는 메모리 영역을 힙(Heap)이라고 한다.
매개변수 전달
ECMAScript의 함수 매개변수는 모두 값으로 전달(Pass by value)된다. 외부에 있는 값을 함수에 인자로 넣어주면 매개변수에 복사되어 함수 내부에서 사용할 수 있다.
☝️ 매개변수(parameters)와 전달인자(arguments)
이 때 복사되는 방식은 위에서 설명한 값 복사와 동일하다. 원시 값은 혼란의 여지가없지만 참조 값의 경우 혼란의 여지가 있다.
function setName(obj) {
obj.name = 'Nicholas';
}
var person = new Object();
person.name = 'Alberto';
setName(person);
alert(person.name); // "Nicholas"
function setName(obj) {
obj.name = 'Nicholas';
}
var person = new Object();
person.name = 'Alberto';
setName(person);
alert(person.name); // "Nicholas"
이 예제에서 setName()
함수를 실행하고 나서 person.name
이 바뀐 것을 보고 매개변수가 참조 형태로 전달되었다고 오해하는 경우가 있다. 하지만, 이는 함수 내에서 참조를 통해 메모리에 있는 객체에 접근하였기 때문에 객체의 프로퍼티를 변경할수 있었던 것이다.
Variables | Values | # | Addresses | Objects |
---|---|---|---|---|
person | <#234> | # | #234 | {name: "Alberto"} → |
obj | <#234> | # |
function setName(obj) {
obj = {
name: 'Nicholas',
};
}
var person = new Object();
person.name = 'Alberto';
setName(person);
alert(person.name); // "Alberto"
function setName(obj) {
obj = {
name: 'Nicholas',
};
}
var person = new Object();
person.name = 'Alberto';
setName(person);
alert(person.name); // "Alberto"
반면, 함수 내부를 매개변수 obj
에 새로운 객체를 할당하는 것으로 변경하면 setName()
함수를 실행해도 person.name
이 변하지 않은 것을 볼 수 있다.
처음에는 매개변수 obj
에 person
의 주소 값이 복사되었으나, 이후에 obj
에새로운 객체( {name: "Nicholas"}
)를 할당한 것으로, 기존의 객체는 영향을 받지않는다.
Variables | Values | # | Addresses | Objects |
---|---|---|---|---|
person | <#234> | # | #234 | |
obj | <#234> → <#567> | # | #567 |
Understanding JavaScript Pass By Value
타입 판별
typeof
연산자는 변수가 Number, String, Boolean, undefined 일 때는 정확한 타입을 알 수 있고, 객체이거나 null 일 경우 object를 반환한다.
객체가 어떤 타입의 객체인지 알고 싶을 때가 있는데 이 때는 typeof
연산자 대신 instanceof
연산자가 도움이 된다. instanceof
연산자는 object의 프로토타입 체인에 constructor.prototype
이 존재하는지 판별한다.
var person = {
name: 'Alberto',
};
var colors = ['red', 'blue', 'green'];
var fruit1 = new String('banana');
var fruit2 = 'apple';
console.log(person instanceof Object); // true
console.log(colors instanceof Object); // true
console.log(colors instanceof Array); // true
console.log(fruit1 instanceof Object); // true
console.log(fruit1 instanceof String); // true
console.log(fruit2 instanceof Object); // false
console.log(fruit2 instanceof String); // false
var person = {
name: 'Alberto',
};
var colors = ['red', 'blue', 'green'];
var fruit1 = new String('banana');
var fruit2 = 'apple';
console.log(person instanceof Object); // true
console.log(colors instanceof Object); // true
console.log(colors instanceof Array); // true
console.log(fruit1 instanceof Object); // true
console.log(fruit1 instanceof String); // true
console.log(fruit2 instanceof Object); // false
console.log(fruit2 instanceof String); // false
위와 같이 객체에 instanceof
을 이용해 어느 객체의 인스턴스인지를 판별할 수 있다.
☝️
fruit1
과fruit2
의 차이에 유의하자.fruit1
은 String 객체를 생성한것이고,fruit2
는 리터럴로 기본 타입 String 값이다.
[자바스크립트] new String("")과 ""(String literal)과 String("")에 대하여
실행 컨텍스트와 스코프
변수나 함수의 실행 컨텍스트는 다른 데이터에 접근할 수 있는지, 어떻게 행동하는지를 규정한다. 각 실행 컨텍스트에는 변수 객체(variable object)가 연결되어 있으며해당 컨텍스트에서 정의된 모든 변수와 함수는 이 객체에 존재한다.
☝️ 이 객체는 코드에서 접근할 수는 없다. 이면에서 데이터를 다룰 때 이 객체를 이용한다.
그림 출처 : 🔗 실행 컨텍스트와 자바스크립트의 동작 원리 - Poiemaweb
컨트롤이 실행 가능한 코드로 이동하면, 논리적 스택 구조를 가지는 새로운 실행컨텍스트 스택이 생성된다. 스택은 LIFO(Last In First Out, 후입 선출)의 구조를가지는 나열 구조이다.
전역 코드(Global code)로 컨트롤이 진입하면, 전역 실행 컨텍스트가 생성되고 실행 컨텍스트 스택에 쌓인다. 전역 실행 컨텍스트는 애플리케이션이 종료될 때(웹페이지에서 나가거나 브라우저를 닫을 때)까지 유지된다.
함수를 호출하면 해당 함수의 실행 컨텍스트가 생성되며 직전에 실행된 코드 블록의 실행 컨텍스트 위에 쌓인다.
함수 실행이 끝나면 해당 함수의 실행 컨텍스트를 파기하고 직전의 실행 컨텍스트에 컨트롤을 반환한다.
컨텍스트에서 코드를 실행하면, 변수 객체에 스코프 체인(scope chain)이 만들어 진다 . 스코프 체인의 목적은 실행 컨텍스트가 접근할 수 있는 모든 변수와 함수에 순서를정하는 것이다.
코드가 실행되는 컨텍스트의 변수 객체
해당 컨텍스트를 포함하는 컨텍스트(부모 컨텍스트)
부모의 부모 컨텍스트
...
전역 컨텍스트
식별자를 찾을 때 이 스코프 체인의 순서를 따라가면서 식별자 이름을 검색한다. 내부컨텍스트는 스코프 체인을 통해 외부 컨텍스트 전체에 접근 가능하지만, 외부 컨텍스트는 내부 컨텍스트에 대해 전혀 알 수 없다.
var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
console.log(color);
var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
console.log(color);
이 코드에는 전역 컨텍스트, changeColor()
의 로컬 컨텍스트, swapColors()
의로컬 컨텍스트로 세 개의 콘텍스트가 있다. swapColors
는 자신의 변수 객체에서 변수나 함수 이름을 검색하고 찾지 못하면 스코프 체인을 따라 한 단계 씩 올라간다. 하지만 전역 컨텍스트에서는 로컬 컨텍스트에 접근할 수 없다.
스코프 체인 확장
실행 컨텍스트의 스코프 체인을 확장할 수 있는 방법이 있다.
try-catch
문의catch
블록with
문
두 문장은 스코프 체인 앞에 변수 객체를 추가한다. with
문은 해당 객체가 스코프체인에 추가되고, catch
문에서는 에러 객체를 선언하는 변수 객체가 생성된다.
function buildUrl() {
var qs = '?debug=true';
with (location) {
var url = href + qs;
}
return url;
}
function buildUrl() {
var qs = '?debug=true';
with (location) {
var url = href + qs;
}
return url;
}
여기서 href
는 with
문이 추가한 location 객체에 들어있는 변수이다. with
문내부에서 선언한 변수 url 이 함수의 컨텍스트로 편입되어 함수 값으로 반환될 수 있다.
자바스크립트의 블록 레벨 스코프
ES5까지 var
가 변수를 선언할 수 있는 유일한 키워드였지만, ES6부터는 let
키워드를 사용하여 블록 레벨 스코프의 변수를 정의할 수 있다.
함수 레벨 스코프(Function-level scope) 함수 내에서 선언된 변수는 함수 내에서만 유효하며 함수 외부에서는 참조할 수 없다. 즉, 함수 내부에서 선언한 변수는지역 변수이며 함수 외부에서 선언한 변수는 모두 전역 변수이다.
블록 레벨 스코프(Block-level scope) 모든 코드 블록(함수, if 문, for 문, while 문, try/catch 문 등) 내에서 선언된 변수는 코드 블록 내에서만 유효하며 코드블록 외부에서는 참조할 수 없다. 즉, 코드 블록 내부에서 선언한 변수는 지역 변수이다.
var
를 사용하여 선언된 변수는 함수 레벨 스코프를 가지기 때문에, if 문, for 문, while 문 등의 블록 내부에서 사용되는 변수라 할지라도 해당 블록이 실행된 이후에파괴되지 않고 존재한다. (다른 언어에서는 일반적으로 블록 레벨 스코프로 해당 변수는파괴된다.)
if (true) {
var color = 'blue';
}
alert(color); // "blue"
for (var i = 0; i < 10; i++) {
doSomething(i);
}
alert(i); // 10
if (true) {
var color = 'blue';
}
alert(color); // "blue"
for (var i = 0; i < 10; i++) {
doSomething(i);
}
alert(i); // 10
변수 선언
var
로 선언된 변수는 자동으로 가장 가까운 컨텍스트에 추가된다. 함수 내부에서는함수의 로컬 컨텍스트, with
문 내부에서는 함수 컨텍스트가 가장 가까운 컨텍스트이다. 변수를 선언하지 않은 채 초기화 할 경우 자동으로 전역 컨텍스트에 추가된다.
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}
var result = add(10, 20);
alert(sum); // 유효한 변수가 아님
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}
var result = add(10, 20);
alert(sum); // 유효한 변수가 아님
function add(num1, num2) {
sum = num1 + num2; // var 키워드를 생략하여 전역 컨텍스트
return sum;
}
var result = add(10, 20);
alert(sum); // 30
function add(num1, num2) {
sum = num1 + num2; // var 키워드를 생략하여 전역 컨텍스트
return sum;
}
var result = add(10, 20);
alert(sum); // 30
식별자 검색
컨텍스트 안에서 식별자를 참조하려 하면 식별자를 검색하게 되는데, 검색은 가장 가까운 컨텍스트에서 시작하여 이름을 찾으면 검색을 멈추고 변수를 설정한다. 찾지 못하면 검색을 계속하는데, 전역에서도 찾지 못할 경우 식별자가 정의되지 않은 것으로판단하고 검색을 멈춘다.
var color = 'blue';
function getColor() {
return color;
}
alert(getColor()); // "blue"
var color = 'blue';
function getColor() {
return color;
}
alert(getColor()); // "blue"
식별자가 로컬 컨텍스트에 정의되어 있으면, 부모 컨텍스트에 같은 이름의 식별자가있어도 참조할 수 없다. 전역 컨텍스트의 변수를 이용할 경우 window 객체를 참조하면사용할 수 있다.
var color = 'blue';
function getColor() {
var color = 'red';
console.log(color);
console.log(window.color);
}
getColor();
var color = 'blue';
function getColor() {
var color = 'red';
console.log(color);
console.log(window.color);
}
getColor();
가비지 콜렉션
C 언어에서는 메모리 관리를 위해 malloc()
과 free()
를 사용하는데, 자바스크립트는 객체가 생성되었을 때 자동으로 메모리를 할당하고 쓸모 없어졌을 때 자동으로해제하는 가비지 컬렉션을 사용한다. 이러한 가비지 컬렉션 프로세스는 주기적으로 실행되고, 특정 시점에서 시행하도록 지정할 수도 있다.
함수 내 지역 변수의 경우 함수를 실행하면 변수가 생성되고, 스택에 변수의 값을 저장할 메모리가 할당된다. 함수가 종료되면 변수가 더 이상 필요하지 않으므로 가비지컬렉터가 회수할 수 있다. 하지만 어떤 변수가 더 이상 사용되지 않는지, 사용될 가능성이 있는지 판단할 수 있는 방법이 필요하다.
표시하고 지우기
자바스크립트에서 널리 쓰이는 가비지 컬렉션 방법은 **"표시하고 지우기 (mark-and-sweep)"**이다. 가비지 컬렉터가 작동하면 메모리에 저장된 변수 전체에 표시를 남긴 다음, 컨텍스트에 있는 변수와 컨텍스트에 있는 변수가 참조하는 변수에서표시를 지운다. 이렇게 한 뒤 표시가 남아 있는 변수가 컨텍스트에 있는 변수와 무관하므로 삭제해도 안전하다고 판단하여, '메모리 청소'를 실행해 파괴하고 메모리를 회수한다.
참조 카운팅
**"참조 카운팅(reference counting)"**이라고 불리는 가비지 컬렉션 방법도 있다. 변수를 선언하고 참조 값이 할당될 때마다 참조 카운트를 1씩 늘리고, 값을 참조하는 변수에 다른 값이 할당 될 때마다 참조 카운트를 1씩 줄여서, 참조 카운트가 0이라면 해당 값에 접근할 방법이 없다고 판단하여 메모리를 회수하는 방법이다.
하지만 이는 순환 참조라는 심각한 문제가 있다.
function problem() {
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
function problem() {
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
이 예제에서 objectA
와 objectB
가 서로를 참조하여 각각의 참조 카운트가 2인데 , 함수 실행이 끝난 뒤에도 참조 카운트가 0이 되지 않으므로 두 변수가 계속 메모리에 남아 낭비가 발생한다.
익스플로러 8과 이전 버전에서 객체 중 일부가 C++을 사용한 참조 카운팅 방식으로구현되었고 순환 참조 문제가 발생했다.
순환 참조 문제를 해결하기 위해서는 해당 객체를 다 사용한 뒤에 둘 사이의 연결을끊어야 한다. null을 할당하면 연결을 제거할 수 있다.
성능
가비지 컬렉션을 실행하는 타이밍이 성능 향상을 위해 중요하다. 인터넷 익스플로러는너무 자주 가비지 컬렉터를 실행하여 성능 문제를 야기해왔다.
메모리 관리
가비지 컬렉션을 지원하는 프로그래밍 환경에서는 개발자가 메모리 관리를 신경쓰지않아도 되므로 편리하다. 하지만 웹 브라우저에서 사용할 수 있는 메모리는 일반 데스크톱 앱에 비해 매우 적으므로 가능한 한 최소한의 메모리만 사용해야 성능을 올릴 수있다.
가장 좋은 방법은 코드 실행에 필요한 데이터만 유지하는 것이다. 필요없어진 데이터는 null을 할당하여 참조를 제거하는 것이 좋다. 수동으로 참조 제거해야 할 대상은주로 전역 변수 및 전역 객체의 프로퍼티 인데, 지역 변수는 컨텍스트를 빠져나가는순간자동으로 참조가 제거되지만, 전역 변수는 참조를 제거하지 않으면 계속 메모리에 남아있게 된다.
☝️ 참조를 제거한 순간 할당된 메모리가 자동으로 반환되는 것은 아니다. 참조 제거를 한 이후에 실행될 가비지 컬렉션에서 해당 메모리가 회수된다.
요약
자바스크립트 변수에는 원시 값과 참조 값 두 가지 형태로 값을 저장할 수 있다.
원시 값과 참조 값에는 다음의 특징이 있다.
원시 값은 고정된 크기를 가지며 스택 메모리에 저장된다.
원시 값을 한 변수에서 다른 변수로 복사하면 값 자체가 복사된다.
참조 값은 객체이며 힙 메모리에 저장된다.
변수에 참조 값을 저장하면 해당 변수는 객체에 대한 참조만 저장할 뿐 객체 자체를저장하는 것은 아니다.
참조 값을 한 변수에서 다른 변수로 복사하면 해당 객체에 대한 참조만을 복사하므로 두 변수는 같은 객체를 참조한다.
typeof
연산자는 값의 원시 타입을 판별하며instanceof
연산자는 값의 참조 타입을 판별한다.
원시 값과 참조 값을 가리지 않고 모든 변수는 스코프라고 부르기도 하는 실행 컨텍스트에 존재하는데, 실행 컨텍스트는 변수가 존재하는 기간을 결정하며 어느 코드가 해당 변수에 접근할 수 있는지도 결정한다.
실행 컨텍스트에는 전역 컨텍스트와 함수 컨텍스트가 있다.
실행 컨텍스트에 진입할 때마다 스코프 체인이 만들어지며, 스코프 체인은 변수와함수를 검색하는데 쓰인다.
함수 컨텍스트는 해당 스코프에 있는 변수, 해당 스코프를 포함하는 컨텍스트에 있는 변수, 전역 컨텍스트에 있는 변수에 모두 접근할 수 있다.
전역 컨텍스트는 전역 컨텍스트에 있는 변수와 함수에만 접근할 수 있으며, 로컬(함수) 컨텍스트에 있는 데이터에 직접적으로 접근할 수는 없다.
실행 컨텍스트는 변수에 할당된 메모리를 언제 해제할 수 있는지 판단하는데 도움이된다.
자바스크립트는 자동으로 가비지 컬렉션을 수행하므로 개발자가 메모리 할당과 회수에크게 신경 쓸 필요는 없다. 자바스크립트의 가비지 컬렉션 루틴은 다음과 같다.
값이 스코프를 벗어나면 자동으로 표시되고 다음에 가비지 컬렉션을 실행할 때 삭제된다.
주로 쓰이는 가비지 컬렉션 알고리즘은 "표시하고 지우기"이다.
다른 알고리즘은 "참조 카운팅"인데 이 방법은 순환 참조 문제가 있다.
변수에서 참조를 제거하면 순환 참조 문제도 해결할 수 있고, 가비지 콜렉션에도 도움이 된다.
효율적으로 관리하기 위해서는 전역 객체, 전역 객체의 프로퍼티, 순환 참조에 대한참조를 제거해야 한다.
참고자료
Explaining Value vs. Reference in JavaScript
Understanding JavaScript Pass By Value
[자바스크립트] new String("")과 ""(String literal)과 String("")에 대하여