Million.js 완벽 가이드 — React 성능 최대화와 Virtual DOM·Block·For·Next.js 통합

Million.js 완벽 가이드 — React 성능 최대화와 Virtual DOM·Block·For·Next.js 통합

이 글의 핵심

Million.js는 React 위에서 동작하는 컴파일러·런타임으로, 핫 경로를 블록 단위로 최적화해 업데이트 비용을 줄입니다. Virtual DOM과의 차이, block()/For, Next.js 설정, 마이그레이션 순서, 제약·우회까지 실무 관점으로 정리합니다.

들어가며

Million.jsReact 16 이상Node 18 이상을 전제로 하는 오픈소스 최적화 스택입니다. 공식 자료와 커뮤니티 벤치마크에서는 동일한 UI 패턴을 기준으로 렌더링·업데이트 시간을 크게 줄이는 사례(예: 약 70% 수준의 개선을 언급하는 자료)가 보고되지만, 실제 이득은 리스트 길이·상태 변동 빈도·서드파티 컴포넌트 비중·SSR/하이드레이션에 따라 달라집니다. 따라서 이 글에서는 “항상 70%”가 아니라 “왜 그런 범위가 나올 수 있는지”를 구조적으로 이해하는 데 초점을 둡니다.

React는 선언적 UIVirtual DOM(이하 VDOM) 기반 재조정(reconciliation)으로 널리 쓰이지만, 초당 수십 번 갱신되는 리스트·대시보드 셀·게임성 UI처럼 동일 패턴의 반복 업데이트가 많으면, VDOM diff 비용이 누적됩니다. Million.js는 이런 구간에 “블록(block)”이라는 고속 경로를 만들어, 가능한 한 DOM을 직접·증분적으로 갱신하도록 설계되어 있습니다.

이 가이드는 다음을 순서대로 다룹니다.

  1. 핵심 개념(컴파일러, 자동/수동 모드, 폴백)
  2. VDOM 최적화 원리(무엇을 줄이는지, 어디까지 React와 동일한지)
  3. block()<For>의 실전 규칙
  4. Next.js(App Router·Pages) 통합
  5. 마이그레이션 전략(어디부터 적용할지)
  6. 벤치마크 해석(비교 조건의 중요성)
  7. 제약과 우회(UI 라이브러리, 조건부 return, SSR 불일치 등)

1. Million.js의 핵심 개념

1-1. 컴파일러 + 런타임

Million.js는 패키지 million번들러/프레임워크 플러그인(컴파일러)의 조합으로 동작합니다. JSX를 Million이 이해하기 쉬운 형태로 변환하고, 런타임에서는 million/reactblock, For으로 최적화된 업데이트를 수행합니다.

컴파일러 없이 런타임만 쓰는 것도 기술적으로 가능하지만, 공식 문서는 기능 제한이 크다고 명시하며 권장하지 않습니다. 실무에서는 프로젝트 빌드 파이프라인에 컴파일러를 넣는 것을 전제로 설계하는 것이 안전합니다.

1-1a. 설치: CLI와 수동 설치

공식 문서는 npx million@latest CLI로 패키지 설치와 설정을 한 번에 맞추는 방법을 안내합니다. Node 18 이상, React 16 이상이 필요합니다. CLI가 어렵거나 커스텀 빌드가 있으면 npm install million번들러별로 컴파일러 플러그인을 직접 연결합니다. Next.js·Vite·Webpack·Astro 등 프리셋이 문서에 정리되어 있으므로, 현재 쓰는 도구의 탭만 따라가면 됩니다.

1-2. 자동(Automatic) 모드와 수동(Manual) 모드

  • Automatic 모드: 설정과 코드 분석을 통해 최적화 후보를 자동으로 적용하는 방향(진입 장벽이 낮음). Next.js에서는 million.next(nextConfig, { auto: true }) 형태로 시작하는 경우가 많습니다.
  • Manual 모드: block()/For를 개발자가 명시해 핫 경로를 제어합니다. 세밀한 제어·디버깅이 필요할 때 적합합니다.

팀 상황에 따라 자동으로 먼저 도입 → 프로파일링으로 병목 구간만 수동 블록화하는 점진 전략이 흔합니다.

1-3. “점진적 저하(progressive degradation)”

Million.js는 지원하지 않는 패턴을 만나면 경고를 내보낼 수 있지만, 앱이 깨지기보다는 기본 React 렌더링으로 폴백하는 철학을 갖습니다. 즉, 도입 초기에 “완벽한 블록”을 강요하지 않아도 앱은 동작합니다. 다만 성능 이점규칙을 준수한 블록에 집중됩니다.


