[모던 자바스크립트 Deep Dive] 04장

Gyuwon Lee·2022년 7월 14일
0
post-thumbnail

42 서울의 모던 자바스크립트 Deep Dive 스터디 학습의 일환으로, 새롭게 알게 된 내용 중심으로 정리되어 있습니다.


CH04. 변수

1) 자바스크립트 엔진과 변수

10 + 20

간단한 JS 코드를 아무거나 실행한다고 생각해 보자. 엔진은 위 표현식 에서 덧셈 연산자 와, 그 양 변의 숫자 리터럴 을 해석하여 전체 식을 평가 한다. 이렇게 엔진은 표현식 으로부터 평가값 을 도출해낼 수 있다. 그런데, 그 다음엔?

위 식에서 리터럴 10과 20, 평가값 30은 각기 다른 메모리 공간에 저장되어 있다. 만약 우리가 두 리터럴을 연산해 얻은 평가값을 코드의 다른 곳에서 사용하고 싶다면, 어떻게 접근할 수 있을까?

값이 저장되는 메모리의 주소는 가변적이다. 컴퓨터의 재실행, 프로그램의 재실행 때마다 주소는 변경될 수 있다. 그래서 변수 를 사용한다.

자바스크립트에서 변수란, "값의 위치를 가리키는 상징적인 이름"이다. 즉, 개발자의 직접적인 메모리 제어를 허용하지 않고, 변수를 선언 및 사용하여서만 메모리에 접근할 수 있다.

우리가 코드 내부에서 사용한 변수는, 인터프리터가 읽어들일 때 값이 저장된 메모리 공간의 주소 로 치환되어 실행된다.

사용자는 이러한 변수를 할당참조 라는 두 가지 방식으로 사용할 수 있다:

  • 할당 : 변수에 값을 저장하는 것
  • 참조 : 변수에 저장된 값을 읽어 들이는 것

2) 변수와 식별자

식별자 는 변수를 포함하는 개념이다. 식별자란 어떤 값을 구별해서 식별할 수 있는 고유한 이름을 말한다. 즉 값들 간의 차이를 알고 분별할 수 있게 해주는 이름이다.

그러기 위해서는 식별자가 값이 저장되어 있는 메모리 주소를 알고 있어야 한다. 즉 값이 저장되어 있는 메로리 주소와 매핑 관계를 맺는다. 이 때, 이 매핑 정보도 메모리에 저장되어야 한다.

즉, 식별자는 값이 아니라 메모리 주소를 기억(저장)하고 있다. 우리는 식별자에 값을 담았다고 생각하지만 사실 이 값은 런타임 과정에서 인터프리터가 식별자에 담긴 메모리 주소에 접근해야 비로소 불러와진다.

앞서 말했듯 식별자는 변수를 '포함' 하는 개념이다. 변수뿐만 아니라 함수, 클래스 등의 '이름' 은 모두 식별자다. 메모리 상에 존재하는 어떤 값을 식별할 수 있는 이름은 모두 식별자라고 부른다.


3) 변수 선언

그럼 변수를 선언하면 어떤 일이 생기는가?

var foo = 10; // typeof foo == Number

(1) 선언 단계

먼저, 선언된 변수 이름을 등록해서 자바스크립트 엔진에 변수의 존재를 알린다.

(2) 초기화 단계

등록된 변수 이름에 매핑할 메모리 공간이 확보된다. 이때 메모리 공간은 undefined 로 초기화된다.

📌 undefinednull
undefined 는 '값이 할당되지 않은 상태’를 나타낼 때 사용한다. 변수는 선언했지만, 값을 할당하지 않았다면 해당 변수에 undefined 가 자동으로 할당된다. null 과 같이, 개발자가 명시적으로 변수에 undefined 를 할당하는 것도 가능하긴 하다. 그러나 관습적으로 undefined 는 값이 할당되지 않은 변수의 초기값을 표현하는 예약어로 사용된다. 변수가 '비어있거나', '알 수 없는' 상태라는 걸 나타내려면 null 을 사용한다.

위의 두 단계를 거쳐야만 해당 변수를 코드의 다른 부분에서 사용할 수 있게 된다. 이렇게 선언되지 않은 변수(식별자)에 접근했을 때는 ReferenceError 가 발생한다.

