---
title: "텍스트, Bidi와 IME — zenit Zig UI 문서"
description: "바이트에서 자소 클러스터, 양방향 단락, IME 조합까지. zenit은 텍스트를 코드 포인트마다 늘어놓은 글리프 행이 아니라 시스템 기능으로 다룹니다."
url: https://zenit.z.express/ko/docs/guide/text-engine
language: ko
alternate_en: https://zenit.z.express/docs/guide/text-engine.md
alternate_zh: https://zenit.z.express/zh/docs/guide/text-engine.md
alternate_es: https://zenit.z.express/es/docs/guide/text-engine.md
alternate_ja: https://zenit.z.express/ja/docs/guide/text-engine.md
alternate_fr: https://zenit.z.express/fr/docs/guide/text-engine.md
alternate_de: https://zenit.z.express/de/docs/guide/text-engine.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 텍스트, Bidi와 IME

바이트에서 자소 클러스터, 양방향 단락, IME 조합까지. zenit은 텍스트를 코드 포인트마다 늘어놓은 글리프 행이 아니라 시스템 기능으로 다룹니다.

## 한 줄의 텍스트, 네 개의 계층

저장, 편집 경계, 방향 해석, 플랫폼 셰이핑은 각각 정확히 하나의 authority를 가지며, 함께 히트 테스트와 선택이 가능한 시각적 줄을 만듭니다. 어떤 계층도 다른 계층을 대신해 결정하지 않습니다.

TEXT STACK

처음 두 계층은 src/text\_core에, 양방향 알고리즘은 src/i18n에 있으며, macOS에서 셰이핑은 CoreText가 담당합니다.

| 계층 | 담당 | 위치 |
| --- | --- | --- |
| UTF-8 문서 좌표 | 안정적인 byte offset, 모든 편집 진입점에서 경계로 정규화 | `src/text_core` |
| UAX #29 자소 | 커서, 삭제, 선택이 ZWJ 이모지, 결합 문자, 국기를 절대 쪼개지 않음 | `src/text_core/grapheme.zig` |
| UAX #9 Bidi | 단락 방향, isolate, embedding, 괄호 쌍, 논리 ↔ 시각 매핑 | `src/i18n/bidi.zig` |
| CoreText 셰이핑 | 폰트 fallback, 합자, glyph run, 시각적 배치와 caret x | `src/render/text_renderer.zig` |

## 코드 포인트가 아닌 자소

가족 이모지는 7개의 Unicode scalar(사람 4명과 ZWJ 3개)이고, `é`는 `e`에 결합 양음 부호를 더한 것일 수 있으며, 국기는 지역 표시자 두 개입니다. 사용자에게는 각각이 한 글자입니다. zenit은 확장 자소 클러스터를 커서 이동, 삭제, 선택, 용량 절단의 원자 단위로 사용합니다.

UAX #29

cursor\_pos는 UTF-8 바이트 오프셋이며 항상 클러스터 경계에만 놓입니다. 한 줄 Input의 2048바이트 용량 절단조차 자소를 반쪽으로 남기지 않습니다.

