[JavaScript 13] 실행 컨텍스트

김헤일리·2022년 11월 25일
0

JavaScript

목록 보기
14/20
  • 실행 컨텍스트 (Execution Context)는 자바스크립트가 실행되고 연산되는 범위를 의미한다.
  • 실행 가능한 코드들이 실행되기 위해 필요한 환경이자 컨테이너의 개념이라고 생각하면 된다.
  • 출처:

1. 실행 컨텍스트 2 step 정리

  1. JavaScript 엔진은 코드가 실행될 때 "Call Stack" 이라는 컨테이너에 전역 실행 컨텍스트를 담는다.

    • 이때 실행 컨텍스트에는 Record와 Outer이 실행된다.
      • Recor는 호이스팅 실행 시 먼저 읽은 선언문들이 저장되는 공간
      • Outer는 실행 컨텍스트의 담을 넘나드는 기법?이라고 할 수 있다 (안 확실함)
  2. 전역에서 함수가 호출될 경우, 해당 함수의 실행 컨텍스트를 전역 실행 컨텍스트 위에 쌓는다. (Stack)

    • 전역에서 코드가 실행되다가 depth (함수)를 발견하면 해당 depth로 다이브 한다. (함수가 끝날때까지)

    • 그래서 Call Stack에선 가장 최근에 실행된 컨텍스트가 실행된다.

      • 전역 컨텍스트가 가장 아래에, 전역 실행 중 depth 1 더 들어간 함수 컨텍스트를 그 위에
      • 만약 1 depth 함수 내에서 함수가 한번 더 호출되면 depth 2 함수의 실행 컨텍스트가 더 위에 쌓이기 때문.
      • depth n의 함수가 종료되면, Call Stack에서 사라지고 다시 depth (n-1) 함수로 돌아간다.
      • 스택이 하나씩 사라지다가 최종적으론 전역 컨텍스트까지 Call Stack에서 사라지면 코드가 끝나는 것.


2. Record - 호이스팅

  • 위에서 말 했듯, 자바스크립트 엔진은 "전역 실행 컨텍스트"를 먼저 "Call Stack"에 쌓는다.
    • 이때 코드 내에서 선언됐던 선언문 (var, let, const, func 등)을 먼저 확인한다.

    • 그리고 이 선언문을 어딘가에 기록하는데, 이 기록되는 공간이 바로 "Environment Record"다.

      • 환경 레코드는 식별자와 식별자에 연결된 (binding) 값을 기록한다.

호이스팅은 크게 두 가지 종류가 있다.

  1. 변수 호이스팅:
  • var 👉 전역 실행 컨텍스트에 일단 선언된 식별자를 기록하고, var로 선언된 경우, 식별자의 값을 "undefined"로 초기화 해둔다.
console.log(tvChannel);
// 2. 선언문을 살펴본 후 처음 만나는 함수일 때, 해당 함수를 먼저 실행한다. 
//이때 JS는 식별자가 있다는 사실과 해당 식별자와 바인딩된 값이 undefined로 환경 레코드에 저장되어있기 때문에, "undefined"라는 값을 참조하여 출력한다. 
//= 실행단계 (Execution Phase) 

var tvChannel = "Netflix";  
// 1. 전역 실행 컨텍스트가 처음 call stack에 쌓일때, 환경 레코드에 변수 식별자 tvChnnel의 값이 초기화 (undefined) 상태로 저장된다. 
//= 생성단계 (Creation Phase)

// 3. 이때 2번의 console.log() 함수가 종료되면, 자바스크립트는 다시 위에서부터 아래로 읽는다.
// 이때 선언되어있던 식별자의 값을 정확히 확인하고 전역 실행 컨텍스트의 환경 레코드에 값을 업데이트 한다.
// "undefined" => "Netflix"