2. Virtual DOM 최적화 원리

2-1. React VDOM이 무엇을 하는가

React는 UI를 상태의 함수로 표현하고, 상태가 바뀌면 새 Virtual Tree를 만들고 이전 트리와 비교(diff)하여 최소한의 DOM 변경을 계산합니다. 이 모델은 생산성과 예측 가능성이 뛰어나지만, 업데이트가 매우 잦고 트리가 크면 diff·할당 비용이 체감될 수 있습니다.

2-2. Million.js가 줄이려는 비용

Million.js는 “반복되는 UI 패턴”블록으로 묶어, 가능한 한 VDOM 전체 diff를 타지 않도록 합니다. 블록 내부에서는 더 직접적인 DOM 업데이트 시퀀스에 가깝게 동작하도록 설계되어, 리스트의 재배치·갱신처럼 같은 구조가 유지되는 변경에서 이득이 크기 쉽습니다.

이때 중요한 점은 Million이 React의 모델을 완전히 버리는 것이 아니라, React 컴포넌트 경계 안에서 특정 서브트리만 최적화한다는 것입니다. 따라서 상태 관리·훅·이벤트는 여전히 React 규칙을 따릅니다.

2-3. 컴파일러가 필요한 이유

블록은 JSX 구조가 컴파일 타임에 분석 가능할 때 효과가 극대화됩니다. 컴파일러는 JSX를 Million이 안전하게 최적화할 수 있는 형태로 맞추고, 훅 호출 위치 같은 React의 제약과 충돌하지 않게 정리하는 역할을 합니다. 컴파일러 누락 시 Invalid Hook Call 같은 이슈가 보고되는 이유도 여기에 있습니다.


3. block()<For> — 수동 모드의 축

3-1. block() 기본 형태

block고차 컴포넌트(HOC)에 가깝게 동작하며, million/react에서 import해야 합니다(million 단일 패키지에서 직접 가져오면 안 되는 경우가 있습니다).

// LionBlock.tsx
import { block } from 'million/react';

const LionBlock = block(function Lion() {
  return <img src="https://million.dev/lion.svg" alt="Million" />;
});

export default LionBlock;

루트 요소 태그를 바꾸고 싶다면 block의 두 번째 인자로 { as: 'div' }처럼 as를 줄 수 있습니다. 기본 래퍼가 slot 계열로 잡히는 경우가 있어, 시맨틱 HTML·레이아웃에 맞게 조정합니다.

import { block } from 'million/react';

const CardBlock = block(
  function Card() {
    return <p>내용</p>;
  },
  { as: 'article' },
);

블록 선언 규칙 중 가장 흔한 것은 다음입니다.

  • 변수 선언으로 정의해야 컴파일러가 분석하기 쉽습니다. export default block(...)처럼 한 줄 export는 피하고, const X = block(...); export default X 형태를 권장합니다.
  • block() 인자로 컴포넌트 함수 참조를 넘깁니다. block(<Component />)처럼 이미 만들어진 JSX 인스턴스를 넘기면 안 됩니다.

3-2. 블록 안에서의 조건부 렌더링

문서에서는 결정적(deterministic) 반환을 권장합니다. 여러 return 분기조건에 따라 완전히 다른 형태의 트리를 반환하면 성능 저하 또는 React 훅 규칙 위반(예: 렌더마다 훅 개수 변화)으로 이어질 수 있습니다. 실무에서는 블록 바깥에서 분기하거나, 블록 내부는 단일 return + 구조 고정을 목표로 리팩터링합니다.

3-3. <For>로 리스트 처리

블록 내부에서 .map()으로 리스트를 도는 패턴은 이상적이지 않습니다. 대신 For 컴포넌트가 권장됩니다. 배열은 each prop으로 넘기고, 자식은 함수로 각 아이템을 렌더링합니다.

import { For } from 'million/react';
import { useState } from 'react';

export function App() {
  const [items, setItems] = useState([1, 2, 3]);

  return (
    <>
      <button type="button" onClick={() => setItems([...items, items.length + 1])}>
        아이템 추가
      </button>
      <ul>
        <For each={items}>
          {(item) => <li>{item}</li>}
        </For>
      </ul>
    </>
  );
}

as prop으로 래퍼 태그 이름을 바꿀 수 있고(기본은 slot), DOM 구조를 명확히 할 때 유용합니다.

