JS의 코드 실행 메커니즘
- #Frontend
- #JavaScript
이 메모에서는 JS의 코드 실행 매커니즘을 정리한다.
JS의 코드 실행 과정
- 컴파일 단계 : 코드를 훑으며 모든 변수와 함수 선언을 찾아 메모리에 할당한다.
- 실행 단계 : 실제 코드를 한 줄 씩 실행한다.
단, C/Java처럼 실행 전 전체 코드를 한 번에 컴파일하는 것과 다르다. JS는 스코프 단위로 컴파일과 실행을 반복한다. 전역 코드를 컴파일 후 실행하고, 함수가 호출되면 그 함수 바디를 다시 컴파일 후 실행하는 방식이다.
변수 선언과 할당 과정
JS에서 변수를 선언하면 선언, 초기화, 할당 과정을 거친다.
- 선언 단계(Declaration) : 변수 이름을 실행 컨텍스트의 변수 객체에 등록하여 자바스크립트에게 변수의 존재를 알린다. 이 과정에서 변수를 관리하기 위한 메모리 공간이 확보된다.
- 초기화 단계(Initialization) : 등록된 변수에 실제 값을 담기 위한 메모리 공간을 할당하고 그곳에 undefined를 할당하여 초기화한다. 이 때부터 변수에 접근이 가능하다.
- 할당 단계(Assignment) : 사용자가 소스 코드에서
=연산자를 통해 지정한 실제 값을 해당 변수에 저장한다.
var 변수의 경우 선언과 초기화가 동시에 발생하지만, let, const의 경우 선언과 초기화 단계가 분리된다.
호이스팅
JS 엔진이 코드를 실행하기 전, 을 말한다. 호이스팅은 변수 선언 방식이나 함수의 선언 방식에 따라 다르게 동작한다.
var과 let과 const
var 변수는 선언 단계와 초기화 단계가 동시에 일어난다. 따라서 var 변수가 컴파일 단계에서 메모리에 등록될 때(선언 단계) undefined로 초기화가 동시에 일어나 변수의 선언 위치보다 상위에서 접근이 가능하다.
var은 함수 실행 컨텍스트를 기준으로 호이스팅되기 때문에 if, for과 같은 블록 내부에 선언되더라도 이를 무시하고 가장 가까운 함수나 전역 스코프 최상단으로 끌어올려진다.
반면 let과 const는 선언 단계와 초기화 단계가 분리되어 있다. 선언은 컴파일 단계에서 되지만, 초기화는 실행 단계에서 해당 변수가 선언된 라인에 도달해야 수행된다. 이 때문에 let과 const도 호이스팅은 발생하나 초기화가 되어있지 않아 접근이 불가능하고 Reference Error가 발생한다.
let과 const는 블록 스코프를 기준으로 호이스팅된다. 따라서 자신이 선언된 블록({}) 블록의 최상단까지만 호이스팅된다.
이 경우 변수가 선언은 되었지만 초기화가 되기 전까지 변수를 호출할 수 없는 구간이 발생하는데 이를 TDZ(Temporal Dead Zone)이라고 부른다.
함수 선언문과 표현식
함수 선언문은 function 키워드를 사용해서 함수를 직접 선언한 경우를 말하고, 함수 표현식은 익명 함수를 변수에 할당하는 방식을 말한다.
함수 선언문의 경우 컴파일 단계에서 식별자 등록과 함수 객체의 생성이 함께 일어난다. 따라서 선언문 자체가 호이스팅 되는 것처럼 보여 실제 선언 위치보다 상단에서 함수를 호출해도 정상 동작한다.
반면, 함수 표현식은 이다. 식별자는 컴파일 단계에서 등록되지만, 함수 객체는 실행 단계에서 해당 라인에 도달해야 생성되기 때문이다. 따라서 변수의 호이스팅 절차를 그대로 따른다.
var로 선언한 경우, 변수 이름이 호이스팅되고undefined로 초기화된다. 함수를 실행하려고하면undefined를 실행하는 것이 되어TypeError가 발생한다.let,const로 선언한 경우, 변수가 호이스팅되더라도 TDZ의 영향을 받아 선언 이전에 접근 시Reference Error가 발생한다.
렉시컬 스코프와 렉시컬 환경
렉시컬 스코프 (Lexical Scope)
코드가 어디에 작성(선언)되었는지에 따라 상위 스코프가 결정되는 규칙(개념)을 말한다. 즉, 코드 혹은 함수가 어디에서 호출되었는지가 아니라 어디에서 정의되었는지에 따라 변수 접근 범위가 결정된다. 이 스코프는 코드가 작성되는 시점에서 이미 결정되며, 실행 중에 변경되지 않는다.
렉시컬 환경 (Lexical Environment)
렉시컬 환경은 실행 컨텍스트 내부(런타임)에서 변수 바인딩과 스코프 연결을 관리하는 실제 저장 구조이다. 렉시컬 환경은 아래 2가지 요소로 구성된다
- 환경 레코드(Environment Record): 실제 변수와 함수, 매개변수 등을 저장한다.
- 외부 환경 참조(Outer Environment Reference) : 상위 스코프의 렉시컬 환경을 가리켜 식별자 탐색을 가능하게 한다.
렉시컬 환경은 스코프 진입 시에 생성된다. let, const가 호이스팅되는 이유도 블록 스코프에 진입하면 블록 스코프를 위한 렉시컬 환경이 생성되고, 이 렉시컬 환경의 환경 레코드에 변수가 등록되기 때문이다.
실행 컨텍스트
실행 컨텍스트는 자바스크립트 엔진이 코드를 실행하기 위한 정보를 담고 있는 실행 환경 단위이다. 코드가 실행될 때(런타임)마다 해당 코드의 실행에 필요한 정보들을 포함하는 실행 컨텍스트가 생성된다.
실행 컨텍스트에는 크게 2가지 종류가 있다.
- 전역 실행 컨텍스트(Global Execution Context): 자바스크립트 파일이 처음 실행될 때 생성되는 단 하나의 실행 컨텍스트. 프로그램이 종료될 때까지 유지되며, 전역에 선언된 변수와 함수를 포함한다.
- 함수 실행 컨텍스트(Function Execution Context): 함수가 호출될 때마다 독립적으로 생성되는 컨텍스트. 해당 함수 내부의 선언문들은 그 컨텍스트 안에서만 유효하며, 함수의 실행이 끝나면 컨텍스트도 함께 사라진다.
실행 컨텍스트는 크게 렉시컬 환경, 변수 환경, this 바인딩이라는 3가지 요소로 구성된다.
- 렉시컬 환경(Lexical Environment) : 현재 활성 스코프의 변수와 함수 선언을 저장하고, 외부 환경 참조를 통해 상위 스코프와 연결되는 구조. 블록 진입 시 새로 생성되어 교체된다.
- 변수 환경(Variable Environment) : 실행 컨텍스트 생성 시 렉시컬 환경과 같은 환경 레코드를 가리키며,
var선언을 저장한다. 블록 진입 시에도 교체되지 않고 함수 실행 내내 고정되기 때문에var이 블록과 무관하게 함수 스코프에 유지될 수 있다. this바인딩 : 현재 실행 컨텍스트에서this가 어떤 값을 가리킬지 결정된 값. ES5까지는 별도의 슬롯이었으나, ES6+에서는 렉시컬 환경의 환경 레코드 안의[[ThisValue]]슬롯으로 포함되었다.
실행 컨텍스트는 콜 스택에 쌓여 관리된다. 전역 컨텍스트가 가장 먼저 쌓이고 위에 함수 컨텍스트들이 쌓이고 나가고를 반복한다.
클로저
함수가 선언될 때의 스코프를 기억하고, 함수가 생성된 이후에도 그 렉시컬 환경에 접근할 수 있는 기능을 말한다. 이는 JS에서 함수가 일급 객체라는 특성과 함수가 작성된 위치에 따라 상위 스코프가 결정되는 렉시컬 스코프라는 특성에서 기인한다.
함수가 일급 객체이므로 내부 함수를 외부 함수의 결과물로 밖으로 내보낼 수 있다. 따라서 외부 함수의 렉시컬 환경을 기억하는 클로저가 가능하다. 만약 함수가 일급 객체가 아니라면 외부 함수가 종료됨과 동시에 내부 함수도 같이 사라져 클로저라는 개념이 성립하지 않는다.
동작 원리
- 외부 함수가 실행되면 함수 실행 컨텍스트와 그 안에 렉시컬 환경이 생성된다.
- 내부 함수 객체가 생성되면서, 현재 존재하는 외부 함수의 렉시컬 환경을 기억한다.
- 외부함수의 실행이 끝나면 해당 실행 컨텍스트는 콜 스택에서 제거되지만, 내부 함수가 외부 함수의 렉시컬 환경을 여전히 참조하고 있다면, JS의 가비지 컬렉터는 이 환경을 메모리에서 지우지 않고 유지한다.
- 이를 통해 외부 함수가 종료된 후에도 내부 함수를 호출하면, 내부 함수는 자신이 기억하고 있는 외부 환경의 변수에 접근할 수 있게 된다.
활용
- 데이터 은닉 : 외부에서 직접 접근할 수 없는 비공개 변수를 만들 수 있다.
- 비동기 작업의 상태 유지 :
setTimeout이나 API 호출 같은 비동기 작업 시, 콜백 함수가 실행되는 시점에도 이전에 정의된 환경의 변수 값을 잃지 않고 참조할 수 있다. - 모듈 패턴 : 특정 기능을 캡슐화하고 외부에는 필요한 인터페이스만 선택적으로 노출하여 코드의 응집력과 유지보수성을 높인다.
this 바인딩
JS에서 this는 함수가 어떻게 호출되었느냐에 따라 참조하는 객체가 달라진다. 렉시컬 환경이 함수가 어디서 '선언'되었는지에 따라 바뀌는 정적인 환경(컴파일 타임)인 반면, this는 함수가 어디에서 '호출'되었는지에 따라 바뀌는 동적 환경(런타임)이다.
this 바인딩 규칙은 6가지 종류가 있다.
전역 호출
전역 공간에서 함수를 호출하면 this는 전역 객체를 가리킨다. 단, 엄격 모드(strict mode)에서는 undefined가 된다.
메서드 호출
객체의 메서드로 호출되면, 호출한 객체가 할당된다.
생성자 함수 및 클래스
new를 통해 생성되는 객체를 가리킨다.
명시적 바인딩
call, apply, bind와 같은 함수를 사용하여 개발자가 this로 가리킬 객체를 직접 지정할 수 있다.
엄격 모드가 아닐 때는 null이나 undefined 같은 비정상적인 값을 넘길 때는 전역 객체로 치환되나, 엄격 모드에서는 전달한 그대로 지정된다.
화살표 함수
자체적인 this 바인딩을 가지지 않는다. 따라서 외부 렉시컬 환경을 타고 올라가 상위 스코프의 this를 참조해 사용한다. ES6+에서 this가 렉시컬 환경의 환경 레코드의 [[ThisValue]] 슬롯으로 포함되었고 이에 따라 상위 렉시컬 환경을 탐색하며 this 값을 가져올 수 있게 되었다.
DOM 이벤트 핸들러
이벤트를 발생시킨 요소를 참조한다.