Yoga로 콘솔 레이아웃 구성
Text.js는 포인트 배율을 1로 설정해 WebAssembly에서 Yoga를 실행하므로 모든 상자가 정수 단위 셀에 맞춰져요. 텍스트는 Intl.Segmenter로 나누고 셀 단위로 측정해요.
모든 Text.js 프레임은 React Native에서 사용하는 flexbox 엔진인 Yoga를 WebAssembly로 컴파일해 레이아웃을 계산해요. 박스와 텍스트는 하나의 Yoga 트리를 공유해요. 콘솔은 문자 셀로 이루어진 격자이므로, 계산된 모든 위치와 크기는 정수 개의 셀이어야 해요.
1포인트는 1개 셀
Yoga는 포인트 배율로 정의한 픽셀 격자에 맞춰 결과를 반올림해요. Retina 화면처럼 배율이 2이면 가장자리가 반 포인트 위치에 놓일 수 있어요. Text.js는 Yoga를 한 번 로드하고 배율을 1로 설정해요:
export function initYoga(): Promise<Yoga> { if (yoga !== null) return Promise.resolve(yoga); if (yogaPromise === null) { yogaPromise = loadYoga().then((instance) => { yoga = instance; const config = instance.Config.create(); config.setPointScaleFactor(1); config.setUseWebDefaults(false); yogaConfig = config; return instance; }); yogaPromise.catch(() => { yogaPromise = null; }); } return yogaPromise;}배율이 1이면 Yoga는 모든 가장자리를 정수 단위로 반올림하고, Text.js는 1단위를 1개 셀로 취급해요. Yoga는 각 가장자리의 절대 위치를 반올림하고 반올림된 가장자리로 크기를 계산하므로, 이웃한 박스는 가장자리를 공유해요. 81열짜리 행을 width="50%" 박스 2개로 나누면 하나는 40열, 다른 하나는 41열을 차지하며, 빈틈도 겹침도 없어요.
Text.js는 CSS 기본값을 직접 적용해요: 브라우저처럼 flexDirection은 row이고 flexShrink는 1이에요.
텍스트 측정
텍스트 노드는 측정 함수가 있는 Yoga 리프 노드예요. Yoga가 사용 가능한 너비로 이 함수를 호출하면 Text.js는 텍스트 크기를 셀 단위로 반환해요:
private measure(width: number, widthMode: MeasureMode, _height: number, _heightMode: MeasureMode): { width: number; height: number } { const natural = this.naturalSize(); if (widthMode === MeasureMode.Undefined || Number.isNaN(width)) return natural; const avail = Math.max(0, Math.floor(width)); if (natural.width <= avail) { return { width: widthMode === MeasureMode.Exactly ? avail : natural.width, height: natural.height }; } if ((this.props.wrap ?? "wrap") !== "wrap") return { width: avail, height: natural.height }; const lines = this.getLines(avail); let maxLine = 0; for (const l of lines) if (l.width > maxLine) maxLine = l.width; return { width: widthMode === MeasureMode.Exactly ? avail : Math.min(avail, maxLine), height: lines.length };}너비 안에 들어가는 텍스트는 원래 크기로 반환해요. 들어가지 않는 텍스트는 사용 가능한 너비에 맞춰 줄바꿈하며, 높이는 줄 수가 돼요. 노드는 마지막 줄바꿈 결과를 보관하므로, 같은 너비를 다시 측정해도 텍스트를 다시 줄바꿈하지 않아요.
자소 클러스터 측정
문자열의 length는 UTF-16 코드 단위 수를 세므로, 몇 개 셀을 채우는지 알기에는 부족해요. 먼저 Intl.Segmenter로 텍스트를 자소 클러스터로 나누므로, e 뒤에 결합 악센트가 붙으면 하나의 클러스터가 되고, 폭이 0인 연결자로 이어진 이모지 시퀀스도 하나의 클러스터가 돼요. 그런 다음 각 클러스터에 너비를 부여해요:
| 텍스트 | 셀 수 | 이유 |
|---|---|---|
abc | 3 | 문자당 1개 셀 |
你好 | 4 | 동아시아 전각 문자는 2개 셀 차지 |
🚀 | 2 | 이모지 표시는 2개 셀 차지 |
é (e + U+0301) | 1 | 결합 부호는 셀을 차지하지 않음 |
─│┌▼←⠋ | 6 | 상자 그리기 문자, 기하학적 도형, 화살표, 점자는 1개 셀로 고정 |
테두리, ▼ 기호, 화살표, 스피너 프레임은 항상 1개 셀로 측정하므로, 박스 테두리가 내용의 정렬을 밀어내지 않아요.
출력 가능한 ASCII는 분할을 건너뛰고, 나머지 너비는 코드 포인트별, 클러스터별로 캐시해요.
줄바꿈
<Text>는 wrap 모드를 받아요:
wrap(기본값)은 공백에서 줄바꿈하고, 단어가 줄보다 넓으면 단어 안에서도 줄바꿈해요.nowrap은 한 줄을 유지하고 박스가 넘치는 부분을 잘라내게 해요.truncate-end,truncate-middle,truncate-start는 한 줄을 유지하고 들어가지 않는 부분을…로 바꿔요.
말줄임도 셀 수를 기준으로 하므로, 중국어 텍스트 줄은 문자 경계에서 잘리고 줄임표는 정확히 1개 셀을 차지해요.
실제 사용
여기서는 30열짜리 사이드바 옆에 남은 너비를 차지하는 패널이 있어요:
<Box flexDirection="row" width="100%" height="100%"> <Box width={30} flexDirection="column" border="rounded" title="Sessions" overflow="hidden"> <Text wrap="truncate-end">session-03 · refactor the renderer pipeline</Text> </Box> <Box flexGrow={1} flexDirection="column" border="single" title="Conversation" paddingX={1}> <Text>Message 0: 你好 🚀</Text> </Box></Box>100열짜리 콘솔에서 사이드바는 테두리를 포함해 30열을 차지하고, 대화 패널은 나머지 70열을 차지해요. 테두리는 양쪽에서 각각 1개 셀을 차지하므로, 세션 제목은 28개 셀에서 말줄임 처리돼요.