← 블로그

뒤집힌 아키텍처

React 커밋이 DevTools의 console.log 호출 하나가 되는 과정.

Adam

DOM 프레임워크에서는 컴포넌트가 DOM 노드가 되고, 브라우저가 페이지에서 노드를 배치하고 그려요. Text.js에서는 컴포넌트가 셀이 되고, 변경된 셀 프레임마다 DevTools에서 console.log를 한 번 호출해요. 이를 역전된 아키텍처라고 불러요.

파이프라인

React ─▶ reconciler ─▶ Yoga ─▶ CellBuffer ─▶ row-hash gate ─▶ RLE ─▶ console.log

콘솔은 일반적이지 않은 디스플레이라서 각 단계가 필요해요. 고정폭 글꼴을 쓰고 행 단위로 구성되며, %c로 스타일을 지정하고, 다시 그리는 시점은 DevTools가 결정해요.

리컨실러

@textjs/react는 React 19 리컨실러([email protected])로, 두 호스트 요소인 tx-box와 tx-text, 문자열 자식을 위한 텍스트 인스턴스를 제공해요. <Box>부터 <Markdown>까지 가져오는 모든 컴포넌트는 이를 기반으로 만들었어요. 커밋은 requestAnimationFrame에 예약되고, 키 입력이나 붙여넣기는 같은 태스크 안에서 동기적으로 렌더링돼요.

레이아웃

WebAssembly로 컴파일된 Yoga가 pointScaleFactor = 1로 트리를 배치하므로 모든 박스가 정수 단위의 셀에 맞춰져요. 텍스트는 Intl.Segmenter와 동아시아 문자 너비 표로 측정하므로 你好는 4개 셀, 🚀는 2개 셀을 차지해요. 편집으로 텍스트 노드의 크기가 바뀔 수 없다면 Yoga에 다시 계산을 요청하지 않아요.

CellBuffer

프레임은 한 번만 할당하는 타입 배열의 격자예요. 문자는 Uint32Array에, 전경색과 배경색은 압축된 0xRRGGBBAA로, 속성은 Uint16Array 비트마스크로 저장해요. 그리기 작업은 이 배열에 값을 쓰고 어느 행을 건드렸는지 추적해요.

게이트

각 행에는 캐시된 32비트 FNV-1a 해시가 있어요. 마지막 프레임 이후에 값이 기록된 행만 해시를 다시 계산해요. 바뀐 행이 없으면 드라이버는 문자열을 만들기 전에 반환하고 콘솔을 호출하지 않아요. 숨겨진 탭의 스피너는 갱신되지 않으므로 비용이 들지 않아요.

런 길이 인코딩

변경된 행을 다시 스캔해 같은 스타일이 이어지는 구간으로 나누고, 각 구간을 하나의 %c 스팬으로 만들어요. 구간은 줄바꿈을 넘어 이어져요. CSS 문자열은 정수 스타일 ID로 참조하도록 인터닝하므로, 빈번하게 실행되는 경로에서 같은 문자열을 두 번 만들지 않아요. 100×32 대시보드는 3,200개 셀과 149개 스팬으로 구성돼요.

서식 문자열은 DevTools의 두 가지 동작도 방지해요. 리터럴 %는 %%로 기록해요. :// 뒤, www. 내부, path:line 안에 워드 조이너를 넣어서 DevTools가 텍스트를 링크로 바꾸고 자체 색상으로 다시 그리지 않게 해요.

한 번의 호출

변경된 프레임마다 한 번만 호출해요:

console.log("%c\u200b%c\n%c▼ OpenCode Lite%c …", "margin-top:100vh;", "", "color:#fafafa;…", …);

프레임은 덧붙이기 방식으로 쌓이고, DevTools의 50ms 스로틀러에 맞춰 간격을 조절하며, 한 번의 그리기 안에서 교체돼요. 이 부분은 Turbolog: 깜박임 제거에서 다뤄요.

입력

콘솔은 키 입력을 받을 수 없어서 페이지가 대신 받아요. 숨겨진 <textarea>가 키 입력, 붙여넣기, IME 조합을 수집하고, wheel 이벤트가 ScrollBox를 스크롤해요. textjs.org의 Playground와 OpenCode 페이지는 각각 하나의 큰 포커스 패드예요. 콘솔을 연 다음 페이지를 클릭해 입력하세요.

페이지에서 볼 수 있는 정보

페이지는 콘솔 크기를 읽을 수 없고 자체 창의 크기만 읽을 수 있어서 Text.js가 콘솔 크기를 추론해요. 도킹된 패널은 outer*가 그대로인 상태에서 innerWidth나 innerHeight를 줄이고, 각 도킹 가설을 Chrome의 확대/축소 수준별로 계산해요. 가장 잘 맞는 결과를 선택해요. 상수(가로 패딩 56px, 콘솔 UI 영역 104px, 행 간격 14px)는 실제 DevTools 프런트엔드에서 측정했어요.