JavaScript 엔진 최적화 — JIT, Hidden Class, Inline Cache

leave_a_comment·약 13시간 전

프론트엔드 성능 최적화를 공부하다 보면 이런 이야기를 자주 듣게 된다.

  • 객체 구조(shape)를 일정하게 유지하라
  • 동적 프로퍼티 추가를 조심하라
  • 객체를 렌더마다 새로 만들지 마라

처음엔 그냥 "최적화 팁"처럼 보이지만, 사실 이건 전부 JavaScript 엔진(V8)의 내부 최적화 방식과 연결되어 있다.

이번 글에서는

  • JIT 컴파일
  • Hidden Class
  • Inline Cache

가 실제로 어떻게 동작하는지, 그리고 왜 React 성능과 연결되는지 정리해보려 한다.

(V8 기준으로 설명)


먼저 큰 흐름

JavaScript는 기본적으로 동적 언어다.

즉 이런 코드가 있을 때

obj.a

엔진은 런타임마다

  • a 프로퍼티가 진짜 있는지
  • 객체 구조가 뭔지
  • 타입이 뭔지

확인해야 할 수도 있다.

원래라면 굉장히 느릴 수 있는 구조다.

그래서 현대 JS 엔진은 내부적으로 엄청난 최적화를 수행한다.


전체 동작 흐름

JavaScript
  ↓
Parser
  ↓
AST 생성
  ↓
Interpreter (Bytecode 실행)
  ↓
Profiler (실행 패턴 수집)
  ↓
JIT Compiler
  ↓
Optimized Machine Code

핵심은:

처음엔 빠르게 실행하고,
자주 쓰이는 코드를 나중에 최적화한다

는 점이다.


1. JIT 컴파일 (Just-In-Time)

왜 필요할까?

예전 JavaScript 엔진은

코드 한 줄 해석 → 실행

방식이었다.

즉 인터프리터 기반.

하지만 이 방식은 반복 실행되는 코드에서 비효율적이었다.


현대 엔진 방식

요즘 엔진은

  1. 처음엔 빠르게 인터프리트
  2. 실행 패턴 수집
  3. 자주 실행되는 코드 발견
  4. 최적화된 머신코드 생성

흐름으로 동작한다.

이걸 JIT(Just-In-Time) 컴파일이라고 한다.


예시

function add(a, b) {
  return a + b;
}

처음엔 일반적인 방식으로 실행된다.

하지만 계속

add(1, 2);
add(3, 4);
add(5, 6);

처럼 숫자만 들어온다면 엔진은 판단한다.

"이 함수는 숫자 계산 전용이네?"

그 순간 JIT가 최적화를 시작한다.


최적화되는 부분

원래 JS는 매 호출마다

  • 타입 검사
  • 연산 방식 결정

같은 작업을 해야 한다.

하지만 숫자만 들어온다고 확신하면

타입 검사 생략

가능해진다.

즉 거의 네이티브 코드 수준으로 빨라진다.


그런데 문제 발생

갑자기

add("a", "b");

가 들어오면?

기존 최적화 가정이 깨진다.

엔진은

Deoptimization (deopt)

을 수행한다.

  • 최적화 취소
  • 일반 인터프리트 모드 복귀

가 발생한다.


핵심

JavaScript 엔진은

"예측 가능한 코드"

를 굉장히 좋아한다.

  • 타입 안정성
  • 일관된 호출 패턴

이 중요하다.


2. Hidden Class

이건 V8 최적화의 핵심 중 하나다.


JavaScript 객체의 문제

JS 객체는 동적으로 구조가 바뀔 수 있다.

const user = {};

user.name = "me";

런타임에 프로퍼티가 계속 추가된다.

문제는 CPU는 원래

struct User {
  char* name;
}

같은 고정 구조를 좋아한다는 점이다.


그래서 등장한 Hidden Class

V8은 객체를 내부적으로

"고정 구조처럼"

관리한다.

예를 들어

const user = {
  name: "me",
  age: 20,
};

를 만들면 내부적으로

HiddenClass1
- name → offset 0
- age → offset 1

