사전 결합 문자
개념

결합 문자

gabury1고친 사람 github-actions[bot]

결합 문자는 앞 글자에 부호를 얹어 새 글자를 만들어 줍니다. 혼자서는 글자가 되지 못하고 바로 앞 글자와 한 덩이로 그려집니다. 덕분에 유니코드가 글자마다 붙이는 번호를 글자와 부호의 조합마다 따로 만들지 않아도 됩니다. 대신 눈에 보이는 글자 하나가 번호 여럿이 되어 길이를 세거나 문자열을 견줄 때 손이 더 갑니다.

쉽고 빠른 이해

앞 글자에 붙어 그 글자를 바꾸는 부호입니다. e 뒤에 강세 부호를 붙이면 화면에는 é 한 글자가 나옵니다.

글자와 부호의 조합은 끝없이 많습니다. 유니코드는 글자마다 번호를 하나씩 붙입니다. 조합마다 번호를 새로 만들면 번호표가 한없이 늘어납니다. 부호를 따로 두면 있는 글자에 얹기만 하면 됩니다.

순서는 이렇습니다.

  1. 바탕이 되는 글자를 먼저 적습니다
  2. 그 뒤에 얹을 부호를 하나 이상 이어 적습니다
  3. 화면은 이 묶음을 글자 하나로 그립니다

대가도 있습니다. 같은 글자를 부호째 번호 하나로 적는 방법도 있습니다. 그래서 눈에 같은 두 문자열이 번호로는 달라집니다. 문자열을 번호 단위로 자르면 부호만 떨어져 나가기도 합니다.

번호 하나짜리가 따로 없는 조합을 적을 때나 부호를 떼어 낼 때 이렇게 나눠 적습니다. 대신 비교하거나 검색하기 전에는 두 적는 법을 한 꼴로 맞춥니다.

상세

케이크 가게를 떠올려 봅니다. 가게는 빵 몇 가지와 토핑 몇 가지를 따로 준비해 둡니다. 손님이 빵 하나를 고르고 그 위에 얹을 토핑을 고르면 케이크 하나가 나옵니다. 조합마다 완성된 케이크를 미리 만들어 둘 필요가 없습니다.

빵은 바탕이 되는 글자에, 토핑은 결합 문자에 해당합니다. 케이크 한 개가 화면에 보이는 글자 하나입니다.

이 절은 결합 문자를 é 와 ệ 두 글자로 따라갑니다. 부호 하나를 얹는 경우와 여럿을 얹는 경우를 보고, 이런 부호를 왜 따로 두는지 봅니다. 뒤의 소절들은 결합 문자의 종류와 헷갈리는 이웃을 가릅니다. 마지막 소절에서는 백엔드 코드가 어디서 부딪히는지 봅니다.

앞 글자에 얹히는 번호

Unicode(유니코드)는 세상의 글자마다 번호를 하나씩 붙입니다. 이 번호를 코드 포인트라고 부릅니다. 적을 때는 U+ 뒤에 십육진수를 씁니다. 프로그램은 문자열을 이 번호의 열로 다룹니다.

대부분의 번호는 혼자서 글자 하나가 됩니다. e 는 U+0065 하나로 끝납니다. 그런데 혼자서는 글자가 되지 못하는 번호가 있습니다. 이 번호는 바로 앞 글자에 얹혀서만 그려집니다. 이것이 결합 문자입니다.

U+0301 이 그 예입니다. e 뒤에 이 번호를 붙이면 화면에는 é 한 글자가 나옵니다. 글자 위에 비스듬히 얹히는 이 부호를 강세 부호라고 부릅니다.

결합 문자가 얹히는 앞 글자를 기본 문자라고 부릅니다. 순서는 늘 기본 문자가 먼저이고 결합 문자가 뒤입니다. 결합 문자는 자기보다 앞에 온 기본 문자에 붙기 때문입니다.