reference 라는 말뜻에서 알 수 있듯, 어떤 식별자의 이름을 통해 값을 참조하려 했지만 엔진에 해당 식별자가 등록되어 있지 않아 찾을 수 없을 때 발생하는 에러다.

그런데, 코드를 짜다 보면 ReferenceError 즉 에러를 뱉고 실행이 중단되어야 하는 상황임에도 에러가 발생되지 않고 실행이 이어지는 경우 가 발생할 수 있다. 이러한 경우 사용자가 정확히 어느 지점에서 코드가 오작동하고 있는지 알아내기 어려워진다. 그 중 하나가 변수 호이스팅 문제다.


4) 변수 선언의 실행 시점 (feat. 호이스팅)

console.log(score); // undefined

var score;

위 예제 코드를 엔진이 실행시킨다고 생각해 보자. 윗 줄에서부터 순차적으로 코드를 실행시키므로 먼저 console.log 가 실행되어야 하는데, 인자로 받고 있는 score 라는 것은 현 단계(첫 줄)에서 아직 선언되지 않은 식별자다. 그러므로 엔진에서 찾을 수 없으니 ReferenceError 가 발생해야 하는데, undefined 를 출력하며 console.log 가 그냥 작동해 버린다.

그 이유는 변수 선언이 런타임 이전 단계에서 사전 실행되기 때문이다. 자바스크립트 엔진은 주어진 소스를 바로 한줄씩 실행하는 것이 아니라, 소스코드 평가 과정을 사전에 거친다.

평가 과정에서 엔진은 변수 선언을 포함한 모든 선언문을 소스코드에서 찾아내 먼저 실행한다. 그 다음 이어지는 런타임에서는 평가한 선언문들을 전부 제외하고 소스코드를 한 줄씩 순차적으로 실행한다.

따라서 자바스크립트에서는 변수(뿐 아니라 함수나 클래스 등 까지도)가 코드의 어디에 위치해 있든, 그 선언 은 다른 코드에 선행해서 실행된다. 따라서 선언문을 제외한 다른 코드는 식별자가 어디에 있든지 이를 참조할 수 있다. 아직 값이 없는 undefined 상태여도 말이다.

이처럼 변수(식별자) 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트 고유의 특징을 변수 호이스팅 이라고 한다.


5) 값의 할당

아까 위에서 변수 선언의 앞선 두 단계, 선언과 초기화 단계를 다루었다. 이 두 단계를 거친 변수는 undefined 값으로 초기화되어있는 상태다. 이 변수에 사용자는 할당 연산자 = 을 사용해서 값을 할당한다.

var foo;
foo = true;

할당은 할당 연산자가 사용된 순간에 이루어진다. 따라서 위의 코드를 var foo = true; 라고 단축해서 표현해도, var foo = true 는 각기 다른 단계에 해당해서 엔진에 의해 분리된다. 앞서 호이스팅 문제에서 다루었듯, 변수 선언과 값의 할당은 실행 시점이 다르다. 변수 선언은 소스코드 평가 단계에서, 값의 할당은 그 다음인 런타임 단계에서 실행된다.

💡 변수 초기화, 값의 할당, 그리고 메모리 공간

값을 할당하기 전에 엔진은 변수의 이름을 등록하고 메모리 공간을 확보해서 undefined 값으로 초기화한다고 했다. 즉 이 때 할당된 메모리 공간은 undefined 값의 크기만큼 확보되어 있다.

이 시점에서는 값의 할당 단계에서 어떤 자료형의 값이 할당될지 아직 알지 못한다. 당연히 undefined 와 새롭게 할당될 값의 메모리상 크기는 다를 수 있다.

즉, 변수에 값이 할당될 때는 초기값 undefined 가 저장되어 있던 메모리 공간을 지우고 그 공간에 값을 새롭게 저장하는 것이 아니라 새로운 메모리 공간을 확보하고 그곳에 할당 값을 저장하게 된다.


6) 값의 재할당

var foo = 10;
const bar = 20;

foo = 30;

var 키워드로 선언된 변수 foo 에 값 10을 할당했다. 그리고 아래에서 동일한 변수에 새롭게 값 30을 할당했다. 그러면 undefined 로 초기화된 변수에 처음 값을 할당할 때와 마찬가지로, 10이 있던 자리에 30을 새롭게 채워넣는 것이 아니라 값 10이 저장된 메모리 공간은 그대로 두고, 새로운 메모리 공간에 값 30을 저장하고 변수에 매핑한다.