console.log(tvChannel);
// 4. 다시 2번 단계와 동일한 함수가 선언되었을 때, 자바스크립트는 환경 레코드를 다시 참조한다.
// 이때 이미 3번 단계에서 변수 tvChannel의 값을 업데이트 했기 때문에, 해당 함수 실행시엔 "Neflix"라는 값이 출력된다.
  • let, const 👉 var로 기록한 경우와는 다르게, 환경 변수에 식별자의 값을 초기화 하지 않는다.

    • 에러 없이 작동되었던 var의 경우와는 다르게, let, const로 선언된 식별자는 식별자만 기록한다.
    • 그렇기 때문에 선언문 보다 위해서 호출되었을 때, 값을 찾을 수 없어 Reference Error 가 발생한다.
    • 변수의 값을 선언하는 두번째 라인에 와서야 비로소 변수 tvChannel에 값을 할당하고, 그 값을 3번째 라인인 console.log()에서 출력할 수 있는 것이다.
      • 이렇게 참조 에러가 생기는 특정 구간은 TDZ (Temporal Dead Zone) 이라고 한다.

  1. 함수 호이스팅:
  • 함수 표현식 👉 var, const 같은 키워드에 함수를 담는 방식.
    • var 키워드에 담긴 함수의 경우, 선언 단계에서 값이 "undefined"로 지정되어 있기 때문에, 선언문보다 호출이 먼저 일어날 경우 함수의 값이 "undefined"로 인식되어 Type Error가 일어난다.
    • const 키워드에 함수를 담은 경우, 선언 단계에 값이 지정되지 않아서 선언문보다 호출이 먼저 일어날 경우 값을 찾을 수 없기 때문에 "Reference Error" 가 발생한다.
    • 기본적으로 함수 표현식은 변수에 값을 담기 때문에, 함수 표현식의 호이스팅은 변수 호이스팅과 동일한 구조로 진행된다는 것을 알 수 있다.

  • 함수 선언식 👉 함수 자체에 이름을 부여하고 함수를 실행하는 방식.
study();

function study() {
    alert (`공부하자!`)
}
  • 함수 선언식으로 함수를 정의하면 환경 레코드에 완성된 함수 객체를 담는다.
  • 이미 완성된 상태로 담겨있기 때문에, 호출이 선언문보다 위에 있어도 선언식 함수는 오류없이 온전하게 실행될 수 있다.


3. Outer를 이용해서 Scope Chain 이해하기

  • outer의 정식 명칭은 "Outer Enviornment Reference (외부 환경 참조)" 다.
  • 환경 레코드와 아우터를 합쳐서 "Lexical Enviornment (정적 환경)" 이라고 한다.

let lamp = false;

console.log(lamp)
  • 위와 같은 환경에서 자바스크립트는 쉽게 변수 램프의 값을 콘솔에 출력할 수 있다.
  • 하지만:
let lamp = false;

function goTo2F () {
  let lamp = true;
  
  console.log(lamp);
}

goTo2F();
  • 선언식 함수가 생성되고 해당 함수 내부에 동일한 변수 이름이 선언되면 상황은 달라진다.
    • 1번 라인과 2번 라인에서 JS는 일단 변수 램프의 값과 선언식 함수를 환경 레코드에 기록한다.

    • 그 후 goTo2F라는 함수가 호출되었을 때, JS는 다시 함수의 선언문으로 이동하고, 새로운 실행 컨텍스트가 기존 전역 환경 컨텍스트 위에 새로 쌓인다.

    • 함수 내부엔 동일한 변수인 lamp가 존재하고, 함수 내부에 있는 lamp의 값은 'true'다.

      • 이제 JS는 환경 레코드에 lamp라는 동일한 식별자를 2개를 갖고 있고, 둘 중 어떤 값을 호출할지 결정해야 한다 => 식별자 결정
      • 그리고 이 단계에서 Outer가 활동한다.
      • 실행 컨텍스트들은 각 함수가 호출될 시 그 이전 컨텍스트 위에 쌓이는데, 이때 JS는 outer라는 이전 depth의 실행 컨텍스트로 이동하는 일종의 사다리를 둔다.
      • 그리고 이 사다리를 통해서 JS는 필요한 경우 식별자를 찾으러 컨텍스트를 넘나들 수 있다.
let lamp = false; // 1. 전역 실행 컨텍스트의 영역 (depth 1)