강세 부호처럼 글자의 소리를 구별하려고 붙이는 부호를 통틀어 발음 구별 부호라고 합니다. 자주 쓰는 발음 구별 부호는 U+0300 부터 U+036F 까지의 한 구간에 모여 있습니다. 이 구간의 번호는 모두 결합 문자입니다.

부호를 여럿 얹는 경우

기본 문자 하나에 결합 문자를 여럿 붙일 수 있습니다. 베트남어의 ệ 는 e 아래에 점을 찍고 위에 꺾쇠 모양의 곡절 악센트를 얹은 글자입니다. 번호로는 e 뒤에 아래 점 U+0323 과 곡절 악센트 U+0302 를 이어 적습니다.

기본 문자 하나와 그 뒤에 붙은 결합 문자들의 묶음을 결합 문자 시퀀스라고 부릅니다. 뒤따르는 부호들은 앞 부호에 붙는 것이 아니라 모두 같은 기본 문자에 얹힙니다. 저장된 번호 열은 셋이지만 화면에는 글자 하나로 나옵니다.

flowchart TD
    subgraph S["저장된 번호 열"]
        B["② U+0323 · 아래 점 · 결합 문자"]
        C["③ U+0302 · 곡절 악센트 · 결합 문자"]
        A["① U+0065 · e · 기본 문자"]
        B -- 얹힘 --> A
        C -- 얹힘 --> A
    end
    S --> D["화면의 글자 하나 · ệ"]

그림 위 상자가 문자열 안에 든 번호 셋입니다. 동그라미 숫자는 적는 순서입니다. 화살표는 두 부호가 모두 e 에 얹힌다는 뜻입니다. 맨 아래가 사람이 보는 글자입니다.

사람이 한 글자로 세는 이런 묶음을 그래핌 클러스터라고 부릅니다. 결합 문자 시퀀스는 그래핌 클러스터를 이루는 대표적인 방식입니다.

아래 점과 곡절 악센트는 서로 부딪히지 않아서 어느 순서로 적어도 같은 글자로 그려집니다. 같은 글자에 번호 열이 둘 생기는 셈입니다.

이것을 한쪽으로 맞추려고 결합 문자마다 붙는 위치를 나타내는 값을 하나 더 붙여 둡니다. 이 값을 정준 결합 클래스라고 부릅니다. 아래 점은 220, 곡절 악센트는 230 입니다. 뒤에서 다룰 유니코드 정규화는 이 값이 작은 부호부터 앞에 오도록 줄을 세웁니다. 그래서 아래 점(220)이 곡절 악센트(230) 앞에 옵니다.

부호를 따로 두는 까닭

글자와 부호의 조합은 끝이 없습니다. 국제 음성 기호로 발음을 적을 때는 한 글자에 부호를 둘셋씩 얹기도 합니다. 조합마다 번호를 새로 만들면 새 조합이 나올 때마다 번호표를 고쳐야 합니다. 결합 문자를 따로 두면 있는 글자와 있는 부호를 이어 적기만 하면 됩니다. 번호 하나짜리가 따로 없는 조합은 이렇게 이어 적어야 나타낼 수 있습니다.

그런데 유니코드에는 é 전용 번호 U+00E9 도 있습니다. 부호까지 합쳐 번호 하나로 만든 이런 글자를 미리 합성된 문자라고 부릅니다.

이런 번호가 남은 까닭은 유니코드 이전의 번호표에 있습니다. 그때는 나라나 회사마다 따로 만든 글자 번호표인 문자 집합을 썼습니다. 여러 문자 집합이 é 같은 글자를 한 칸으로 갖고 있었습니다. 그 번호표로 쓰인 문서를 옮겨 올 때 한 글자가 한 글자로 대응되도록 이 번호들을 남겨 두었습니다.

그 결과 é 를 적는 방법이 둘이 됩니다. 미리 합성된 문자 하나로 적거나, 기본 문자와 결합 문자 둘로 적습니다. 화면에는 같아도 번호 열이 달라서 프로그램은 두 문자열을 다르게 봅니다. 두 꼴을 한쪽으로 맞추는 일이 유니코드 정규화입니다.

