조각조각 - 자바스크립트 실행 과정 살펴보기 총정리

eocode·2024년 3월 6일
0
post-thumbnail

전체 과정 살펴보기

이번 글에선 코드의 전체적인 실행 과정을 보면서 전역 실행 컨텍스트, 함수 실행 컨텍스, 호이스팅, 스코프, 어휘적 환경, 환경 레코드, 외부 어휘적 환경 참조 등 이전에 공부 했던 개념을 복습해보겠습니다.

console.log(v);
console.log(l);
console.log(c);

var v;
let l;
const c='const';

varFn('var');
constFn('const');
letFn('let');

console.log(varFn);
console.log(constFn);
console.log(letFn);

var varFn = (p) => console.log(p);
const constFn = (p) => console.log(p);
let letFn = (p) => console.log(p);


const fn1 = normalOuter1();
fn1();
fn1();
fn1();

const fn2 = normalOuter2();
fn2();
fn2();
fn2();

const fn3 = arrowOuter();
fn3();
fn3();
fn3();



function normalOuter1() {
	let free = "자유 변수";
	function normalInner() {
		free=free+'!';
		console.log(free);
	}
	return normalInner;
}

function normalOuter2() {
	let free = "자유 변수";
	
	return function () {
		free=free+'!';
		console.log(free);
	};
}

const arrowOuter = () => {
	let free = "자유 변수";
	const arrowInner = () => {
		free=free+'!';
		console.log(free);
	}
	return arrowInner;
}

코드가 실행되면 자바스크립트 엔진은 코드를 진짜로 실행하기전 평가 단계를 거친 후 실행 단계를 진행합니다. 이때 평가 단계 선언 코드들만 실행하여 실행 컨텍스트 즉 실행 가능한 환경을 생성합니다.

전역 코드 평가 단계

code1

전체 코드에서 전역 코드의 선언 코드만 실행됩니다. 주황색으로 칠해진 코드가 평가 단계에서 실행되는 코드입니다. 자바 스크립트 엔진은 이 선언문을 실행해 전역 실행 컨텍스트의 어휘적 환경에 값을 채워나갑니다.

부분 코드 1

var v;
let l;
const c='const'

변수 선언 코드가 가장 먼저 실행됩니다. 평가 단계에서 변수 타입에 상관없이 모두 식별자가 환경 레코드에 등록됩니다. 이때 var 변수는 전역 객체 widnow의 프로퍼티가 되어 객체 환경 레코드에서 관리됩니다. let, const 변수 식별자는 전역 선언전 환경 레코드에 등록됩니다.

식별자는 등록되었지만 let, const 타입 변수는 평가 가정에서 값을 할당받지 못합니다. 즉 환경 레코드에 값이 등록되지 않습니다. 하지만 var 타입 변수의 경우 undefined로 암묵적으로 값이 할당됩니다.

  • const, let 타입 변수는 자바스클비트 엔진 실행 단계에서 할당을 받습니다.
    이때 할당 받은 값이 환경 레코드에 등록됩니다.
  • var 타입 변수는 자바스클비트 엔진 평가 단계에서 암묵적으로 undefined 값을 할당 받습니다. 이 값이 환경 레코드에 등록됩니다.
context

부분 코드 2

var varFn = (p) => console.log(p);
const constFn = (p) => console.log(p);
let letFn = (p) => console.log(p);

다음 평가 과정에서 함수 표현식 코드들이 실행됩니다. 위 변수 선언과 마찬가지로 함수 식별자들이 환경 레코드에 등록됩니다. 하지만 여전히 값은 환경 레코드에 등록되지 않습니다. 단지 var 타입 변수만 암묵적으로 undefined 값을 할당 받습니다.

위와 마찬가지로 var 타입은 전역 객체 window의 프로퍼티가 되어 객체 환경 레코드에서 관리되며 const, let 타입 변수는 선언적 환경 레코드에서 관리됩니다.

부분 코드 3

function normalOuter1() {
	let free = "자유 변수";
	function normalInner() {
		free=free+'!';
		console.log(free);
	}
	return normalInner;
}

function normalOuter2() {
	let free = "자유 변수";
	
	return function () {
		free=free+'!';
		console.log(free);
	};
}

const arrowOuter = () => {
	let free = "자유 변수";
	const arrowInner = () => {
		free=free+'!';
		console.log(free);
	}
	return arrowInner;
}

이어서 함수 선언문과 함수 표현식 코드가 등장합니다. 함수 식별자들이 환경 레코드에 등록됩니다. 변수 선언과 다르게 함수 선언문으로 작성된 함수 객체는 평가 단계에서 바로 환경 레코드에 등록됩니다. 하지만 함수 표현시으로 작성된 함수 객체는 평가 단계에서 환경 레코드에 등록되지 않습니다. 따라서 함수 선언문으로 작성된 함수는 선언 코드 이전에 이미 환경 레코드에 함수 객체도 담겨있어서 호출이 가능하지만 함수 표현식으로 작성된 함수는 환경 레코드에 함수 객체가 등록되어 있지 않아 참조도 실행도 불가능 합니다.

