클로저 · 싱글 스레드 · 이벤트 루프 · 렌더링 프로세스
리액트 심화 개념(예: 렌더링 최적화, useCallback, useMemo, 비동기 상태 업데이트 등)을 이해하려면 자바스크립트의 실행 원리를 탄탄히 아는 것이 필수입니다.
이번 글에서는 리액트를 공부하기 전에 반드시 짚고 넘어가야 할 JS 핵심 4개념을 정리했습니다.
정의: “함수가 선언될 때의 렉시컬 스코프(변수 환경)를 기억하여, 실행 시점에도 그 변수들에 접근할 수 있는 현상(함수 + 환경의 조합).”
function makeCounter(step = 1) {
let value = 0; // ← 렉시컬 환경에 저장
return function inc() { // ← [[Environment]]로 value를 기억
value += step;
return value;
};
}
const c1 = makeCounter(); // c1은 value=0을 기억
const c2 = makeCounter(2); // c2는 value=0, step=2를 기억
console.log(c1()); // 1
console.log(c1()); // 2
console.log(c2()); // 2
console.log(c2()); // 4
varconst btns = Array.from(document.querySelectorAll('button'));
for (var i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', () => console.log(i));
}
// 모든 버튼이 마지막 값만 출력
var는 함수 스코프라 하나의 i를 공유합니다.let(블록 스코프) 사용하거나 즉시실행함수(IIFE)로 캡쳐for (let i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', () => console.log(i));
}
function App() {
const [count, setCount] = useState(0);
// 이 핸들러는 "정의된 렌더링 시점의 count"를 기억
const onClick = () => {
setTimeout(() => {
// 여기서의 count는 과거 값일 수 있음
setCount(count + 1);
}, 1000);
};
return <button onClick={onClick}>{count}</button>;
}
setCount(prev => prev + 1); // 항상 최신 state 기반
useCallback/useMemo는 의존성 배열을 통해 어떤 값을 클로저로 “고정”할지 제어합니다.
자바스크립트의 렉시컬 환경(Lexical Environment) 구조를 시각적으로 표현한 다이어그램
정의: 자바스크립트 엔진은 기본적으로 단 하나의 호출 스택(Call Stack) 에서 코드를 순차 실행합니다.
console.log("A");
setTimeout(() => console.log("B"), 0);
console.log("C");
// A -> C -> B
// 200ms 동안 CPU 바쁜 작업(동기)
const block = (ms) => {
const end = performance.now() + ms;
while (performance.now() < end) {}
};
console.log("start");
block(500); // 이 동안 클릭, 스크롤, 렌더링 모두 지연
console.log("end");
setTimeout, queueMicrotask, requestIdleCallback, requestAnimationFrame 등으로 틈 주기React 18의 Concurrent Rendering은 싱글 스레드 환경에서 “작업을 잘게 쪼개고, 우선순위를 둬서 중단/재개할 수 있도록” 만든 모델입니다.

자바스크립트 런타임 구조
정의: 콜 스택이 비는 순간, 대기 중인 태스크(작업)를 정해진 우선순위에 따라 가져와 실행시키는 스케줄러/조정자
setTimeout, setInterval, MessageChannel, script…Promise.then/catch/finally, queueMicrotask, MutationObserverconsole.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve()
.then(() => console.log("microtask-1"))
.then(() => console.log("microtask-2"));
console.log("end");
// start -> end -> microtask-1 -> microtask-2 -> timeout
queueMicrotask 타이밍console.log("A");
queueMicrotask(() => console.log("B (micro)"));
console.log("C");
// A -> C -> B (micro)
requestAnimationFrame(rAF) 타이밍requestAnimationFrame(() => {
// 여기는 다음 페인트 직전: DOM 읽고/쓰고, 스타일/레이아웃 최소화 전략 가능
});
requestAnimationFrame(() => {
// 여기는 다음 페인트 직전: DOM 읽고/쓰고, 스타일/레이아웃 최소화 전략 가능
});
setState는 보통 동기처럼 보이지만 배치/스케줄링되어 나중에 반영됩니다.useEffect는 커밋 후(페인트 이후) 비동기적으로 실행useLayoutEffect는 DOM 커밋 직후(페인트 이전) 실행되어 레이아웃 측정에 적합microtask vs task 타이밍에 따라 reflow 횟수가 달라질 수 있음rAF/IdleCallback으로 분할, 또는 가상화·버퍼링 적용
이벤트 루프(Event Loop) 의 전체 흐름
정의: 브라우저가 HTML/CSS/JS를 받아 화면에 픽셀로 그릴 때까지의 전체 파이프라인
Recalculate Style → Layout → Paint → Composite 순으로 DevTools 타임라인에 보이는 이유가 이 흐름 때문
width/height/top/left, 폰트 바뀜, DOM 삽입/삭제 등)transform/opacity 변경은 Layout/ Paint를 건너뛸 수 있어 상대적으로 저렴/* 좋음: 합성 단계에서 처리됨 */
.box.animate {
will-change: transform, opacity;
transition: transform 300ms, opacity 300ms;
}
offsetWidth 읽기 → layout flush) 프레임이 끊깁니다.// layout thrashing (피해야 함)
for (const el of items) {
const w = el.offsetWidth; // 레이아웃 강제
el.style.width = (w + 10) + 'px';
}
// layout thrashing (피해야 함)
for (const el of items) {
const w = el.offsetWidth; // 레이아웃 강제
el.style.width = (w + 10) + 'px';
}
// 1) 먼저 모두 읽고
const widths = items.map(el => el.offsetWidth);
// 2) 다음 프레임에 한번에 쓰기
requestAnimationFrame(() => {
items.forEach((el, i) => { el.style.width = (widths[i] + 10) + 'px'; });
});
React.memo, useMemo/useCallback, 리스트 가상화, 서버 컴포넌트/SSR, Concurrent 특성(transition) 활용)
브라우저 렌더링 과정(Critical Rendering Path)
코드로만 알고 있었던 개념을 함수형 업데이트, 렌더 스텝 등으로 명칭을 명시해줘서 협업에서 소통할 때 많이 도움이 될 것 같아요!
리액트의 Concurrent Rendering으로 UI적인 부분을 먼저 실행한다는 점, useEffect가 페인트 이후 비동기적으로 실행한다는 점을 알게되어서 개발할 때 코드 작동에 대한 이해가 편해졌어요
transform과 opacity 변경이 레이아웃과 페인트를 건너뛸 수 있다는 점도 알게되어서, 앞으로 hover할 때나 background-color를 바꿀 때, 요소를 이동시킬때 두 속성을 먼저 생각해보려고 해요 감사합니다!!
자바스크립트 동작 원리를 리액트와 자연스럽게 연결해 설명해주셔서 이해가 잘 된 것 같아요 !! 특히 클로저 부분에서 stale closure 문제를 예시로 들어 함수형 업데이트가 왜 필요한지 구체적으로 보여주신 점이 인상적이었어요 !! 렌더링 프로세스 부분에서 각 단계의 비용과 최적화 방향까지 짚어주셔서 성능적인 관점에서도 생각해볼 수 있었습니다 !! 글이 전반적으로 개념과 실제 동작을 연결해줘서 덕분에 리액트의 렌더링 구조를 깊이 있게 이해하는데에 너무 좋았네요 ㅎㅎ 아티클 작성하시느라 수고 많으셨어요 ! 다음주도 화이팅 ~!