결합 문자의 세 갈래

유니코드는 번호마다 그 번호의 성질을 적은 값을 여럿 붙여 둡니다. 그중 하나가 그 번호가 글자인지, 숫자인지, 부호인지를 가르는 일반 범주입니다. 결합 문자는 일반 범주가 M(Mark, 부호)으로 시작하는 번호입니다.

M 은 다시 셋으로 갈립니다. 부호가 옆으로 폭을 차지하는지, 앞 글자를 바깥에서 감싸는지가 기준입니다.

일반 범주 이름 하는 일 예
Mn 비간격 부호 (Nonspacing Mark) 폭을 차지하지 않고 앞 글자 위나 아래에 얹힙니다 강세 부호 U+0301 · 아래 점 U+0323
Mc 간격 부호 (Spacing Mark) 옆으로 폭을 차지하며 앞 글자에 붙습니다 데바나가리 모음 기호 ि U+093F
Me 둘러싸는 부호 (Enclosing Mark) 앞 글자를 바깥에서 감쌉니다 둘러싸는 원 U+20DD

앞 소절까지 본 부호는 모두 Mn 입니다. 라틴 문자의 발음 구별 부호가 여기 듭니다.

Mc 는 힌디어를 적는 데바나가리 문자에서 봅니다. कि 는 자음 क(U+0915) 뒤에 모음 기호 ि(U+093F)를 적은 것입니다. ि 는 Mn 부호와 달리 자기 폭을 차지해서 글자가 옆으로 넓어집니다.

이 예는 저장하는 순서와 그리는 위치가 다를 수 있다는 것도 보여 줍니다. 화면에서는 모음 기호가 자음의 왼쪽에 그려집니다. 번호 열에서는 늘 기본 문자가 먼저라서, 화면의 순서로 번호 열을 짐작하면 틀립니다.

Me 는 앞 글자를 바깥에서 두릅니다. 1 뒤에 U+20DD 를 붙이면 동그라미 안에 든 1 로 그려집니다.

눈에 안 보이는 Mn 도 있습니다. 변이 선택자 U+FE0F 는 앞 글자를 이모지 모양으로 그리라고 알려 주는 번호입니다. 이 번호는 모양만 바꿀 뿐 부호를 그리지 않습니다. 그래도 앞 글자에 붙으므로 결합 문자입니다.

결합 문자와 헷갈리는 이웃

번호 여럿이 글자 하나로 그려진다고 모두 결합 문자는 아닙니다. 결합 문자인지는 일반 범주가 M 인지로 가립니다.

한글이 그 예입니다. 한 은 첫소리 ᄒ(U+1112), 가운뎃소리 ᅡ(U+1161), 끝소리 ᆫ(U+11AB) 세 번호로도 적을 수 있습니다. 이 셋은 화면에서 한 음절로 합쳐집니다. 그래도 일반 범주는 글자(Lo, Other Letter)입니다. 부호가 아니라 음절을 이루는 낱자라서 한글 자모라는 이름으로 따로 다룹니다.

제로 폭 결합자도 이름에 「결합」이 들어가지만 결합 문자가 아닙니다. 이모지 둘 사이에 끼워 두 그림을 한 그림으로 이어 그리게 하는 번호입니다. 일반 범주는 서식 문자(Cf, Format)입니다. 앞 글자에 얹히는 것이 아니라 앞뒤 두 글자를 잇는다는 점이 다릅니다.

백엔드 코드에서 부딪히는 곳

결합 문자는 문자열을 번호 단위로 다루는 코드에서 문제를 냅니다. 이 소절은 café 를 기본 문자와 결합 문자로 쪼개 적은 문자열 하나로 길이와 자르기를 봅니다. 이어서 검색을 봅니다. 코드는 파이썬입니다. 오른쪽 주석은 그 줄이 내는 값입니다.

Python
import unicodedata as ud