같은 구조를 생성한다.


왜 빠를까?

user.name

접근 시

offset 0 접근

만 하면 된다.

즉 일반 해시 탐색이 아니라:

거의 C 구조체 접근처럼 동작한다.


문제 상황

const a = { x: 1, y: 2 };

const b = { y: 2, x: 1 };

겉보기엔 같아 보이지만

  • 프로퍼티 생성 순서가 다름
  • Hidden Class도 다르게 생성됨

즉 엔진 입장에서는 서로 다른 객체 구조다.


React에서 왜 중요할까?

const state = {
  name,
  age,
};

객체 구조가 계속 일정하게 유지되면 엔진 최적화가 잘 유지된다.

반대로

const obj = {};

if (condition) {
  obj.name = "me";
}

처럼 구조가 계속 바뀌면 최적화가 깨질 가능성이 높아진다.

객체 shape 안정성

이 성능과 연결된다.


3. Inline Cache (IC)

이건 Hidden Class와 연결되는 최적화다.


문제

user.name

를 매번

  • 프로퍼티 탐색
  • 위치 검색

하면서 찾으면 느리다.


해결 방식

엔진은 캐싱한다.

예를 들어

"이 객체는 HiddenClass1 이었지?"

를 기억해둔다.

그러면 다음 접근부터는

바로 offset 접근

가능하다.

이걸 Inline Cache(IC)라고 한다.

"이 형태의 객체는 여기 접근하면 된다"

를 캐싱하는 구조다.


동작 흐름

처음 접근

lookup 발생

두 번째부터

cache hit

즉 훨씬 빨라진다.


문제 상황 — Megamorphic

foo(obj1);
foo(obj2);
foo(obj3);

근데 매번 객체 shape가 다르다면?

엔진 입장에서는

"왜 맨날 다른 객체가 들어오지?"

상태가 된다.

이를

Megamorphic

상태라고 부른다.

즉 Inline Cache가 너무 많은 형태를 처리해야 해서 최적화 효율이 떨어진다.


React와 연결되는 부분

예를 들어

<Component style={{ color: "red" }} />

이 코드는 렌더마다

{ color: "red" }

새 객체를 생성한다.

  • 새 reference
  • 새 allocation
  • GC 증가
  • shape 최적화 방해 가능성

이 생긴다.

그래서

  • useMemo
  • useCallback
  • 객체 memoization

같은 패턴이 중요해진다.


실제 프론트엔드 예시

좋지 않은 패턴

items.map((item) => ({
  ...item,
  active: true,
}));

렌더마다

  • 새 객체 생성
  • 새 allocation
  • GC 비용 증가

가 발생한다.

물론 무조건 나쁘다는 건 아니지만:

자주 렌더되는 대규모 리스트

에서는 성능에 영향을 줄 수 있다.


전체 흐름 정리

1단계 — 인터프리트

일단 빠르게 실행


2단계 — 패턴 수집

엔진이 관찰한다.

- 숫자만 오네?
- 객체 구조 일정하네?
- 호출 패턴 안정적이네?

3단계 — Hidden Class 생성

객체 구조 최적화


4단계 — Inline Cache 적용

프로퍼티 접근 캐싱


5단계 — JIT 최적화

최적화된 머신코드 생성


6단계 — 예측 실패

타입이나 구조가 바뀌면

deopt 발생

다시 일반 모드로 복귀


결국 핵심은 하나

JavaScript 엔진 최적화는 결국

"예측 가능한 코드"

를 좋아한다.

  • 타입 안정성
  • 객체 구조 안정성
  • 일관된 호출 패턴

이 성능 최적화의 핵심이다.

이걸 이해하면

  • 왜 객체 shape 유지가 중요한지
  • 왜 dynamic property 추가가 느릴 수 있는지
  • 왜 React re-render 최적화가 필요한지
  • 왜 useMemo/useCallback을 쓰는지

를 단순 React 레벨이 아니라

"엔진 수준"

에서 이해할 수 있게 된다.

profile
나도 성장하고파

0개의 댓글