function goTo2F () { // 2. 선언식 함수 기록 / 4. 함수 실행 (depth 2 실행 컨텍스트 생성)
  let lamp = true;
  function goTo3F() { // 5. depth 2 함수 내부에서 선언식 함수 발견, 기록 / 7. 함수 실행 (depth 3 실행 컨텍스트 생성)
  	let pet = 'puppy'
    
    console.log(pet) // 8. "pet"을 depth 3 함수 실행 컨텍스트에서 찾을 수 있기 때문에 쉽게 "puppy"라는 값을 리턴. 
    console.log(lamp) // 9. depth 3 실행 컨텍스트에서 "lamp" 식별자를 찾을 수 없기 때문에 outer를 타고 depth 2로 이동한다.
   	//10. depth 2에서 "lamp"의 값을 찾았기 때문에 JS는 depth 1의 "lamp"까지 찾을 필요가 없기 때문에 해당 "lamp"의 값은 "true"로 호출되고 depth 3 함수는  종료되고, 실행 컨텍스트 삭제 (depth 2 함수를 이어서 실행)
  };
  goTo3F(); // 6. 새로운 함수의 호출 (depth 3)
  
  console.log(lamp); //10. depth 2 함수 실행 종료, 실행 컨텍스트 삭제. (전역 실행 컨텍스트를 마저 이어감)
}

goTo2F(); // 3. 새로운 함수의 호출 (depth 2)
  • 보통 이 순서로 코드가 실행되지만, outer를 사용하는 경우는 현재 depth (현재 실행중인 컨텍스트) 내부에서 찾을 수 없는 식별자의 값을 찾기 위해서다.

  • depth 3 함수에서 출력값이 console.log(pet)일 때, JS는 무리 없이 "puppy"라는 값을 찾을 수 있다.

  • 만약 여기서 console의 매개변수가 console.log(corona) 였다면, JS는 depth 3 함수에서 해당 식별자를 찾을 수 없기 때문에 outer라는 사다리를 타고 이전 depth의 실행 컨텍스트로 이동한다.

    • 현 예시의 경우, depth 2 함수의 실행 컨텍스트로 이동을 한다.

    • 이동했을 때 식별자를 찾을 수 있다면 해당 식별자의 값을 depth 3에 사용된 식별자의 자리에 할당해서 문제를 해결할 것이다.

    • 하지만 이전 depth의 실행 컨텍스트에서도 원하는 식별자를 찾을 수 없다면, JS는 계속해서 outer를 통해 현재 실행중인 컨텍스트의 바깥 환경 (lexical environment)을 depth별로 돌면서 값을 찾는다.

      • 이렇게 실행 컨텍스트들을 돌아다니면서 식별자를 찾아다니는 과정을 Scope Chain이라고 한다.
    • 전역 실행 컨텍스트까지 이동해서도 참조하고자 하는 식별자의 값을 찾을 수 없을 때 JS는 해당 식별자가 존재하지 않음을 깨닫고 Reference Error으로 결론짓는다.

  • 그리고 console.log(lamp)의 경우, depth 3 함수의 실행 컨텍스트에서 "lamp"를 찾을 수 없기 때문에 outer를 타고 depth 2 함수의 실행 컨텍스트로 이동한다.

    • 이동한 이전 depth의 실행 컨텍스트에서 원하는 식별자를 찾을 경우, 찾는 과정을 중단하고 현재 있는 컨텍스트에 있는 식별자의 값을 이후 depth로 끌고가서 사용한다.
    • 이때 더 상위 실행 컨텍스트에 있는 식별자의 값을 JS가 접근하지 않았기 떄문에, 이런 현상을 "variable shadowing"이라고 한다.

  • 만약 식별자를 찾기 위해 별도의 메모리를 돌아다니면서 JS 엔진이 진행됐다면, 효율성은 지금보다 훨씬 떨어졌을 것이다.
  • 실행 컨텍스트라는 개념이 생기고서부터 JS가 실행되는 과정이 훨씬 효율적으로 변했기 때문에, 실행 컨텍스트는 식별자 결정을 더욱 효율적으로 하기 위한 수단이라는 것을 알 수 있다.
    • 실행 컨텍스트는 식별자 결정을 하는 것에 필요한 정보를 한 곳에 모아두는 객체인 것이다.
profile
공부하느라 녹는 중... 밖에 안 나가서 버섯 피는 중... 🍄

0개의 댓글