Javascript 실행 컨텍스트와 클로저 (1)

Benzy·2023년 8월 14일
0

JavaScript

목록 보기
4/5

서론


import { useState, useEffect, useRef } from "react";
import "./styles.css";

export default function App() {
  const [number, setNumber] = useState(0);
  const appRef = useRef();
  const testTag = appRef.current?.querySelector("#test-message");

  const handleClickEvent = (event) => {
    setNumber((prevNumber) => prevNumber + 1);
    console.log(
      "querySelector in event",
      appRef.current.querySelector("#test-message")
    );
    console.log("testTag in event", testTag);
  };

  useEffect(() => {
    appRef.current
      .querySelector(".button")
      .addEventListener("click", (event) => {
        handleClickEvent(event);
      });
  }, []);

  return (
    <div className="App" ref={appRef}>
      <button className="button">click me!</button>
      <p id="test-number">{number}</p>
      <p id="test-message">Test</p>
    </div>
  );
}

개발을 하던 도중 eventListener 콜백 함수에서 전역 함수의 초깃값을 제대로 불러오지 못하는 문제를 마주쳤다. 위의 코드는 당시의 상황과 비슷하게 재현한 코드이다.

eventListener의 콜백 함수인 handleClickEvent 함수의 console.log 값을 예상해봤을 때, 두 코드의 결괏값이 모두 <p id=”test-message”>Test</p>일거라 생각했다. 하지만 실제로 코드를 작동 시켜 보았을 때, 첫 번째 값은 querySelecotr in event <p id=”test-message”>Test</p> , 두 번째 값은 testTag in event undefined가 찍히는 것을 확인할 수 있다.

원래는 리액트 라이프 사이클과 연관된 문제인 줄 알고 삽질을 했고..🥲 비슷한 예제를 만들어 챗GPT에게 물어보니 JavaScript의 클로저와 연관이 있다는 답변을 받았다.

아래는 챗GPT 답변이다.

이러한 현상은 JavaScript의 클로저와 관련이 있습니다.
handleClickEvent 함수 내부에서 testTag를 참조하고 있는데,
이 함수는 클로저로서 useEffect 바깥에서 정의된 변수(testTag)에 접근하려고 합니다.
그런데 testTag 변수는 useRef로 생성된 appRef.current 프로퍼티를 참조하려고 하는 것이기 때문에,
컴포넌트가 처음 렌더링될 때 testTagundefined일 가능성이 있습니다.

이 답변을 듣고 실행 컨텍스트에 대해 몰랐을 때 든 생각은 이거였다.

처음 렌더링 될 때 undefined 였어도, 버튼 클릭 → 상태값 변경 후에 리렌더링 되면 undefined가 아니지 않나? 전역변수인데 왜 계속 undefined일까?

그래서 클로저에 대해서 검색해보았다.

클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.

면접 준비할 때 외웠던 대답.. 아직 클로저, 렉시컬 환경에 대해 전혀 모르는 상태라는 걸 깨달았다.

일단, 클로저를 알기 위해서는 Execution Context 실행 컨텍스트에 대해 이해를 하는 것이 좋다.




Execution Context (실행 컨텍스트)


실행 컨텍스트 (Execution Script)는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 자바스크립트의 동적 언어로서 성격을 가장 잘 파악할 수 있는 개념이다.
- 코어 자바스크립트

실행 컨텍스트는 자바스크립트 코드가 실행되는 환경이다.
함수가 실행되면 함수 실행에 해당되는 실행 컨텍스트가 생성되고, 자바스크립트 엔진에 있는 콜 스택에 쌓인다.
스택은 FILO (First In, Last Out) 구조이기 때문에 실행 순서가 보장되며, 콜스택 내부에 쌓인 실행 컨텍스트의 정보를 통해 실행 환경을 보장할 수 있다.


소스코드의 평가와 실행

자바스크립트 엔진은 소스코드를 소스코드의 평가소스코드의 실행 과정으로 나누어 처리한다.

소스코드 평과 과정에서 실행 컨텍스트를 생성하고, 생성된 변수, 함수의 식별자를 키로 실행 컨텍스트가 관리하는 스코프에 등록한다. 소스코드 평가 과정이 끝나면 선언문을 제외한 소스코드가 순차적으로 실행되기 시작한다. (런타임 환경) 실행 컨텍스트는 코드 실행에 필요한 정보를 제공하고, 실행이 끝나면 해당 코드의 실행 결과는 다시 실행 컨텍스트가 관리하는 스코프에 등록된다.


실행 컨텍스트 구성 요소

1. Variable Environment

실행 컨텍스트를 생성할 때, Variable Environment에 먼저 정보를 담고, 그대로 LexicalEnvironment에 복사해 사용한다. 즉, 선언 시점의 LexicalEnvironment의 스냅샷을 유지한다.

Environment Record (환경 레코드)

  • 스코프에 포함된 식별자를 등록하고 등록된 식별자에 바인딩된 값을 관리하는 저장소이다. 즉, 현재 컨텍스트 내부의 식별자 정보를 가진다.

Outer Lexical Environment Reference (외부 렉시컬 환경에 대한 참조)

  • 외부 렉시컬 환경에 대한 참조는 상위 스코프를 가리킨다.
  • 이때 상위 스코프란 외부 렉시컬 환경, 즉 해당 실행 컨텍스트를 생성한 소스코드를 포함하는 상위 코드의 렉시컬 환경을 말한다.

