← 블로그

console.log에 React 렌더링

Text.js는 프레임마다 스타일이 적용된 console.log 호출 하나로 DevTools 콘솔에 React 앱을 그려요.

Adam

Text.js는 Chrome DevTools 콘솔에 React 앱을 렌더링해요. 숨겨진 입력 요소 외에는 페이지에 UI가 없어요. 앱의 각 프레임은 console.log 호출 하나예요.

화면으로 쓰는 콘솔

console.log는 형식 문자열을 받아요. 각 %c는 다음 인자의 CSS를 그 뒤의 텍스트부터 다음 %c 앞까지 적용해요:

console.log("%c▼ Text.js%c ready", "color:#ededed;font-weight:700", "color:#45a557");

이 스타일로 색상, 배경, 굵게, 기울임꼴, 밑줄을 설정할 수 있고, 콘솔 글꼴은 고정폭 글꼴(macOS에서는 Menlo)이에요. 고정폭 글꼴과 연속된 문자 구간마다 바꿀 수 있는 색상 덕분에 콘솔은 스타일이 적용된 문자 셀의 격자로 작동해요. 여기에 테두리, 패널, 입력 요소, 차트를 모두 그릴 수 있어요.

셀 격자로 구성된 프레임

렌더러는 각 프레임을 CellBuffer에 보관해요. 문자는 하나의 타입 배열에, 전경색과 배경색은 각각 별도의 배열에, 굵게와 밑줄 같은 속성은 네 번째 배열에 저장해요. 배열은 주어진 크기에 맞춰 한 번 할당하고 모든 프레임에서 재사용해요.

프레임을 기록할 때 드라이버는 각 줄을 순회하며 같은 스타일을 공유하는 이웃 셀들을 연속 구간으로 묶어요. 각 구간은 하나의 %c 스팬이 되고, 줄은 \n으로 연결해요. 3,200개 셀로 구성된 100×32 테스트 대시보드는 약 200개 스팬으로 인코딩돼요.

프레임당 한 번의 호출

각 줄을 별도의 메시지로 기록하는 방식은 제대로 작동하지 않아요. DevTools에서는 메시지마다 테두리와 소스 링크가 붙고, 연속된 동일 메시지는 반복 횟수 표시로 접히므로 표에서 내용이 같은 두 줄이 하나로 합쳐져요. 하나의 메시지로 기록한 프레임은 나뉘지 않고 유지돼요.

먼저 텍스트에서 두 가지를 이스케이프해야 해요. 리터럴 %는 %%로 써서 100% 같은 텍스트가 스타일 인자를 소모하지 않게 해요. DevTools는 URL을 링크로 바꾸고 자체 색상으로 다시 그리기도 하므로, ://와 www.에 폭이 없는 워드 조이너(U+2060)를 넣어요:

export function escapeConsoleText(text: string, allowLink = false): string {  let s = text;  if (s.includes("%")) s = s.replaceAll("%", "%%");  if (!allowLink) {    if (s.includes(LINK_SCHEME)) s = s.replaceAll(LINK_SCHEME, LINK_SCHEME_SAFE);    if (s.includes(WWW) || s.includes("WWW.")) s = s.replace(/www\./gi, (m) => `${m.slice(0, 3)}\u2060.`);  }  return s;}

대기 중인 프레임의 로그 생략

버퍼의 모든 줄에 해시를 계산해요. 마지막 프레임 이후 바뀐 해시가 없으면 드라이버는 문자열을 만들기 전에 반환하고 콘솔을 건드리지 않아요. 입력을 기다리는 앱은 호출을 하지 않아요.

바뀐 줄이 있으면 드라이버는 콘솔을 지우고 새 프레임을 기록해요:

const { frame, dirtyRows } = this.encode(buffer, options);if (frame === null) return SKIPPED;if (this.clearEachFrame) target.clear();target.log(frame.format, ...frame.args);

해시가 바뀐 줄만 다시 인코딩해요. 나머지는 이전 프레임의 스팬을 재사용해요.

입력

콘솔은 키 입력을 받지 못하므로 페이지에서 수집해요. 숨겨진 <textarea>가 키 누르기, 붙여넣기, IME 조합을 받아 앱의 키 이벤트로 바꿔요. 페이지를 클릭하면 이 요소에 포커스가 맞춰져요.

다음 단계

npm에 있는 @textjs/core 0.0.x 프로토타입은 모든 기능을 하나의 패키지에 담고 있어요. Text.js 0.1은 여기서 설명한 렌더러를 @textjs/core로, React 리컨실러를 @textjs/react로 옮기고 App Router를 추가해요.