부분 코드 4

const fn1 = normalOuter1();
/*fn1();
fn1();
fn1();*/

const fn2 = normalOuter2();
/*fn2();
fn2();
fn2();*/

const fn3 = arrowOuter();
/*fn3();
fn3();
fn3();*/

다음으로 클로저 함수를 변수에 할당하는 함수 표현식 코드가 실행됩니다. 위 함수 표현식 설명과 같이 동작합니다. 함수 식별자만 환경 레코드에 등록되고 값(함수 객체)는 환경 레코드에 등록되지 않습니다. var 타입 변수가 아니기 때문에 암묵적으로 undefined 값을 할당 받지도 않습니다. 그냥 아무 값도 가지지 않고 식별자만 등록된 상태입니다.

여기까지 자바스크립트 엔진의 전역 코드 평가 단계가 완료되었습니다.

전역 코드 실행 단계

전역 코드 평가 완료 후 전역 코드 실행 단계가 이어서 진행됩니다. 이제 선언 코드를 제외한 코드들이 실행됩니다. 녹색으로 칠해진 코드가 실행될 코드입니다.

선언문 처럼 보이지만 실행되는 코드들은 선언 과정만 거치고 할당 과정을 마치지 못한 상태로 이번 실행 단계에 처리됩니다.

code2

부분 코드 1

console.log(v); //undefined
console.log(l); //참조 에러
console.log(c); //참조 에러

var v;
let l;
const c='const';

변수가 선언되기 이전에 console.log로 출력하려는 코드입니다.
과연 제대로 출력이 나올까요? 변수 선언전에 변수를 참조해서 출력하면 변수 타입에 따라 undefined가 나오거나 참조 에러가 발생합니다.

위 평가 과정에서 모든 변수 식별자가 환경 레코드에 등록되었지만 값은 var 타입 변수만 암묵적으로 undefined가 등록되었습니다. 따라서 변수 선언 코드 이전에 v 타입 변수를 참조하면 환경 레코드에서 undefined 값을 얻어올 수 있습니다. 하지만 const, let 타입 변수는 아무 값도 등록되어있지 않기 때문에 출력하면 참조 에러가 발생합니다.

부분 코드 2

varFn('var'); // 실행 X
constFn('const'); // 실행 X
letFn('let'); // 실행 X

console.log(varFn); //undefined 출력
console.log(constFn); //참조 에러
console.log(letFn); //참조 에러

var varFn = (p) => console.log(p);
const constFn = (p) => console.log(p);
let letFn = (p) => console.log(p);

이어서 함수를 호출하는 코드와 함수 자체를 출력하는 코드가 이어집니다. 그 이후에야 해당하는 함수가 함수 표현식으로 선언됩니다. 즉 함수가 선언되기 이전에 호출하고 참조한 코드입니다.

함수 표현식은 변수 선언과 유사합니다. 변수 타입에 따라 다른 양상을 보입니다. 위에서 보았듯이 평가 과정을 거치며 var 타입 변수는 undefined의 값을 암묵적으로 가지게 되고 const, let 타입 변수는 아무 값도 가지게 됩니다.

함수 표현식도 동일하게 동작합니다. varFn 함수는 평가 단계를 거치며 환경 레코드에 undefined 값이 등록되지만 constFn 함수와 letFn 함수는 환경 레코드에 아무값도 등록되지 않습니다. undefined 값을 가져도 함수가 실행 가능한것은 아니기 때문에 결과적으로 세함수 모두 실행은 불가능합니다. 대신 참조 자체도 안되는 constFn, letFn과 다르게 varFn 함수를 출력하면 undefined가 출력됩니다.

  • var 타입 변수 할당 함수 표현식
    • 평가 단계에서 식별자 호이스팅
    • 평가 단계에서 undefined 암묵적 할당
    • 함수 선언 이전 출력 가능 (undefined 출력)
    • 함수 선언 이전 홈수 호출 불가능
  • cosnt ,let 타입 변수 할당 함수 표현식
    • 평가 단계에서 식별자 호이스팅
    • 평가 단계에서 할당 X
    • 함수 선언 이전 출력 불가능 (참조 에러)
    • 함수 선언 이전 홈수 호출 불가능

부분 코드 3

const fn1 = normalOuter1();
fn1();
fn1();
fn1();

이어서 식별자만 등록되어 있던 fn1, fn2, fn3에 함수가 할당됩니다. fn1 변수 할당 과정인 const fn1 = normalOuter1(); 코드에서 함수 normalOuter1가 실행되어야 합니다. 함수 normalOuter1 식별자로 환경 레코드를 확인합니다. 환경 레코드 안에 함수의 값이 함수 객체로 이미 등록되어있습니다. 바로 normalOuter1 함수가 함수 선언문으로 생성되어 전역 코드 평가 단계에서 함수 객체가 환경 레코드에 등록되었기 때문입니다. 함수가 실행 가능하기 때문에 자바스크립트 엔진이 함수 평가 단계를 진행합니다.