그러면 10이 저장되어 있는 메모리 공간에는 여전히 값이 들어있으므로 빈 공간은 아니지만, 그렇다고 접근가능하거나 변수에 의해 사용되고 있는 상태도 아니다. 즉 불필요한 값이 된다.

이러한 값들은 가비지 콜렉터 에 의해 메모리에서 자동 해제된다. 단, 메모리에서 언제 해제될지는 예측할 수 없다.

당연히, const 키워드로 선언된 변수는 재할당이 불가능하다. 값이 새롭게 할당되는 원리를 반영한다면 '한 번 매핑된 메모리 주소를 변경할 수 없다' 는 뜻으로도 해석해볼 수 있다.


[스터디 내용 추가] 함수의 호이스팅

변수가 아닌, 함수에서 호이스팅은 어떤 형태로 나타날까?

아래 코드는 네 가지 방식으로 함수를 작성한다:
1. 함수 선언식 으로 작성
2. const 키워드 사용, 함수 표현식 으로 작성
3. let 키워드 사용, 화살표 함수 로 작성
4. var 키워드 사용, 화살표 함수 로 작성

console.log(dec(2,3))
console.log(exp(2,3))
console.log(arrow(2,3))
console.log(arrow2(2,3))

function dec(num, num2) {
    return num + num2;
}

const exp = function(num1, num2) {
    return num + num2;
  }

let arrow = (num, num2) => {
	return num + num2;
} 

var arrow2 = (num, num2) => {
	return num + num2;
} 
  • dec : function 함수명 () {} 형태의 함수 선언식 으로 작성된 dec 는 호이스팅되지만 에러를 일으키지 않으며, 코드의 위치에 관계없이 함수의 기능대로 정상 작동한다.
    • 함수 선언식 으로 작성된 함수의 경우, 엔진이 런타임 이전에 소스코드를 평가할 때 함수의 스코프 전체를 포함한다. 즉 function 함수명 까지만 평가하지 않고, function 함수명 () {} 을 전부 끌어올려 평가하므로 해당 함수가 코드의 어느 부분에서 사용되든 정상적으로 사용할 수 있다.
  • exp : function () {} 형태의 함수 표현식 으로 작성된 const 변수 exp 는 호이스팅되어 ReferenceError 에러를 일으킨다.
    • 함수 표현식 으로 작성된 함수의 경우, 할당 연산자 = 를 사용하여 임의의 변수에 저장된다. JS에서는 함수도 값이기 때문에 이러한 할당이 가능하다.
    • 이 때 소스코드 평가 과정에서 엔진은 할당 연산자의 좌변, 즉 const exp 라는 변수 선언 부분만 평가하고 우변의 함수 표현은 런타임 시점에 할당한다. 이는 함수의 선언과 초기화를 각각 따로 하는 let, const 키워드의 특성이다.
    • 따라서 console.log(exp(2,3)) 가 호출된 시점에는 참조할 수 있는 값이 없는 상태이므로 ReferenceError 가 발생한다.
  • arrow : () => {} 형태의 화살표 함수 로 작성된 let 변수 arrow 는 호이스팅되어 ReferenceError 에러를 일으킨다.
    • exp 의 경우와 마찬가지로, 할당 연산자 좌변의 let arrow 라는 변수 선언만 평가되고 함수의 할당은 런타임 시점에 이루어지므로 동일한 에러가 발생한다.
  • arrow2 : () => {} 형태의 화살표 함수 로 작성된 var 변수 arrow2 는 호이스팅되어 TypeError 에러를 일으킨다.
    • 위의 두 변수와 달리, var 키워드로 선언된 arrow2 는 변수의 선언과 초기화가 동시에 이루어져, undefined 값을 무조건 갖게 된다. 이 경우가 호이스팅이 문제가 되는 시점으로, 에러가 나야 하는데 코드가 멈추지 않고 돌아가는 문제가 발생한다.
    • 하지만, var arrow2undefined 값을 가진 변수인 데 반해, console.log 내부에서는 arrow2() 와 같이 함수 호출의 형태로 사용하고 있다. 따라서 함수 값이 아닌 변수를 함수와 같이 사용하려 했으므로 TypeError 가 발생하며 코드가 작동하지 않는다.
profile
하루가 모여 역사가 된다

0개의 댓글