---
title: "UniText 는 글리프 고르기만 HarfBuzz 에 맡기고 나머지를 C# 으로 짰다"
description: "TextMeshPro 는 글자 하나에 글리프 하나를 붙여서 아랍어와 섞인 방향을 그리지 못합니다. UniText 는 글리프 고르기를 HarfBuzz 에 맡기고 방향 분석, 줄나누기, 배치를 C# 으로 짜서 이 빈자리를 채운 uGUI 텍스트 엔진입니다."
author: "유니얼"
pubDate: 2026-09-29
updatedDate: 2026-10-06
category: "game-dev/unity/libraries"
tags: ["Unity", "라이브러리", "UI", "uGUI"]
canonical: https://blog.unialgames.com/posts/unity-unitext-why/
---
<x-repo>
<script type="application/json">
{
  "repo": "LightSideMeowshop/unitext",
  "text": "HarfBuzz 셰이핑과 유니코드 양방향 처리를 갖춘 유니티용 텍스트 엔진. 이 시리즈는 무료판 1.0.0 브랜치의 소스를 읽습니다.",
  "chips": ["1.0.0", "MIT", "Unity 2021.3+"]
}
</script>
</x-repo>

```csharp
label.text = "Hello مرحبا";   // TextMeshPro 에 넣으면 아랍어 글자가 서로 떨어지고 순서가 뒤집혀 나온다
```

유니티의 TextMeshPro 는 영어와 한국어는 잘 그리지만 아랍어, 히브리어, 힌디어, 태국어는 그대로 넣으면 깨집니다. 글자마다 그림 하나를 찾아 왼쪽부터 붙이는 방식이기 때문입니다.

UniText 는 이 문제를 풀려고 만든 uGUI 텍스트 컴포넌트입니다. **글자에 맞는 그림을 고르는 일은 HarfBuzz 라는 검증된 라이브러리에 맡기고, 그 앞뒤의 분석과 배치는 C# 으로 직접 짰습니다.** Light Side LLC 가 MIT 로 공개했고, 기준은 1.0.0 브랜치의 커밋 `2087eff` 입니다.

## TextMeshPro 는 글자 하나에 그림 하나를 붙인다

폰트 파일 안에 든 그림 한 장을 **글리프**라고 부릅니다. 문자열은 유니코드 번호(**코드포인트**)의 열이고, 텍스트를 그린다는 것은 코드포인트마다 글리프를 골라 놓는 일입니다.

TextMeshPro 는 이렇게 그립니다.

1. 폰트 에셋의 표에서 코드포인트에 해당하는 글리프 하나를 찾습니다.
2. 그 글리프를 왼쪽부터 차례로 놓습니다.
3. 폰트 에셋에 뽑아 둔 표(합자, 커닝, 기호 붙이기)가 있으면 찾아서 적용합니다.

앞뒤 글자를 보고 글리프를 바꾸는 단계가 없고, 글의 방향은 인스펙터의 불리언 하나입니다. 글자와 글리프가 일대일이고 한 방향으로만 읽는 언어에서는 이것으로 충분합니다.

## 일대일이 아닌 언어에서 깨진다

글자와 글리프가 일대일이 아닌 경우가 많습니다.

| 깨지는 방식 | 예 |
| --- | --- |
| 문맥 형태 | 아랍어 글자는 단어 안 위치(첫, 중간, 끝, 단독)에 따라 모양이 넷 |
| 합자 | 라틴의 `fi`, 아랍어의 람과 알리프. 글자 둘이 글리프 하나 |
| 재배열 | 힌디어 모음 ि 는 메모리에서는 자음 뒤, 화면에서는 자음 앞 |
| 마크 부착 | 태국어, 아랍어의 모음과 성조 기호가 기준 글자 위아래 정해진 자리에 |

