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
프로퍼티를 참조하려고 하는 것이기 때문에,
컴포넌트가 처음 렌더링될 때testTag
는undefined
일 가능성이 있습니다.
이 답변을 듣고 실행 컨텍스트에 대해 몰랐을 때 든 생각은 이거였다.
처음 렌더링 될 때 undefined 였어도, 버튼 클릭 → 상태값 변경 후에 리렌더링 되면 undefined가 아니지 않나? 전역변수인데 왜 계속 undefined일까?
그래서 클로저에 대해서 검색해보았다.
클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.
면접 준비할 때 외웠던 대답.. 아직 클로저, 렉시컬 환경에 대해 전혀 모르는 상태라는 걸 깨달았다.
일단, 클로저를 알기 위해서는 Execution Context 실행 컨텍스트에 대해 이해를 하는 것이 좋다.
실행 컨텍스트 (Execution Script)는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 자바스크립트의 동적 언어로서 성격을 가장 잘 파악할 수 있는 개념이다.
- 코어 자바스크립트
실행 컨텍스트는 자바스크립트 코드가 실행되는 환경이다.
함수가 실행되면 함수 실행에 해당되는 실행 컨텍스트가 생성되고, 자바스크립트 엔진에 있는 콜 스택에 쌓인다.
스택은 FILO (First In, Last Out) 구조이기 때문에 실행 순서가 보장되며, 콜스택 내부에 쌓인 실행 컨텍스트의 정보를 통해 실행 환경을 보장할 수 있다.
자바스크립트 엔진은 소스코드를 소스코드의 평가와 소스코드의 실행 과정으로 나누어 처리한다.
소스코드 평과 과정에서 실행 컨텍스트를 생성하고, 생성된 변수, 함수의 식별자를 키로 실행 컨텍스트가 관리하는 스코프에 등록한다. 소스코드 평가 과정이 끝나면 선언문을 제외한 소스코드가 순차적으로 실행되기 시작한다. (런타임 환경) 실행 컨텍스트는 코드 실행에 필요한 정보를 제공하고, 실행이 끝나면 해당 코드의 실행 결과는 다시 실행 컨텍스트가 관리하는 스코프에 등록된다.
실행 컨텍스트를 생성할 때, Variable Environment에 먼저 정보를 담고, 그대로 LexicalEnvironment에 복사해 사용한다. 즉, 선언 시점의 LexicalEnvironment의 스냅샷을 유지한다.
초기에는 Variable Environment와 같지만 변경 사항이 실시간으로 적용된다.
렉시컬 환경은 식별자와 식별자에 바인딩 된 값, 그리고 상위 스코프에 대한 참조를 기록하는 자료구조로 실행 컨텍스트를 구성하는 컴포넌트이다. 실행 컨텍스트 스택이 실행 순서를 관리한다면, 렉시컬 환경은 스코프와 식별자를 관리한다. 렉시컬 환경은 키와 값을 갖는 객체 형태의 스코프를 생성해서, 식별자를 키로 등록하고 식별자에 바인딩된 값을 관리한다. 즉, 렉시컬 환경은 스코프를 구분하여 식별자를 등록하고 관리하는 저장소 역할을 하는 렉시컬 스코프의 실체이다.
this 식별자가 바라봐야 할 대상 객체. 실행 컨텍스트가 활성될 때 this가 지정되지 않은 경우에는 전역 객체가 저장된다.
const str = '안녕';
function outer() {
function inner() {
const greeting = '하이';
console.log(greeting);
console.log(str);
}
inner();
}
outer();
console.log(str);
{str, outer}
식별자를 저장한다. 전역 컨텍스트는 가장 최상위 컨텍스트이므로 outerEnvironmentReference는 null이다.str
에 '안녕'을 outer
에 함수를 할당한다.outer
함수를 호출한다. 전역 컨텍스트의 코드는 잠시 중단되고, outer
실행 컨텍스트가 활성화 된다.outer
실행 컨텍스트의 environmentRecord에 {inner}
식별자를 저장한다. outerEnvironmentReference에는 outer
함수가 선언될 당시의 Lexical Environment가 담긴다.outer
스코프에 inner
함수를 할당한다.inner
함수를 호출한다. 여기서 outer
실행 컨텍스트의 코드는 임시 중단되고, inner
실행 컨텍스트가 활성화된다.inner
실행 컨텍스트의 environmentRecord에 {greeting}
식별자를 저장한다. outerEnvironmentReference엔 inner
함수가 선언될 당시의 Lexical Environment가 담긴다. inner
함수는 outer
함수에서 선언되었으므로, outer
함수의 Lexical Environment 즉, {outer {inner}}
를 참조한다.greeting
을 찾아서 실행한다.str
에 접근하려고 한다. 이때, 자바스크립트 엔진은 활성화된 실행 컨텍스트의 Lexical Environment에 접근한다. 첫 요소의 enivironmentRecord에 str
이 있는지 찾아보고, 없으면 outerEnvironmentReference에 있는 environmentRecord로 넘어가 검색한다. 예제에서는 전역 Lexical Environment에 str
이 있으므로 '안녕'을 출력한다.inner
함수 실행이 종료된다. inner
실행 컨텍스트가 콜 스택에서 제거되고, outer
실행 컨텍스트가 다시 활성화된다.outer
함수 실행이 종료된다. outer
실행 컨텍스트가 콜 스택에서 제거되고, 전역 컨텍스트가 다시 활성화된다.str
을 검색해서 실행한다.https://east-star.tistory.com/14
https://gamguma.dev/post/2022/04/js_execution_context
https://velog.io/@edie_ko/js-execution-context