3-4. Formemo prop

문서에 따르면 For는 내부적으로 블록 재사용을 기본적으로 제한해 예기치 않은 동작을 피합니다. 아이템이 오직 item 값에만 의존한다고 확신할 때 memo prop을 켜면 재사용·메모리 절감에 도움이 될 수 있습니다. 반대로 아이템 외부 상태에 의존하는 복잡한 셀에는 부적절할 수 있어, 프로파일링 후 적용하는 편이 안전합니다.

3-5. SSR과 ssr: false

서버와 클라이언트에서 렌더링 알고리즘이 다르기 때문에 하이드레이션 불일치가 날 수 있습니다. 예를 들어 Math.random() 같은 비결정적 값을 리스트에 넣으면 문제가 됩니다. 이때 block 옵션의 ssr: falseForssr={false}SSR을 끄는 패턴이 문서에 나옵니다. SEO가 중요한 콘텐츠에는 신중히 적용해야 합니다.


4. Next.js 통합

4-1. next.config에 컴파일러 연결

공식 문서의 예시는 대략 다음과 같은 형태입니다(next.config.mjs 등).

import million from 'million/compiler';

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
};

const millionConfig = {
  auto: true,
  // React Server Components를 쓰는 경우:
  // auto: { rsc: true },
};

export default million.next(nextConfig, millionConfig);

App Router에서는 /app의 클라이언트 컴포넌트('use client') 위주로 Million이 적용됩니다. 서버 컴포넌트 트리클라이언트 경계가 섞인 프로젝트에서는 최적화 후보가 클라이언트 쪽에 국한된다는 점을 염두에 두어야 합니다.

4-2. Pages Router와의 공존

문서상 Pages Router와 App Router 모두 지원을 언급합니다. 기존 /pages 기반 코드에도 동일하게 컴파일러를 끼워 넣을 수 있으나, 레거시 바벨/플러그인 체인과 충돌하면 빌드 설정 조정이 필요할 수 있습니다.

4-3. 도입 시 흔한 이슈

GitHub 이슈 등에서 보고되는 유형은 다음과 같습니다.

  • 컴파일러 미적용으로 인한 런타임 경고·훅 오류
  • 서드파티 UI와 블록 규칙 충돌
  • SSR/하이드레이션 불일치

이럴 때는 (1) 컴파일러 적용 여부 확인, (2) 문제 구간을 일반 React로 되돌려 재현 최소화, (3) 블록 경계를 단순 DOM으로 재구성 순으로 접근합니다.


5. 마이그레이션 전략

5-1. 단계적 도입 로드맵

  1. 베이스라인 측정: React DevTools Profiler, Chrome Performance, Core Web Vitals(LCP, INP)로 병목 화면을 고릅니다.
  2. 컴파일러만 먼저: auto: true로 빌드·테스트·스테이징 배포를 거칩니다. 기능 회귀가 없는지 확인합니다.
  3. 핫 리스트에 For: 대시보드 테이블·채팅·피드 등 같은 행 UI가 반복되는 곳부터 For로 교체합니다.
  4. block으로 셀 단위 분리: 행 컴포넌트가 단순해질수록 블록화가 쉬워집니다. 먼저 조건부 return을 줄이는 리팩터링을 병행합니다.
  5. SSR이 필요한 페이지ssr 옵션과 콘텐츠 우선순위를 재검토합니다.

5-2. 코드 리뷰 체크리스트

  • 블록이 million/react에서 import되었는가
  • 블록이 변수 선언 + export 패턴인가
  • 리스트가 .map()이 아니라 For인가(블록 내부 특히)
  • UI 라이브러리 추상화가 블록 안에 과도하지 않은가
  • 비결정적 값이 SSR을 깨지 않는가

5-3. 롤백 계획

Million은 폴백이 있으나, 팀이 리스크를 줄이려면 feature flag로 컴파일러 설정을 끄거나, 문제 라우트만 블록을 제거하는 전략적 롤백을 문서화해 두는 것이 좋습니다.

5-4. 리스트 마이그레이션 예시(의도)

아래는 교육용 의사 코드에 가깝습니다. 실제 프로젝트에서는 키·접근성·에러 경계를 함께 맞춥니다.

이전(블록 내부에서 .map) — 문서가 권장하지 않는 패턴:

