Ink는 React를 브라우저가 아니라 터미널에서 쓰게 해주는 React 렌더러다. 평소 웹에서 <div>·<span>으로 화면을 짜듯, Ink에서는 <Box>·<Text>로 화면을 짜면 그 결과가 HTML이 아니라 까만 터미널 창의 글자들로 그려진다. useState·useEffect 같은 React 훅이 전부 그대로 동작하므로, React를 아는 사람에게 학습 비용은 거의 0이다.
예전 터미널 출력은 글자를 한 칸씩 손으로 칠하는 명령형(imperative) 방식이었다. Ink는 "여기 박스, 그 안에 텍스트, 옆에 진행바"라고 선언만 하면 알아서 그려주는 선언형(declarative) 방식이다.
핵심은 React의 숨은 부품 react-reconciler다. 이 부품 덕에 React는 "상태가 바뀌면 무엇을 다시 그릴지 계산하는 엔진"과 "실제로 어디에 그리는 렌더러"가 분리돼 있다. Ink는 그 렌더러 자리에 터미널을 끼워 넣은 것이다.
chalk는 글자에 색만 입힌다. inquirer는 질문-답변 한 사이클만 처리한다. 실시간으로 갱신되는 대시보드를 만들려면 blessed·ncurses로 커서 좌표와 화면 지우기를 손수 관리해야 해서 수백~수천 줄이 든다. "상태가 바뀌면 화면도 바뀐다"는 흐름을 직접 구현해야 한다.
상태가 바뀌면 바뀐 부분만 골라 다시 그린다(Reconciler). 레이아웃은 Flexbox(Yoga)로 잡는다. ncurses로 1000줄짜리 인터랙티브 TODO가 Ink에선 100줄로 줄어든다. AI CLI 시대가 오면서 "예쁘고 동적인 터미널 UI"가 산업 표준이 되자, 2017년 라이브러리가 2026년에 다시 폭발했다.
Claude Code, GitHub Copilot CLI, Gemini CLI 같은 차세대 도구는 모두 터미널에서 돈다. 스트리밍 응답, 진행 스피너, 인터랙티브 승인 프롬프트, 실시간 토큰 카운터가 필요하다. 이 UI를 가장 빠르게 만드는 방법이 Ink라서, AI CLI가 늘어날수록 Ink 채택도 함께 늘었다.
| 비교 대상 | Ink의 위치 |
|---|---|
| chalk | 색칠 전용 저수준 도구. Ink는 chalk를 내부적으로 쓰면서 그 위에 컴포넌트/레이아웃/상태 계층을 얹는다 |
| inquirer / prompts | 일회성 질문-답변 흐름 전용. Ink는 지속적으로 갱신되는 풀 UI를 만든다 (둘을 같이 쓰기도 함) |
| blessed / ncurses | 저수준 화면 제어 라이브러리. 강력하지만 명령형이라 복잡. Ink는 선언형으로 코드량을 1/10로 |
| React Native / R3F | 같은 react-reconciler 형제. RN은 모바일, R3F는 3D, Ink는 터미널을 캔버스로 삼는다 |
Ink 코드베이스는 거의 전부 TypeScript다. 빌드 산출물(build/)은 JS + .d.ts 타입 선언으로 npm에 배포된다.
※ 비율은 레포 구성상의 근사치다. 핵심은 "거의 단일 언어(TS) 코드베이스"라는 점.
| 패키지 | 역할 | 상세 |
|---|---|---|
| react-reconciler | React 엔진 | JSX 트리의 변경분만 계산. Ink가 호스트 설정(HostConfig)을 구현해 터미널에 연결 |
| react / scheduler | 코어 React | peerDependency. v7은 React 19.2+를 요구 |
| yoga-layout | 레이아웃 엔진 | Meta의 Flexbox 계산기. 박스 위치·크기를 CSS Flexbox 규칙대로 산출 |
| chalk / ansi-styles | 색상/스타일 | 글자에 ANSI 색·굵게·기울임 등을 입힘 |
| ansi-escapes | 커서/화면 제어 | 커서 이동·줄 지우기 등 ANSI 이스케이프 시퀀스 생성 |
| string-width / cli-truncate / wrap-ansi / slice-ansi | 폭 계산·자르기 | 한글·이모지 등 2칸 폭 문자를 정확히 측정하고 줄바꿈·자르기 |
| cli-boxes / cli-cursor / cli-truncate | 터미널 위젯 | 테두리 문자, 커서 숨김 등 CLI 공통 부품 |
| signal-exit / patch-console / terminal-size | 환경 제어 | 종료 정리, console.log 가로채기, 터미널 크기 감지 |
| 도구 | 역할 |
|---|---|
| ava | 테스트 러너 (Ink는 ink-testing-library로 출력 프레임을 스냅샷) |
| node-pty | 가상 터미널(pseudo-TTY)을 띄워 실제 키 입력/렌더를 테스트 |
| xo / prettier | 린트·포매팅 |
| react-devtools-core | DEV=true로 실행 시 React DevTools에서 컴포넌트 트리 검사 |
react-dom은 host를 DOM으로, React Native는 네이티브 뷰로, Ink는 host를 터미널 가상 노드로 연결한다. 연결 코드가 HostConfig — createInstance, appendChild, commitUpdate 등 약 20여 개 메서드를 구현하면 React가 그대로 동작한다.React 공식 문서로는 "Reconciler가 뭘 하는지" 감이 안 잡히지만, Ink의 reconciler.ts를 펼치면 React를 새 환경에 이식하려면 정확히 어떤 메서드를 구현해야 하는지가 코드로 보인다. React 내부를 배우기에 최고의 실물 교과서다.
flexDirection·justifyContent·gap 같은 CSS 개념이 터미널에서도 통한다.터미널은 본래 글자만 찍을 줄 안다. 색·커서 이동·줄 지우기는 글자 사이에 특수 신호를 섞어 보내는 약속으로 표현한다 — 이게 ANSI 이스케이프 시퀀스다.
\x1b[(이스케이프 문자)로 시작한다. \x1b[31m = 빨간 글씨 시작, \x1b[0m = 원래대로. Ink가 그리는 예쁜 화면도 결국 본문 + 모드 변경 신호가 잔뜩 섞인 긴 문자열 하나다.0.01초마다 화면을 다시 그리면 깜빡인다. Ink는 짧은 시간 내 여러 setState를 한 번으로 묶어 약 34ms 간격으로만 다시 그린다(throttle, 기본 30fps 기준). 또 완료된 로그처럼 다시 그릴 필요 없는 출력은 <Static>으로 빼서, 매 프레임 재계산에서 제외해 성능을 지킨다.
가장 먼저 열어볼 파일은 src/reconciler.ts(React 이식의 핵심)와 examples/(바로 실행되는 미니 앱들)다. 빌트인 컴포넌트는 6개, 훅은 십수 개로 단출해서 전체를 며칠이면 통독할 수 있다.
| 구분 | 이름 | 역할 |
|---|---|---|
| 컴포넌트 | Box | Flexbox 레이아웃 컨테이너 (가장 많이 씀) |
| 컴포넌트 | Text | 색·굵게·기울임·밑줄·취소선이 가능한 텍스트 |
| 컴포넌트 | Static | 완료 로그/리스트를 위쪽에 고정 출력 (리렌더 제외) |
| 컴포넌트 | Spacer · Newline · Transform | 여백 채우기 · 줄바꿈 · 출력 문자열 가공 |
| 훅 | useInput | 키 입력(화살표·Enter·문자) 감지 |
| 훅 | useApp | 앱 종료 등 앱 제어 (exit()) |
| 훅 | useStdin / useStdout / useStderr | 표준 입출력 스트림 접근, raw 모드 제어 |
| 훅 | useFocus / useFocusManager | Tab 키 포커스 순환 · 프로그래밍적 포커스 제어 |
배울 것: React가 "엔진 ↔ 렌더러"로 분리된 구조라는 사실. reconciler.ts를 읽으면 createInstance·appendChild·commitUpdate 같은 메서드 집합이 React를 새 환경에 이식하는 인터페이스임을 본다. "React = HTML"이라는 흔한 오해가 깨진다.
<div>·<span> 같은 HTML 태그가 React의 본질이라 오해하면, React를 다른 환경에 쓴다는 발상 자체가 막힌다.
React의 본체는 "상태가 바뀌면 무엇을 어떻게 다시 그릴지 계산하는 엔진"이지 HTML이 아니다. Ink는 같은 엔진을 빌려 결과만 터미널에 그린다. React Native는 같은 엔진으로 결과를 모바일 네이티브 뷰에 그린다.
배울 것: 브라우저 밖에서 Flexbox가 어떻게 "순수 계산"으로 동작하는지. CSS를 매일 쓰면서도 모르던 레이아웃 알고리즘의 입력(스타일)과 출력(좌표)이 분리돼 보인다.
배울 것: 색·커서·화면 제어가 모두 \x1b[ 신호임을, 한글/이모지의 문자 폭(string-width) 문제와 TTY 감지를 실전 코드로. CLI를 직접 만들 때 평생 쓰는 지식이다.
배울 것: "컴포넌트 + 상태 → 자동 갱신" 모델을 화면이 아닌 임의의 출력 대상에 적용하는 발상. 이 패턴을 익히면 React Three Fiber(3D)·react-pdf 같은 다른 커스텀 렌더러도 같은 눈으로 읽힌다.
배울 것: ink-testing-library의 lastFrame()으로 "렌더된 화면 문자열"을 스냅샷 비교하는 패턴. UI를 순수 함수처럼 테스트하는 사고방식.
| 항목 | 요구사항 | 비고 |
|---|---|---|
| Node.js | 22 이상 (engines.node >=22) | v7 기준. 구버전 Node는 이전 Ink 메이저(v4~v5)를 사용 |
| React | 19.2 이상 (peerDependency) | 직접 설치 필요: npm i ink react |
| OS | macOS / Linux / Windows | 크로스플랫폼. Windows Terminal 권장 |
| 터미널 | ANSI 지원 터미널 | 색상 수준(16/256/트루컬러)은 터미널마다 다름 → 자동 감지하나 완벽치 않음 |
| RAM/CPU | 일반 Node 앱 수준 | 무거운 런타임 아님. 렌더 자체는 CPU 거의 안 씀 |
./mycli > out.txt처럼 리다이렉트하면 받는 쪽이 터미널이 아니라 파일이라 ANSI 코드가 그대로 박혀 지저분해진다. process.stdout.isTTY로 TTY 여부를 분기해 인터랙티브 모드를 끄는 게 안전하다. Ink가 어느 정도 자동 처리하지만 실시간 UI·키 입력 부분은 직접 챙겨야 한다.
create-ink-app으로 프로젝트를 만들고, 색상·굵게가 들어간 텍스트를 띄워 React가 터미널에서 도는 마법을 체감한다.
# 스캐폴딩 (TypeScript 템플릿)
npx create-ink-app my-first-cli
cd my-first-cli && npm start
# cli.tsx 안에서
import {render, Text} from 'ink';
render(<Text color="green" bold>Hello, Ink!</Text>);
README의 대표 예제. useState + useEffect만으로 화면이 자동 갱신되는 흐름을 확인한다. 웹 React와 코드가 사실상 동일하다.
import {render, Text} from 'ink';
import {useState, useEffect} from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const t = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(t);
}, []);
return <Text color="green">{count}초 경과</Text>;
};
render(<Counter />);
useInput으로 방향키·Enter·'d'키를 받아 할 일을 추가/완료/삭제한다. <Box flexDirection="column">으로 목록을 세로 배치하고 선택 항목만 강조한다. 100줄 내로 완성 — ncurses라면 1000줄.
CPU·메모리·디스크 사용량을 1초마다 갱신하는 3단 대시보드. <Box flexDirection="row">로 칼럼을 잡고, 사용량에 따라 글자색을 초록→노랑→빨강으로 바꾼다. 완료 로그는 <Static>으로 분리해 깜빡임을 없앤다.
react-reconciler를 직접 임포트해 "트리를 콘솔에 JSON으로 찍는" 초간단 커스텀 렌더러를 만든다. HostConfig 메서드 20여 개를 채우며 React 내부를 처음으로 들여다본다 — React 학습의 분수령. 그다음 Ink의 reconciler.ts와 비교하면 "프로 구현"이 보인다.
<Static>, 입력창=useInput 패턴을 찾아보면 실전 사용례가 산다.| 요일 | 학습 내용 | 실습 |
|---|---|---|
| 월 | create-ink-app & render() | Hello World, 색상 텍스트 출력 |
| 화 | Box & Flexbox 레이아웃 | flexDirection·gap·border로 카드 배치 |
| 수 | useState/useEffect 리렌더 | 카운터·스피너 만들기 |
| 목 | useInput 키 입력 | 방향키 메뉴 네비게이션 |
| 금 | Static & 로그 출력 | 완료 작업 목록을 위쪽에 고정 |
| 요일 | 학습 내용 | 실습 |
|---|---|---|
| 월 | useFocus/useFocusManager | Tab으로 여러 입력 칸 순환 |
| 화 | ink-ui / ink-text-input 등 생태계 | 기성 입력·선택 컴포넌트 조립 |
| 수 | TTY 감지 & 비인터랙티브 분기 | 파이프 시 단순 로그로 폴백 |
| 목 | useApp / exit 흐름 제어 | 작업 완료 후 깔끔히 종료 |
| 금 | ink-testing-library | lastFrame() 스냅샷 테스트 작성 |
| 요일 | 학습 내용 | 실습 |
|---|---|---|
| 월 | react-reconciler 개념 | 공식 react-reconciler README 정독 |
| 화 | HostConfig 메서드 분석 | Ink reconciler.ts 줄 단위로 읽기 |
| 수 | 미니 커스텀 렌더러 | 콘솔에 트리 찍는 렌더러 구현(과제 5) |
| 목 | Yoga 레이아웃 엔진 | yoga-layout로 좌표 계산 직접 호출 |
| 금 | ANSI escape & string-width | 색/커서 직접 제어, 한글 폭 측정 |
| 요일 | 학습 내용 | 실습 |
|---|---|---|
| 월 | Pastel (Next.js식 CLI 프레임워크) | 파일 기반 명령 구조로 CLI 작성 |
| 화 | Claude Code 소스 패턴 읽기 | Static/useInput 사용처 분석 |
| 수 | 스트리밍 UI | AI 응답을 토큰 단위로 흘려 출력 |
| 목 | 배포 & npm bin 설정 | 전역 설치 가능한 CLI로 패키징 |
| 금 | 종합 프로젝트 | 인터랙티브 + 스트리밍 미니 CLI 완성 |
| 키워드 | 설명 |
|---|---|
| React 렌더러 | React 컴포넌트를 특정 환경(브라우저·모바일·터미널)에 실제로 그리는 부품. Ink는 터미널용 렌더러 |
| react-reconciler | 이전·이후 트리를 비교해 변경분만 골라내는 React의 엔진. 커스텀 렌더러의 핵심 의존성 |
| HostConfig | reconciler를 새 환경에 연결하는 메서드 모음(createInstance, appendChild, commitUpdate 등 ~20개) |
| 선언형 UI | "이렇게 그려라"가 아니라 "이런 상태면 이런 화면"이라고 선언하면 차이를 알아서 반영하는 방식 |
| Yoga | Meta의 Flexbox 레이아웃 엔진. 스타일을 받아 박스의 좌표·크기를 계산. Ink가 의존 |
| Flexbox | CSS의 1차원 정렬 레이아웃 모델. flexDirection·justifyContent·gap 등이 터미널에서도 동작 |
| ANSI 이스케이프 시퀀스 | \x1b[로 시작하는 터미널 제어 신호. 색·커서 이동·줄 지우기를 표현 |
| TTY | 사람이 키보드로 입력하고 화면으로 보는 인터랙티브 환경인지 여부. 파이프/리다이렉트면 비TTY |
| string-width | 한글·이모지처럼 2칸을 차지하는 문자의 실제 출력 폭을 계산하는 라이브러리 |
| throttle(쓰로틀) | 리렌더를 ~34ms 간격으로 묶어 깜빡임·성능 저하를 막는 기법(기본 30fps) |
| Box | Ink의 레이아웃 컨테이너 컴포넌트. 웹의 <div style="display:flex">에 해당 |
| Text | 색·굵게·기울임·밑줄이 가능한 텍스트 컴포넌트. 웹의 <span>에 해당 |
| Static | 완료 로그처럼 한 번 그리고 고정하는 컴포넌트. 매 프레임 재계산에서 제외돼 성능에 유리 |
| useInput | 키 입력(화살표·Enter·문자)을 감지하는 Ink 훅 |
| useFocus / useFocusManager | Tab 포커스 순환과 프로그래밍적 포커스 제어 훅 |
| ink-testing-library | lastFrame()으로 렌더된 화면 문자열을 스냅샷 비교하는 테스트 도구 |
| create-ink-app | Ink CLI 프로젝트를 즉시 스캐폴딩하는 공식 도구 |
| Pastel | Ink 위에 얹은 Next.js식(파일 기반) CLI 프레임워크. 같은 저자(Vadim Demedes) |