normalOuter1 함수 코드 평가 단계

function normalOuter1() {
	let free = "자유 변수";
	function normalInner() {
		free=free+'!';
		console.log(free);
	}
	return normalInner;
}

자바스크립트 엔진이 함수를 평가하며 함수 실행 컨텍스트가 생성합니다. 동시에 어휘적 환경에 값이 채워지기 시작합니다.

  1. let free = "자유 변수"; 코드가 평가 되며 free 변수 식별자가 환경 레코드에 등록됩니다. let 타입이기 때문에 값은 환경 레코드에 등록되지 않습니다.
  2. normalInner 함수 선언문이 평가 됩니다. 함수 식별자가 환경 레코드에 등록되고 함수 선언식 형태이므로 함수 객체도 환경 레코드에 등록됩니다.

normalOuter1 함수 코드 실행 단계

normalOuter1 함수 평가 단계가 완료되고 함수 코드가 실행됩니다.

  1. let free = "자유 변수"; 코드가 실행되어 변수 free에 "자유 변수" 값이 할당 됩니다. 따라서 함수 실행 컨텍스트의 선언적 환경 레코드에 "자유 변수"가 등록됩니다.
  2. normalInner 함수가 리턴됩니다. 환경 레코드에 등록된 normalInner 함수의 함수 객체가 리턴됩니다.

전역 코드로 복귀 후 실행 단계 이어서 진행

부분 코드 1

const fn1 = normalOuter1();
fn1(); //자유변수!
fn1(); //자유변수!!
fn1(); //자유변수!!!

const fn1 = normalOuter1(); 코드가 이어서 진행됩니다. normalOuter1 함수가 함수를 반환하고 있어 변수 fn1에 함수가 할당됩니다. 실행 단계 중이므로 바로 환경 레코드에 해당 함수 객체가 등록됩니다. 따라서 이어서 나오는 fn1(); 코드에서 오류가 발생되지 않고 함수가 실행됩니다.

클로저

이 함수가 실행되는 과정에서 클로저를 확인할 수 있습니다!!!

먼저 fn1 함수가 무엇인지 보겠습니다. fn1 함수는 normalOuter1 함수의 중첩함수로 아래와 같습니다.

function normalInner() {
	free=free+'!';
	console.log(free);
}

이 중첩함수는 외부 스코프 즉 normalOuter1 함수 스코프에서 선언된 변수 free에 접근하고 있습니다. 사실 normalOuter1 함수가 실행되며 중첩 함수를 리턴하면서 normalOuter1 함수 실행 컨텍스트는 콜스택에서 제거되어 변수 free에 접근 불가능해야합니다. 그런데 결과를 보면 free 변수에 계속 접근 가능한 것으로 보입니다.

이 이유는 중첩함수의 어휘적 환경이 외부 함수인 normalOuter1이 어휘적 환경을 참조하고 있어 가비지 컬렉터가 normalOuter1의 어휘적 환경을 지우지 않기 때문입니다.

따라서 fn2(); 코드를 반복해서 사용해도 모두 변수 free에 접근 가능합니다. 처음 함수를 실행했을때 free의 값인 "자유 변수"에 느낌표가 추가되어 "자유 변수!"가 되었습니다. 두번째로 실행했을때 변경된 free 값인 "자유 변수!"에 느낌표가 붙습니다. 이후 반복 과정은 동일하게 동작합니다.

부분 코드 2

const fn2 = normalOuter2();
fn2(); //자유변수!
fn2(); //자유변수!!
fn2(); //자유변수!!!

const fn3 = arrowOuter();
fn3(); //  참조 에러
fn3(); // 참조 에러
fn3(); // 참조 에러

이어서 const fn2 = normalOuter2(); 코드가 실행됩니다. normalOuter1 함수도 함수 선언문으로 정의되어있어 함서 선언 이전 호출이 가능합니다. 따라서 위 const fn1 = normalOuter1(); 코드와 동일하게 동작합니다. 단지 약간 다른 부분은 익명함수로 선언된 중첩함수를 사용한다는 부분입니다.

fn2(); //자유변수!
fn2(); //자유변수!!
fn2(); //자유변수!!!

동작은 모두 동일하기 때문에 위와 돌일한 출력 결과를 가집니다.

이어서 const fn3 = arrowOuter(); 코드가 실행됩니다. 하지만 여기서 에러가 발생합니다. 함수 호출이 불가능 합니다. 왜냐하면 arrowOuter 함수가 화살표 함수로 정의되어있기 때문입니다. 함수가 화살표 함수로 정의된 경우 함수 선언 코드 이전인 현재 위치에선 함수의 식별자만 환경 레코드에 존재하고 함수 객체는 존재하지 않습니다. 따라서 실행이 불가능합니다.

fn3(); //  참조 에러
fn3(); // 참조 에러
fn3(); // 참조 에러

이어진 코드에서도 에러가 발생학 전체 코드가 종료됩니다.

profile
프론트엔드 개발자

0개의 댓글