s = "cafe\u0301"      # 화면에는 café
len(s)                 # 5
s[:4]                  # 'cafe'
ud.category(s[4])      # 'Mn'

화면에는 네 글자인데 len 은 5를 돌려줍니다. 파이썬의 문자열 길이는 코드 포인트 수이기 때문입니다. 마지막 줄은 다섯째 번호가 결합 문자라는 것을 일반 범주로 확인합니다.

앞에서 네 번호만 잘라 내면 강세 부호가 떨어져 cafe 가 남습니다. 오류도 없이 다른 낱말이 됩니다. 길이 제한에 맞추려고 문자열을 자르는 코드가 이렇게 조용히 글자를 바꿉니다.

그래서 자를 때는 그래핌 클러스터 경계에 맞춥니다. 언어마다 이 경계를 찾는 도구가 있습니다. Java 의 BreakIterator 가 그 예입니다.

검색에서는 반대 방향의 문제가 납니다. 입력한 기기나 프로그램에 따라 같은 글자가 쪼갠 꼴로 들어오기도 합니다. 사용자는 é 를 미리 합성된 문자로 입력했는데 저장된 값은 쪼갠 꼴이면 둘이 안 맞습니다. 그래서 비교하기 전에 양쪽을 유니코드 정규화로 같은 꼴에 맞춥니다.

부호를 무시하고 찾고 싶을 때도 결합 문자를 씁니다. 먼저 NFD(Normalization Form D, 정규화 형식 D)로 미리 합성된 문자를 기본 문자와 결합 문자로 쪼갭니다. 그다음 일반 범주가 Mn 인 번호를 걸러 내면 부호만 빠집니다.

Python
d = ud.normalize("NFD", "café")
len(d)                 # 5
keep = [c for c in d if ud.category(c) != "Mn"]
"".join(keep)          # 'cafe'

첫 줄의 café 는 미리 합성된 문자로 적은 네 글자입니다. 쪼갠 뒤에는 번호가 다섯이 됩니다. 부호를 걸러 내면 cafe 가 남습니다. 검색 색인에 부호를 뗀 값을 함께 넣어 두면 cafe 로도 café 를 찾습니다.

유니코드는 기본 문자 하나에 붙는 결합 문자의 개수를 막지 않습니다. 부호 수십 개를 한 글자에 쌓으면 글자가 위아래 줄까지 번져 그려집니다. 이런 글을 흔히 Zalgo 텍스트라고 부릅니다. 입력 길이를 보이는 글자 수로만 재면 글자 하나에 번호 수백 개가 들어올 수 있어서, 저장하는 쪽은 바이트 수로도 상한을 둡니다.

관련 항목

결합 문자가 얹혀 이루는 글자 단위

기본 문자 · 코드 포인트 · 결합 문자 시퀀스 · 그래핌 클러스터 · 글리프

결합 문자를 정의하는 표준과 문자 속성

Unicode · 일반 범주 · 정준 결합 클래스 · 유니코드 문자 데이터베이스 · 유니코드 블록

결합 문자의 하위 종류

비간격 부호 · 간격 부호 · 둘러싸는 부호 · 발음 구별 부호 · 변이 선택자

결합 문자 대신 번호 하나로 적는 글자와 그 출처

미리 합성된 문자 · 문자 집합 · Latin-1 · ASCII

결합 문자 열을 한 꼴로 맞추는 정규화

유니코드 정규화 · NFC · NFD · 정준 동등 · 정준 분해 · 정준 결합

결합 문자와 헷갈리는 이웃

한글 자모 · 제로 폭 결합자 · 이모지 · 합자 · 서식 문자

결합 문자 때문에 나는 문자열 문제

문자열 자르기 · 문자열 길이 · 문자열 비교 · 악센트 제거 · Zalgo 텍스트

결합 문자를 바이트로 적는 인코딩

문자 인코딩 · UTF-8 · UTF-16 · UTF-32

다른 이름: combining character · combining mark · 결합 부호 · 결합 기호