[Video](https://zenit.z.express/media/stories/emoji.mp4?v=5f65f98444)

컬러 이모지는 BGRA 아틀라스 페이지와 셰이더의 컬러 분기를 거칩니다. 그레이스케일 대조군은 회색으로 유지되고, 혼합된 run은 일반 텍스트 파이프라인을 공유합니다.

[Video](https://zenit.z.express/media/stories/wordnav.mp4?v=1fd0aa8709)

Alt+← / →는 그리스 문자와 키릴 문자 단어를 단어 단위로 건너뛰고, 더블 클릭은 단어를 선택하며, 가족 이모지는 항상 하나의 클러스터로 유지됩니다.

## 양방향 텍스트

텍스트는 메모리에 논리 순서로 저장되고 화면에는 시각 순서로 표시됩니다. UAX #9는 먼저 모든 문자에 대해 임베딩 레벨을 해석한 다음, 규칙 L2에 따라 가장 높은 레벨부터 차례로 run을 뒤집습니다. 아래는 LTR 단락 안의 `abc אבג 123`입니다. 히브리 문자는 레벨 1로, 그 뒤의 숫자는 레벨 2로 해석됩니다.

UAX #9

숫자 자체는 여전히 왼쪽에서 오른쪽으로 읽히지만, 숫자를 포함한 RTL 구간 전체가 하나의 블록으로 오른쪽에서 왼쪽으로 배치됩니다.

UAX #9 해석기는 `src/i18n/bidi.zig`에 있는 zenit 자체의 이식 가능한 구현이며, 속성 테이블은 고정된 Unicode 17.0.0 데이터에서 생성됩니다. macOS에서 최종 글리프 셰이핑과 RTL 결합은 여전히 run 단위로 통째로 CoreText에 넘겨집니다. 텍스트 렌더러는 RTL run을 코드 포인트마다 나누지 않습니다.

[![zenit Storybook showing mixed Arabic, Hebrew and Latin text](https://zenit.z.express/media/unicode-bidi.png?v=d1e0e89e51)](https://zenit.z.express/media/unicode-bidi.png?v=d1e0e89e51)

실제 Storybook: 아랍 문자, 히브리 문자, latin + arabic + latin 혼합을 하나의 텍스트 파이프라인이 배치합니다. 가상 커서는 RTL story 위에 있습니다.

[Video](https://zenit.z.express/media/stories/rtl.mp4?v=bc200dee27)

RTL 및 방향이 혼합된 단락에서의 커서 이동과 선택.

## Unicode 17 적합성 게이트

런타임 속성 테이블은 저장소에 고정된 Unicode 17.0.0 데이터에서 생성됩니다. 공식 테스트 파일은 `vendor/unicode/17.0.0`에 수정 없이 포함되며 `SHA256SUMS`에서 SHA-256으로 고정됩니다. 여기서 ‘지원’이란 이모지 몇 개로 단위 테스트를 작성하는 것이 아니라 공식 테스트 스위트 전체를 실행한다는 뜻입니다.

**490,846**BidiTest 유형 시퀀스

**770,241**단락 방향 판정

**91,707**BidiCharacterTest 시퀀스

**766**GraphemeBreakTest 케이스

```sh
python3 tools/generate_grapheme_data.py --check
python3 tools/generate_bidi_data.py --check
(cd vendor/unicode/17.0.0 && shasum -a 256 -c SHA256SUMS)
zig build test-text-core
zig build test-bidi-conformance
```

`--check`는 생성된 Zig 테이블이 데이터와 일치하는지 확인합니다. `test-text-core`는 GraphemeBreakTest의 모든 케이스를 실행하고, `test-bidi-conformance`는 두 개의 공식 bidi 파일 전체를 (UAX #9 규칙 L2까지) 실행하며 `zig build test-headless`에도 포함됩니다.

## 일급 시민인 IME

IME는 최종 문자를 한 번의 키 입력으로 위장하는 것이 아닙니다. 플랫폼 이벤트는 단계를 유지합니다. `ime_preedit`는 조합 중인 텍스트와 그 안의 커서 오프셋을, `ime_commit`은 최종 텍스트를 전달합니다. 조합하는 동안 [Input](https://zenit.z.express/ko/components/input)과 Textarea는 밑줄이 그어진 marked text를 렌더링하지만, canonical buffer는 커밋할 때만 바뀝니다. 별도의 “취소” 이벤트는 없으며, 빈 preedit가 조합을 취소합니다.

IME LIFECYCLE

preedit와 commit은 두 종류의 이벤트이지 한 문자열의 스냅숏이 아닙니다. 커밋 후 input은 잠시 commit\_pending\_end 상태에 머물며 플랫폼이 이어서 보낼 수 있는 같은 텍스트의 중복을 흡수한 뒤 idle로 돌아갑니다.

| 일급의 의미 | 구체적 계약 |
| --- | --- |
| 독립된 이벤트 | `ime_preedit` / `ime_commit`가 단계를 유지. 빈 preedit는 취소 |
| 재변환 | 두 이벤트 모두 replacement range(UTF-8 바이트 구간)를 가질 수 있어 커밋된 텍스트를 조합 상태로 되돌리거나 제자리에서 교체 |
| 시각적 조합 | marked range 밑줄과 강조, 조합 커서, 후보 창 위치가 시각적 줄을 따라감 |
| 편집 안전성 | 커서, 삭제, 선택, 용량 절단이 모두 확장 자소 경계에 놓임 |
| 테스트 가능 | harness가 preedit / commit을 따로 주입하고 `ime_phase`, `ime_preedit_len`, `buffer`, `cursor_pos`, `anchor`를 다시 읽음 |
| 실제 시스템 게이트 | `scripts/verify_ime.sh`가 시스템 병음 / 일본어 입력 소스로 NSTextInputClient 전체 경로를 구동 |

[![Input showing underlined nihongo preedit text](https://zenit.z.express/media/ime-preedit.png?v=6c12ff351b)](https://zenit.z.express/media/ime-preedit.png?v=6c12ff351b)

Preedit: 밑줄 그어진 marked text와 I-beam이 보이며, canonical buffer는 아직 바뀌지 않았습니다.

[![Input showing committed 日本語](https://zenit.z.express/media/ime-commit.png?v=f665179c69)](https://zenit.z.express/media/ime-commit.png?v=f665179c69)

Commit: 日本語가 canonical buffer에 들어가고 preedit가 비워집니다.

## IME 테스트

E2E 클라이언트는 두 단계를 별도의 RPC로 노출하고, `inputState`는 test id로 입력 필드의 실제 상태를 다시 읽습니다.

`ime.test.ts`

```ts
import { imeCommit, imePreedit, inputState } from "./client";

// The input must already have focus (e.g. click it first).

await imePreedit("nihongo");
let s = await inputState("story.input.name");
// s.ime_phase === "composing", s.ime_preedit_len === 7, s.buffer === ""

await imeCommit("日本語");
s = await inputState("story.input.name");
// s.buffer === "日本語", s.ime_preedit_len === 0

await imePreedit("");   // empty preedit cancels an active composition
```

[Video](https://zenit.z.express/media/e2e-text-ime.mp4?v=efa93e6b3f)

harness는 실제 Retina 창에서 Input에 포커스를 주고 preedit와 commit을 따로 주입한 뒤 RTL과 Checkbox로 넘어갑니다. I-beam, 손 모양 커서, 클릭 펄스는 모두 가상 커서가 drawable에 그립니다.

> TIP
> 
> **주입과 실제 IME는 서로 다른 것을 테스트합니다.** harness 주입은 단계, buffer, 픽셀 단언을 반복 가능하게 만듭니다. 후보 창, 입력 소스 전환, NSTextInputClient 통합은 여전히 `scripts/verify_ime.sh`의 실제 시스템 경로로 검증합니다(GUI 세션과 손쉬운 사용 권한이 필요하며, 실행 중 약 8초간 키보드에서 손을 떼야 합니다).

harness의 전체 사용법은 [E2E 자동화](https://zenit.z.express/ko/docs/advanced/e2e)를 참고하세요.