2. Lexical Environment

초기에는 Variable Environment와 같지만 변경 사항이 실시간으로 적용된다.
렉시컬 환경은 식별자와 식별자에 바인딩 된 값, 그리고 상위 스코프에 대한 참조를 기록하는 자료구조로 실행 컨텍스트를 구성하는 컴포넌트이다. 실행 컨텍스트 스택이 실행 순서를 관리한다면, 렉시컬 환경은 스코프와 식별자를 관리한다. 렉시컬 환경은 키와 값을 갖는 객체 형태의 스코프를 생성해서, 식별자를 키로 등록하고 식별자에 바인딩된 값을 관리한다. 즉, 렉시컬 환경은 스코프를 구분하여 식별자를 등록하고 관리하는 저장소 역할을 하는 렉시컬 스코프의 실체이다.

  • Environment Record
    함수 안의 코드가 실행되기 전에 현재 컨텍스트와 관련된 코드의 식별자 정보가 저장된다. 즉, 코드가 실행되기 전에 현재 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명 등을 모두 알게 된다.
    - 호이스팅 ?
    자바스크립트 엔진이 실행 컨텍스트를 구성할 때 Environment Record에 식별자의 정보를 수집한다.
    이러한 과정을 통해 엔진은 함수를 실행하기도 전에 해당 컨텍스트 내부의 변수명들을 이미 알고 있다.
    여기서 식별자들을 코드의 최상단으로 끌어올렸다는 호이스팅이라는 개념이 생겨난다.
    물리적으로 끌어올리는 것이 아닌,
    실행 컨텍스트 관점에서 이미 식별자들의 정보를 알고 있으니 식별자 정보를 수집하는 과정을 이해하기 쉬운 방법으로 나타낸 추상화된 가상 개념이다.
  • Outer Environment Reference (외부 렉시컬 환경에 대한 참조)
    상위 스코프를 가리킨다. 즉, 현재 Environment Record 보다 바깥에 있는 Environment Record를 참고한다는 뜻이며, 해당 실행 컨텍스트를 생성한 함수의 바깥 환경을 가리킨다. 따라서 자바스크립트 엔진이 현재 렉시컬 환경에서 변수를 찾을 수 없다면 외부 환경에서 찾는 것이다. 만약 상위 스코프에서 해당 식별자를 찾을 수 없다면 참조 에러(uncaught reference error)를 발생시킨다.

3. this binding

this 식별자가 바라봐야 할 대상 객체. 실행 컨텍스트가 활성될 때 this가 지정되지 않은 경우에는 전역 객체가 저장된다.


실행 컨텍스트의 생성과 작동 과정

예제

const str = '안녕';

function outer() {
	function inner() {
		const greeting = '하이';
		console.log(greeting);
		console.log(str);
	}
	inner();
}
outer();

console.log(str);
  1. 전역 컨텍스트가 생성된다. 전역 컨텍스트의 environmentRecord에 {str, outer} 식별자를 저장한다. 전역 컨텍스트는 가장 최상위 컨텍스트이므로 outerEnvironmentReference는 null이다.
  2. 전역 스코프에 있는 변수 str에 '안녕'을 outer에 함수를 할당한다.
  3. outer 함수를 호출한다. 전역 컨텍스트의 코드는 잠시 중단되고, outer 실행 컨텍스트가 활성화 된다.
  4. outer 실행 컨텍스트의 environmentRecord에 {inner} 식별자를 저장한다. outerEnvironmentReference에는 outer 함수가 선언될 당시의 Lexical Environment가 담긴다.
  5. outer 스코프에 inner 함수를 할당한다.
  6. inner 함수를 호출한다. 여기서 outer 실행 컨텍스트의 코드는 임시 중단되고, inner 실행 컨텍스트가 활성화된다.
  7. inner 실행 컨텍스트의 environmentRecord에 {greeting}식별자를 저장한다. outerEnvironmentReference엔 inner 함수가 선언될 당시의 Lexical Environment가 담긴다. inner 함수는 outer 함수에서 선언되었으므로, outer 함수의 Lexical Environment 즉, {outer {inner}}를 참조한다.
  8. environmentRecord에 있는 greeting을 찾아서 실행한다.
  9. 식별자 str에 접근하려고 한다. 이때, 자바스크립트 엔진은 활성화된 실행 컨텍스트의 Lexical Environment에 접근한다. 첫 요소의 enivironmentRecord에 str이 있는지 찾아보고, 없으면 outerEnvironmentReference에 있는 environmentRecord로 넘어가 검색한다. 예제에서는 전역 Lexical Environment에 str이 있으므로 '안녕'을 출력한다.
  10. inner 함수 실행이 종료된다. inner 실행 컨텍스트가 콜 스택에서 제거되고, outer 실행 컨텍스트가 다시 활성화된다.
  11. outer 함수 실행이 종료된다. outer 실행 컨텍스트가 콜 스택에서 제거되고, 전역 컨텍스트가 다시 활성화된다.
  12. 전역 컨텍스트의 environmentRecord에서 str을 검색해서 실행한다.
  13. 모든 코드의 실행이 종료되어 전역 컨텍스트가 콜 스택에서 제거되고 종료된다.


Reference

https://east-star.tistory.com/14
https://gamguma.dev/post/2022/04/js_execution_context
https://velog.io/@edie_ko/js-execution-context

profile
상호작용을 구현하는 개발자

0개의 댓글