크게 통신, 렌더링, 스크립트 실행 으로 나눌 수 있다.
통신은 말 그래도 서버와의 통신을 의미한다. 브라우저가 서버에게 요청을 보내면, 서버는 특정 값을 응답한다.
렌더링은 dom이라는 객체를 화면에 그리는 것을 의미한다. dom은 통신을 통해 받은 html을 브라우저가 읽어서 생성된다. dom은 트리구조로 이루어진다.
스크립트 실행은 JS의 실행을 의미한다. 브라우저가 <script>
태그를 통해 js 파일을 읽으면 바로 실행된다. 이를 통해 동적인 화면을 구성 가능하다.
일반적으로 메모리는
할당 => 사용 => 해제의 단계를 거친다.
우리가 변수를 선언하면, 메모리 한 구역에 변수가 할당 되고
우리는 변수에 값을 넣어 사용할 수 있다.
사용을 마치면 해제해서 메모리에서 변수를 제거할 수 있다.
만약 메모리가 꽉 찬다면 ?
=> 프로그램이 터지기 때문에 별도의 조치가 필요하다.
=> 그래서 메모리를 해제하는 과정이 필요하지만 우리는 메모리를 해제하는 방법을 모른다.
JS 엔진은 Garbage Collector을 통해 메모리를 정리한다.
=> Garbage Collector는 Garbage Collection이라는 자동 메모리 관리 알고리즘을 통해 만들어진 객체로 사용하지 않는 메모리를 해제합니다.
=> 그래서 우리는 메모리를 신경쓰지 않고 코딩이 가능하다 !
let variable = 126
이 코드가 실행될 때
=> 우리가 선언한 변수나 상수는, 값을 바라보는 것이 아닌, 메모리 주소를 바라보고 있다.
let variable = 126
let variable2 = variable
이렇게 새로운 변수에 기존 변수를 대입한다면,
=> variable2
가 variable
의 메모리 주소를 참조하게 된다.
여기서,
variable = variable + 1
이렇게 기존 변수를 조작한다면 어떻게 될까 . . . ?
=> 기존 variable
가 가리키던 메모리 주소가 아닌, 새로 메모리 주소를 할당 받고, 그곳에 값을 넣게 된다.
=> 이는 JS에서 원시 타입은 값의 변경이 불가능하기 때문이다 !
특정 메모리에 값이 할당이 되면, 그 메모리에 다른 값은 할당이 불가능하다. Garbage collector가 이를 해제한 후에야 가능하다
=> 원시 타입의 값이 변경될 때는 항상 메모리가 새로 할당된다.
JS 엔진은 가상 머신(Virtual Machine)으로 구성되어 있다.
가상 머신에는 메모리 영역이 구현되어 있는데 Heap 영역과 CallStack 영역이 있다.
Heap에는 참조 타입이 들어가고, Callstack에는 원시 타입이 들어간다.
=> 이 때문에 상수로 선언한 배열에 값이 추가가 가능한 것 !
참고
Callstack vs Memory Heap 과 let vs const
실행 컨텍스트(Execution Context)의 렉시컬 환경(Lexical Environment) ?
Garbage Collector를 통해 사용하지 않는 메모리를 해제한다.
현대의 Garbage Collector는 Mark and Sweep Algorithm
을 통해 메모리를 정리한다.
Mark and Sweep Algorithm
=> 닿을 수 없는 주소를 더 이상 필요없는 주소로 정의하고 지우는 알고리즘
스코프란?
유효 범위라고도 부르며, 변수가 어느 범위까지 참조되는 지를 뜻한다.
어디서든 접근 가능한 global scope
해당 context 내에서만 접근 가능한 local scope
var를 사용하면 안되는 이유
var a = 5;
{
// 호이스팅 되어 변수 선언이 상단으로 올라가 버린다.
var a = 10;
console.log(a); // 10
}
console.log(a); // 10
=> block 내부에 a를 새롭게 선언하더라도 외부 변수 값도 변하게 된다.
=> var는 function scope이고, const와 let은 block scope 이기 때문이다.
=> 개발자가 오류를 찾기 힘들어지기 때문에 가급적 사용하지 말자.
함수가 선언된 환경의 스코프를 기억하여 함수가 스코프 밖에서 실행될 때에도 기억한 스코프에 접근할 수 있게 만드는 방법
function makeGreeting(name){
const greeting = "Hello, ";
return function() {
console.log(greeting + name);
};
}
const world = makeGreeting("World!");
const minwoo = makeGreeting("Min-woo");
world(); // Hello, world !
minwoo();
const greeting = "Hello, ";
는 지역 스코프라서 함수가 종료되면 메모리에서 사라지지만,
world();
실행 시점에 greeting이 살아있는 걸 확인할 수 있다.
=> 클로저를 사용하면 내부 변수와 함수를 숨길 수 있다. (은닉화)
클로저를 잘 알아야하는 이유는 유용하게 사용하기보단 알기 힘든 버그를 잘 수정하기 위해서이다 !
function counting() {
let i=0;
for (i=0; i<5; i+=1) {
setTimeout(function() {
console.log(i);
}, i*100);
}
}
counting();
-> 5 5 5 5 5
콜백함수가 실행된 시점에는 이미 루프가 종료되어 상위 i 값은 이미 5까지 증가한 상태라서 5가 5번 출력된다.
이를 해결할 수 있는 방법에는 두 가지가 있다.
function counting() {
let i = 0;
for (i=0; i<5; i+=1) {
(function (number) {
setTimeout(function() {
console.log(number);
}, number*100);
})(i);
}
}
counting();
-> 0 1 2 3 4
function counting() {
for (let i = 0; i < 5; i += 1) {
setTimeout(function () {
console.log(i);
}, i * 100);
}
}
count(); // 0 1 2 3 4
let은 블록 수준 스코프라서, for문 내에서 새로운 스코프를 갖기 때문에 루프마다 클로저가 생성됩니다.
for문이 한번 반복될 때마다 안에서 let 변수가 계속 새로 생성되는 것이다.
호이스팅을 설명하기 전에, JS에서 변수가 생성되는 과정을 자세히 알아보자.
자바스크립트 엔진은 코드를 실행하기 전 실행 가능한 코드를 형상화하고 구분하는 과정(실행 컨텍스트를 위한 과정)을 거친다.
자바스크립트 엔진은 코드를 실행하기 전 실행 컨텍스트를 위한 과정에서 모든 선언(var, let, const, function, class)을 스코프에 등록한다.
변수는 3단계에 걸쳐 생성된다.
1단계: 선언 단계(Declaration phase)
2단계: 초기화 단계(Initialization phase)
3단계: 할당 단계(Assignment phase)
var 키워드로 선언한 변수는 선언 단계와 초기화 단계가 한번에 이뤄진다. 즉, 스코프에 변수를 등록(선언 단계)하고 메모리에 변수를 위한 공간을 확보한 후, undefined로 초기화한다. 따라서 변수 선언문 이전에 변수에 접근하여도 스코프에 변수가 존재하기 때문에 에러가 발생하지 않는다. 다만 undefined를 반환한다. 이후 변수 할당문에 도달하면 비로소 값이 할당된다.
=> 즉, var는 호이스팅 시 선언과 초기화 단계가 함께 이루어진다.
let 키워드로 선언된 변수는 선언 단계와 초기화 단계가 분리되어 진행된다. 즉, 스코프에 변수를 등록(선언 단계)하지만 초기화 단계는 변수 선언문에 도달했을 때(코드 실행 후) 이뤄진다. 초기화 이전에 변수에 접근하려고 하면 참조 에러가 발생한다. 이는 아직 변수가 초기화되지 않았기 때문이다. 즉, 변수를 위한 메모리 공간이 아직 확보되지 않았기 때문이다. 따라서 스코프의 시작 지점부터 초기화 시작 지점까지는 변수를 참조할 수 없다. 스코프의 시작 지점부터 초기화 시작 지점까지의 구간을 ‘일시적 사각지대(Temporal Dead Zone; TDZ)’라고 부른다.
=> 즉, let은 호이스팅 시 선언 단계만 진행된다.
console.log(num); // 호이스팅한 var 선언으로 인해 undefined 출력
var num; // 선언
num = 6; // 초기화
이는 사실
var num = undefined; // 선언 + 초기화
console.log(num); // 호이스팅한 var 선언으로 인해 undefined 출력
num = 6; // 할당
순서로 진행되는 것 !
console.log(num); // ReferenceError
num = 6; // 초기화
이는 사실
let num; // 선언 + 초기화되지 않음
console.log(num); // ReferenceError
num = 6; // 초기화
변수 선언 키워드 없이 num = 6;
로 초기화만 했을 경우, 자동적으로 let
선언으로 동작하기 때문에 호이스팅 시 변수를 초기화하지 않기 때문에 변수를 읽으려는 시도에서 ReferenceError가 발생한다.
let
과 const
로 선언한 변수도 호이스팅 대상이지만, var
와 달리 호이스팅 시 undefined
로 변수를 초기화하지는 않는다. 따라서 변수의 초기화를 수행하기 전에 읽는 코드가 먼저 나타나면 예외가 발생한다.
렉시컬 환경이란 식별자-변수 맵핑 데이터 구조이다.
여기서 식별자는 변수/함수의 이름을 참조하고, 변수는 실제 객체 [포함된 함수 객체] 또는 원시값을 참조한다.
렉시컬 환경의 컨셉은 다음과 같다.
LexicalEnvironment = {
Identifier: <value>,
Identifier: <function object>
}
다시 말해, 렉시컬 환경은 프로그램이 실행되는 동안 변수와 함수가 존재하는 장소이다.
undefined VS "초기화되지 않은 상태"
undefined
lexicalEnvironment = { a: undefined }
초기화되지 않은 상태
lexicalEnvironment = { a: <uninitialized> }
호이스팅 함수 선언 시 렉시컬 환경의 변화
helloWorld(); // 'Hello World!'가 콘솔에 찍힌다
function helloWorld(){
console.log('Hello World!');
}
컴파일 단계에서 함수 선언이 메모리에 추가된다는 것을 이미 알고 있기 때문에 실제 함수 선언 전에 우리 코드로 그 선언에 접근할 수 있다.
lexicalEnvironment = {
helloWorld: < func >
}
자바스크립트 엔진이 helloWorld() 호출을 접하게되면, 렉시컬 환경 내부를 살펴보고, 함수를 찾은 후에, 실행 시킬수 있게 된다.
=> 하지만 함수 표현식은 var, let, const로 선언하기 때문에 호이스팅되어도 undefiend 또는 uninitialized 상태로 렉시컬 환경에 저장될 것이기 때문에 에러가 발생할 것이다.