방향 문제도 따로 있습니다. 아랍어와 히브리어는 오른쪽에서 왼쪽으로 읽는데, 그 안의 숫자와 영어는 왼쪽에서 오른쪽으로 읽습니다. 한 줄에 둘이 섞이면 화면에 놓는 순서가 저장된 순서와 달라집니다. 이 순서를 정하는 규칙이 유니코드 양방향 알고리즘(UAX #9)입니다.

그래서 TextMeshPro 로 아랍어를 쓰려면, 문자열을 미리 바꿔서 넣어 주는 별도 플러그인을 붙여 왔습니다. 글자를 위치별 모양의 코드포인트로 바꾸고 순서를 뒤집어 넘기는 방식입니다. UniText 는 이 일을 텍스트 엔진 안에서 하려고 만들어졌습니다.

## 글리프를 고르는 일이 셰이핑이고, HarfBuzz 가 그것을 한다

코드포인트 열을 받아 어느 글리프를 어디에 놓을지 정하는 일을 **셰이핑(text shaping)** 이라고 부릅니다. 위 표의 네 가지가 모두 셰이핑이 푸는 문제입니다.

셰이핑 규칙은 프로그램이 아니라 폰트 파일 안에 들어 있습니다. OpenType 폰트의 GSUB 표는 "이 글자들이 이렇게 놓이면 이 글리프로 바꾼다" 를, GPOS 표는 "이 글리프는 이만큼 옮긴다" 를 담습니다. 필요한 것은 언어별 규칙을 외운 코드가 아니라, 폰트의 표를 언어에 맞게 제대로 돌리는 엔진입니다.

**HarfBuzz** 가 그 엔진입니다. 오픈소스 C++ 라이브러리이고 Chrome, Firefox, Android 가 글자를 그릴 때 씁니다. 코드포인트 열과 폰트, 방향, 스크립트(문자 체계)를 넘기면 글리프 번호와 위치를 돌려줍니다.

HarfBuzz 가 하지 않는 일도 있습니다.

- 글리프를 그리지 않습니다. 번호와 위치만 돌려줍니다.
- 방향을 정하지 않습니다. 넘겨받은 방향대로 셰이핑할 뿐입니다.
- 줄을 어디서 끊을지 정하지 않습니다.

## UniText 는 셰이핑만 맡기고 나머지를 C# 으로 짰다

UniText 가 한 선택은 셋입니다.

**셰이핑은 직접 짜지 않고 HarfBuzz 를 썼습니다.** 셰이핑은 스크립트마다 명세를 다시 구현하는 일이고, 틀리면 그 언어 사용자에게만 깨져 보여서 만든 쪽이 알아채기 어렵습니다. 2006년부터 이 일을 해 온 라이브러리를 쓰는 편이 안전합니다.

**글리프를 그리는 일은 FreeType 을 썼습니다.** 폰트 파일 안의 글리프는 픽셀이 아니라 윤곽선(곡선의 좌표)으로 들어 있습니다. 화면에 그리려면 이 윤곽선을 원하는 크기의 픽셀 그림으로 바꿔야 하고, 이 일을 래스터화라고 부릅니다. **FreeType** 은 폰트 파일을 읽어 글리프를 래스터화해 주는 오픈소스 C 라이브러리입니다. 리눅스, Android, 그리고 유니티 자체의 폰트 엔진이 씁니다. HarfBuzz 가 "몇 번 글리프를 어디에" 를 정하면, FreeType 이 그 번호의 글리프를 그림으로 만듭니다.

**HarfBuzz 가 하지 않는 일은 C# 으로 직접 짰습니다.** 방향 분석(UAX #9), 스크립트 분석, 줄나누기, 배치, 메시 생성입니다. 방향 분석에는 ICU 나 FriBidi 같은 네이티브 라이브러리가 있지만, UniText 는 하나 더 묶는 대신 C# 으로 옮겼습니다(`Runtime/Unicode/BidiEngine.cs`, 런타임에서 가장 큰 파일). 플랫폼마다 빌드해야 하는 네이티브 의존성을 늘리지 않은 대신 유지보수 부담을 진 선택입니다.

네이티브 쪽은 HarfBuzz 12.x, FreeType 2.14.x, 컬러 이모지용 Blend2D 를 소스 수정 없이 **내보내는 심볼 이름 앞에 접두사만 붙여** `unitext_native` 하나로 묶었습니다. 같은 프로젝트의 다른 플러그인이 HarfBuzz 를 품고 있어도 링크 때 이름이 부딪히지 않습니다.

문자열 하나가 화면에 오는 길에서 네이티브가 끼는 곳은 두 군데뿐입니다.

<x-wiring>
<script type="application/json">
{
  "label": "문자열 하나가 화면에 오기까지 C# 과 네이티브가 주고받는 길",
  "width": 860,
  "height": 250,
  "nodes": [
    { "id": "comp", "title": "UniText 컴포넌트", "sub": "Text 대입, 더티 표시", "x": 0, "y": 8, "w": 180, "h": 54 },
    { "id": "cs", "title": "C# 분석과 배치", "sub": "방향 · 스크립트 · 줄", "x": 340, "y": 8, "w": 180, "h": 54 },
    { "id": "hb", "title": "HarfBuzz", "sub": "네이티브 · 글리프 선택", "x": 680, "y": 8, "w": 180, "h": 54 },
    { "id": "ft", "title": "FreeType", "sub": "네이티브 · SDF 굽기", "x": 680, "y": 180, "w": 180, "h": 54 },
    { "id": "mesh", "title": "C# 메시 생성", "sub": "쿼드 · 폰트별 묶음", "x": 340, "y": 180, "w": 180, "h": 54 },
    { "id": "cr", "title": "자식 CanvasRenderer", "sub": "폰트마다 하나", "x": 0, "y": 180, "w": 180, "h": 54 }
  ],
  "edges": [
    { "from": "comp", "to": "cs", "label": "코드포인트 배열", "bend": 0 },
    { "from": "cs", "to": "hb", "label": "런 하나씩", "bend": 18 },
    { "from": "hb", "to": "cs", "label": "글리프 번호와 이동량", "bend": 18 },
    { "from": "cs", "to": "ft", "label": "아틀라스에 없는 글리프", "bend": 0 },
    { "from": "cs", "to": "mesh", "label": "위치가 정해진 글리프", "bend": 0 },
    { "from": "ft", "to": "mesh", "label": "아틀라스 칸의 UV", "bend": 0 },
    { "from": "mesh", "to": "cr", "label": "폰트별 메시", "bend": 0 }
  ],
  "steps": [
    { "edge": 0, "why": "UTF-16 문자열을 코드포인트 배열로 접습니다. 이 뒤로는 모든 번호가 char 가 아니라 코드포인트 기준입니다." },
    { "node": "cs", "why": "C# 이 방향(UAX #9), 스크립트(UAX #24), 줄바꿈 기회(UAX #14)를 계산하고, 방향과 스크립트와 폰트가 같은 구간(런)으로 자릅니다." },
    { "edge": 1, "why": "런 하나를 HarfBuzz 에 넘깁니다. 방향과 스크립트는 C# 이 이미 정해서 같이 넘깁니다." },
    { "edge": 2, "why": "HarfBuzz 는 폰트의 GSUB, GPOS 규칙을 돌려 글리프 번호와 이동량을 돌려줍니다. 어느 글리프를 쓸지는 여기서만 정해집니다." },
    { "node": "cs", "why": "돌아온 글리프로 C# 이 줄을 나누고, 줄 안의 런을 시각 순서로 뒤집고, 정렬해서 좌표를 매깁니다." },
    { "edge": 3, "why": "아직 아틀라스에 없는 글리프만 FreeType 에 넘겨 SDF 로 굽습니다. 텍스처 업로드는 폰트마다 한 번입니다." },
    { "edge": 4, "why": "좌표가 정해진 글리프가 메시 생성으로 갑니다." },
    { "edge": 5, "why": "구운 글리프가 놓인 아틀라스 칸의 UV 가 쿼드에 붙습니다." },
    { "edge": 6, "why": "폰트마다 아틀라스가 다르니 메시도 폰트마다 하나입니다. 숨은 자식 CanvasRenderer 가 하나씩 맡아 그립니다." }
  ],
  "note": "글리프를 고르는 일과 굽는 일만 네이티브입니다. 그 앞의 분석과 뒤의 배치, 메시는 전부 C# 입니다."
}
</script>
</x-wiring>

| | TextMeshPro | UniText |
| --- | --- | --- |
| 글리프 고르기 | 코드포인트마다 하나, 폰트 에셋의 표 조회 | HarfBuzz 셰이핑 |
| 방향 | 인스펙터의 불리언 하나 | UAX #9 를 C# 으로 구현 |
| 글리프 굽기 | 폰트 에셋 아틀라스 | FreeType 으로 처음 쓰일 때 SDF 로 구움 |

## 무료판에도 파이프라인이 전부 있다

README 는 유료판 Folio 가 "SDF/MSDF rendering" 을 더한다고 적어 무료판에는 SDF 가 없는 것처럼 읽히지만, 무료판 기본 폰트 에셋의 렌더 모드가 바로 SDF 이고 기본 머티리얼도 `UniText/Mobile/SDF` 입니다. 코드포인트 분해, 방향과 스크립트 분석, 셰이핑, 줄나누기, 굽기, 메시가 모두 무료판에 있고, 유료판이 더하는 것은 MSDF, 가변 폰트, 효과 같은 표현 쪽입니다.

`Defaults/` 에 든 폰트는 `NotoSans-Regular`, `NotoSansArabic-Regular`, `NotoSansHebrew-Regular` 셋이고, 이 순서로 묶은 폰트 스택이 기본값입니다. 상자를 열자마자 오른쪽에서 왼쪽으로 읽는 글이 나오게 짠 구성입니다. 한국어, 중국어, 일본어 폰트는 없습니다.

## 컴포넌트 하나에 에셋 둘을 물린다

폰트 데이터(`UniTextFont`), 폴백 순서(`UniTextFontStack`), 머티리얼(`UniTextAppearance`)을 에셋 셋으로 나눴고, 컴포넌트에는 스택과 외형 둘을 넣습니다. 캔버스 아래 오브젝트에 코드로 붙이는 가장 짧은 모양입니다.

```csharp
using LightSide;
using UnityEngine;

public class Hello : MonoBehaviour
{
    [SerializeField] UniTextFontStack fontStack;     // Defaults/UniTextFonts_Default
    [SerializeField] UniTextAppearance appearance;   // Defaults/UniTextAppearance_Default

    void Start()
    {
        var uniText = gameObject.AddComponent<UniText>();
        uniText.FontStack = fontStack;      // 코드로 붙이면 기본값이 안 들어온다
        uniText.Appearance = appearance;
        uniText.FontSize = 36;
        uniText.Text = "Hello مرحبا";
    }
}
```

스택 하나로 여러 언어가 나옵니다. 라틴 글자는 첫 폰트가, 아랍어는 둘째 폰트가 맡습니다. `Text` 에 넣은 문자열은 이 줄에서 처리되지 않고, 캔버스가 그리기 직전에 바뀐 컴포넌트를 모아 한꺼번에 처리합니다.

## 주의할 점

### 1. Unity 6000.5 이상에서는 그대로 컴파일되지 않는다

**안 지키면 생기는 문제.** 6000.5 는 `GetInstanceID()` 사용 중지를 에러로 올려 `UniTextFont.cs` 와 에디터 파일 하나가 CS0619 로 막히고, 6000.6 은 함께 오는 uGUI 2.6 의 `ILayoutElement` 에 `maxWidth`, `maxHeight` 가 늘어 그보다 먼저 CS0535 가 납니다. 업스트림에는 수정이 없습니다.

**이렇게 쓴다.** 패키지를 임베드해서 6000.5 는 `(int)GetEntityId().GetRawData()` 로 두 줄을, 6000.6 은 거기에 `LayoutUtility.DefaultMaxSize` 를 돌려주는 두 속성을 더해 네 줄을 고칩니다. 이 네 줄을 넣은 판에서 샘플 씬 다섯 개는 에디터, Mono, IL2CPP 개발 빌드 모두 에러 0 건이었습니다. 이 판에서 런타임에 폰트를 만들고 부수면 ID 가 재사용되는 문제가 있어, 부수기 전에 셰이퍼 캐시를 비웁니다.

### 2. 코드로 붙인 컴포넌트에 폰트 스택과 외형을 직접 넣는다

**안 지키면 생기는 문제.** Project Settings > UniText 의 기본값은 인스펙터에서 붙일 때만 들어갑니다. `AddComponent<UniText>()` 로 붙이고 `Text` 만 넣으면 에러 없이 빈 칸만 남습니다.

**이렇게 쓴다.** 인스펙터에서 붙인 프리팹을 인스턴스화하거나, `FontStack` 과 `Appearance` 를 필드로 받아 `Text` 보다 먼저 대입합니다.

### 3. 한국어 폰트는 직접 넣는다

**안 지키면 생기는 문제.** 기본 스택은 라틴, 아랍어, 히브리어 셋뿐이라 한글은 어느 폰트에서도 찾지 못합니다.

**이렇게 쓴다.** 한글 폰트로 `UniTextFont` 에셋을 만들어 스택에 더합니다. 대부분의 글자를 덮는 폰트를 스택 앞에 둡니다.

## 정리

UniText 는 **글리프를 고르고 굽는 일만 네이티브에 맡기고, 그 앞뒤를 C# 으로 짠** 유니티 텍스트 엔진입니다.

- 셰이핑은 HarfBuzz, 굽기는 FreeType 이고 둘은 심볼 이름만 바꿔 한 바이너리로 묶였습니다. 방향 분석, 줄나누기, 배치, 메시는 C# 입니다.
- 무료판에도 파이프라인이 전부 있고 기본 렌더 모드는 SDF 입니다. 동봉 폰트는 라틴, 아랍어, 히브리어뿐입니다.
- 컴포넌트에는 폰트 스택과 외형이 필요하고, 코드로 붙이면 직접 넣어야 합니다.