← 블로그

Text.js 0.1

Text.js 0.1은 DevTools 콘솔에서 렌더링하는 앱에 React 19 리컨실러, Yoga 레이아웃, 파일 시스템 기반 App Router를 제공해요.

Adam

Text.js 0.1에 태그가 붙었어요. 세 계층을 모두 갖춘 첫 버전이에요: @textjs/core는 콘솔에 프레임을 그리고, @textjs/react는 그 프레임에 React 19를 렌더링하며, textjs는 App Router와 CLI를 추가해요.

App Router

라우트는 Next.js App Router 규칙을 따르는 app/ 안의 폴더예요. 다음은 create-text-app의 dashboard 템플릿이에요:

app/├── layout.tsx├── page.tsx├── not-found.tsx├── deployments/│   └── [id]/│       └── page.tsx└── api/    └── metrics/        └── route.ts

레이아웃은 하위의 모든 페이지를 감싸고 탐색 중에도 상태를 유지해요. <Link>는 shortcut을 받아 키 하나로 링크를 따라 이동할 수 있고, activeClassName은 현재 라우트로 향하는 링크에 스타일을 적용해요:

app/layout.tsxtsx
import type { ReactNode } from "react";import { Box, KeyHint, Text, useTheme, type Metadata } from "textjs";import Link from "textjs/link";export const metadata: Metadata = { title: { default: "Acme", template: "%s · Acme" } };export default function RootLayout({ children }: { children?: ReactNode }): ReactNode {  const theme = useTheme();  return (    <Box className="flex-col w-full h-full">      <Box className="flex-row h-1 px-1 gap-3 bg-surface shrink-0">        <Text bold color={theme.fg} wrap="nowrap">          ▼ Acme        </Text>        <Link href="/" shortcut="1" className="text-muted no-underline" activeClassName="text-fg bold">          1 Metrics        </Link>        <Link href="/deployments/dpl_a1" shortcut="2" className="text-muted no-underline" activeClassName="text-fg bold">          2 Latest deploy        </Link>      </Box>      <Box className="grow flex-col px-2 pt-1">{children}</Box>      <Box className="flex-row h-1 gap-2 pl-6 shrink-0 bg-surface">        <KeyHint keys="1-2" label="navigate" />        <KeyHint keys="tab" label="focus" />        <KeyHint keys="F2" label="devtools" />      </Box>    </Box>  );}

동적 세그먼트는 Next.js와 같은 방식으로 작동해요. [id]는 매개변수가 되고, [...slug]는 경로의 나머지를 포착하며, [[...slug]]는 상위 경로에도 일치해요. 정적 세그먼트는 동적 세그먼트보다 우선하고, (marketing) 같은 라우트 그룹은 URL에 경로를 추가하지 않고 파일을 정리해요.

app/deployments/[id]/page.tsxtsx
import { Box, Text, useInput } from "textjs";import { notFound, useParams, useRouter } from "textjs/navigation";import { DEPLOYMENTS } from "../../../lib/data";export default function DeploymentPage() {  const router = useRouter();  const { id } = useParams<{ id: string }>();  const deployment = DEPLOYMENTS.find((d) => d.id === id);  useInput((e) => {    if (e.key === "escape") router.back();  });  if (!deployment) notFound();  return (    <Box className="flex-col gap-1">      <Text bold>{deployment.id}</Text>      <Text>{deployment.branch}</Text>    </Box>  );}

loading.tsx와 error.tsx는 세그먼트를 각각 Suspense 경계와 에러 바운더리로 감싸요. 오류 컴포넌트는 오류와 reset 함수를 받아요:

app/deployments/error.tsxtsx
import { Box, Text, useInput, type ErrorComponentProps } from "textjs";export default function DeploymentsError({ error, reset }: ErrorComponentProps) {  useInput((e) => {    if (e.key === "r") reset();  });  return (    <Box className="flex-col grow items-center justify-center gap-1">      <Text bold>Something went wrong loading deployments.</Text>      <Text>{error.message}</Text>    </Box>  );}

라우트 핸들러는 app/api에 있고 표준 Response를 반환해요:

app/api/metrics/route.tsts
export function GET(): Response {  return Response.json({ cpu: Math.random(), memory: Math.random(), at: new Date().toISOString() });}

라우터는 history.pushState로 window.location을 동기화하므로 뒤로 가기, 앞으로 가기, 딥 링크가 작동하고, metadata.title은 document.title에 기록돼요. 앱은 콘솔에 화면을 그리고, 페이지에는 입력을 수집하는 포커스 패드가 있어요.

CLI

  • text dev는 React Fast Refresh와 함께 Vite를 시작해요. 파일을 저장하면 현재 콘솔 화면이 갱신돼요.
  • text build는 앱과 라우트 핸들러의 프로덕션 빌드를 생성해요.
  • text start는 빌드와 라우트 핸들러를 제공해요.

create-text-app은 starter 또는 dashboard 템플릿으로 프로젝트의 기본 구조를 만들어요.

검사기

실행 중인 앱에서 Ctrl+` 또는 F2를 눌러 콘솔 안의 [▼] 검사기를 여세요. 일치하는 라우트와 매개변수, 프레임 크기, 스팬 수와 단계별 소요 시간, dirty gate가 건너뛴 프레임 수를 보여 줘요. W는 Yoga 레이아웃을 와이어프레임으로 그려요.

0.1의 추가 기능

@textjs/react는 PascalCase 컴포넌트(Box, Text, ScrollBox, Input, Textarea, Select, Tabs, Code, Markdown, Diff, Modal, Spinner), 셀 단위로 작동하는 Tailwind 스타일의 className 유틸리티, 테마를 제공해요. 레이아웃은 WebAssembly의 Yoga에서 실행돼요.

전체 목록은 변경 기록에 있어요.