// ⚠️ 블록 안에서 map — 경고/성능 저하 가능
const RowListBlock = block(function RowList({ items }: { items: string[] }) {
  return (
    <ul>
      {items.map((id) => (
        <li key={id}>{id}</li>
      ))}
    </ul>
  );
});

이후(For로 치환):

import { block, For } from 'million/react';

const RowListBlock = block(function RowList({ items }: { items: string[] }) {
  return (
    <ul>
      <For each={items}>{(id) => <li key={id}>{id}</li>}</For>
    </ul>
  );
});

행 단위로 더 얇게 나눌 때는 Row를 별도 block으로 분리하고, 부모는 데이터만 내려주는 얇은 컨테이너로 두면 프로파일링 결과가 읽기 쉬워집니다.


6. 성능 벤치마크 — 숫자를 올바르게 읽기

6-1. “70% 개선”의 의미

Million.js 관련 자료에서는 기존 React 대비 렌더링 시간을 크게 줄였다는 주장이 반복적으로 등장하며, 약 70% 같은 수치가 인용되기도 합니다. 다만 벤치마크는 항상 조건부입니다.

  • 리스트 길이·키 안정성·상태 위치
  • 프로덕션 빌드 vs 개발 모드
  • 브라우저·CPU 스로틀링
  • 동시성(Concurrent) 기능·Strict Mode

위 요소에 따라 결과는 크게 달라집니다. 따라서 외부 벤치마크는 참고하고, 자사 앱에서 Profiler로 검증하는 것이 정석입니다.

6-2. 자체 측정 팁

  • 동일 시나리오 스크립트(예: 1000행 스크롤·필터 토글)를 고정합니다.
  • 프로덕션 번들로 측정합니다.
  • INP가 중요한 화면은 입력 지연을 별도로 봅니다.

7. 제약사항과 우회 방법

7-1. UI 컴포넌트 라이브러리

문서는 블록 안에서 MUI·Chakra 등 추상 컴포넌트성능 저하를 유발할 수 있다고 경고합니다. 우회: 블록 안은 순수 DOM + 클래스로 구성하고, 디자인 시스템은 블록 바깥이나 덜 자주 렌더되는 레이어에 둡니다.

7-2. Spread props / children

변경 가능한 배열 spread 등은 비결정적으로 취급될 수 있어 제한이 있습니다. 우회: 명시적 props고정 구조의 children을 선호합니다.

7-3. 조건부 return과 훅

블록 규칙과 React 훅 규칙이 겹치면 디버깅이 어려운 오류가 납니다. 우회: 블록을 작게 쪼개고, 조건 분기는 상위 컴포넌트에서 처리합니다.

7-4. SSR 하이드레이션

우회: ssr: false/ssr={false}로 해당 블록만 클라이언트 렌더링하거나, 결정적 데이터만 서버에서 내려보냅니다.

7-5. 자식으로 블록에 임의 노드를 넣는 경우

문서의 예시처럼 <YourBlock>텍스트</YourBlock> 형태로 블록에 예상치 않은 자식을 넣으면 경고 지점이 될 수 있습니다. 블록은 props로 제어 가능한 단순 트리에 맞출 때 안정적입니다. 슬롯형 레이아웃이 필요하면 블록 바깥에서 합성하거나, props로 슬롯 내용을 넘기는 방식을 검토합니다.


7-6. 트러블슈팅 빠른 참조

증상점검할 것
[Million.js] Block needs to be defined as a variable declarationconst X = block(...)export default X로 바꿨는지
Invalid Hook Call컴파일러가 빌드에 실제로 포함됐는지, 중복 React 여부
Found unsupported import for blockimport { block } from 'million/react'인지
하이드레이션 mismatchMath.random()·시간·로케일 등 비결정적 값, ssr: false 검토
성능이 기대만큼 안 나옴블록 안 MUI 등 추상 컴포넌트 비중, .map 잔존 여부

8. 정리

Million.js는 React 생태계를 유지한 채, 컴파일러 + 블록 + For반복 업데이트 비용을 줄이는 도구입니다. 핵심은 규칙을 준수한 “얇고 단순한 블록”을 만드는 것이며, 자동 모드로 시작해 수동 모드로 핫 스팟을 깎는 점진 전략이 실무에 잘 맞습니다. 숫자 목표(예: 70%)는 팀의 계측 결과로 대체해야 하며, Next.js에서는 클라이언트 경계와 RSC 설정을 함께 설계해야 합니다.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 지키는 것이 이 저장소의 관례입니다.


참고로 보기 좋은 글