Raku의 유니코드 지원
Raku의 유니코드 지원 (unicode)
Raku는 유니코드 지원 수준이 매우 높아요. 최신 버전은 유니코드 17.0을 지원합니다. 이 문서는 유니코드 기능 전반을 한눈에 훑는 개요이면서, 루틴·메서드 문서에는 들어가기 어려운 유니코드 기능들을 설명하는 자리예요.
Rakudo가 쓰는 VM(MoarVM) 내부의 문자열 표현에 대한 개요는 문자열이 실제로 어떻게 다뤄지는지 궁금할 때 참고할 만해요.
보안상 브라우저(Firefox 등)는 OS에 해당 글리프 폰트가 설치돼 있어도 신뢰하지 않는 폰트의 유니코드 글리프 표시를 막을 수 있어요. Firefox라면
privacy.fingerprintingProtection설정을False로 바꾸면 해결돼요.
파일 핸들과 입출력 (Filehandles and I/O)
정규화 (Normalization)
Raku는 기본적으로 모든 입력과 출력에 정규화(normalization)를 적용해요. 단, 파일 이름은 예외인데, 파일 이름은 UTF8-C8로 읽고 씁니다. 글로벌하게 보이는 문자 형태인 그래핌(grapheme)은 정규화된 형태로 다뤄져요.
예를 들어 그래핌 á는 하나의 코드포인트로도, 두 개의 코드포인트로도 표현될 수 있어요.
á (U+E1 "LATIN SMALL LETTER A WITH ACUTE")
a + ́ (U+61 "LATIN SMALL LETTER A" + U+301 "COMBINING ACUTE ACCENT")
Raku는 이 두 입력을 모두 하나의 코드포인트로 바꿔요. 정규화 형식 C(NFC)에 명시된 방식이죠. 대부분의 경우 이는 유용한데, 서로 동등한 두 입력이 똑같이 취급된다는 뜻이에요. 유니코드에는 표준 동등성(canonical equivalence)이라는 개념이 있고, 이를 통해 문자열의 표준 형태를 알 수 있어서 문자열을 비교하거나 조작할 때 그 성질이 유실될 걱정 없이 다룰 수 있어요. 기본적으로 Raku로 처리하거나 출력하는 모든 텍스트는 수정·연결 시에도 이 "표준" 형태를 유지해요(피하는 방법은 아래에서 다뤄요). NFC와 표준 동등성에 대한 자세한 설명은 유니코드 재단의 정규화와 표준 동등성 문서를 참고하세요.
파일 이름은 기본적으로 정규화하지 않아요. 파일 이름은 디스크에 기록된 그대로의 바이트로 접근해야 하기 때문이에요.
정규화를 피하고 싶다면 UTF8-C8라는 특별한 인코딩을 쓰면 돼요. 어떤 파일 핸들에 이 인코딩을 쓰면 디스크의 바이트를 정규화 없이 그대로 읽을 수 있어요. UTF-8 핸들로 출력하면 이상해 보일 수 있는데, 출력 인코딩이 UTF8-C8인 핸들로 출력하면 바이트 단위로 그대로, 원래 모습대로 렌더링돼요. MoarVM의 UTF8-C8에 대한 더 기술적인 내용은 아래에 이어져요.
UTF8-C8
UTF-8 Clean-8은 기본적으로 UTF-8처럼 동작하는 인코더/디코더예요. 다만 유효한 UTF-8로 디코딩되지 않거나 정규화 때문에 왕복(round-trip)이 깨지는 바이트 시퀀스를 만나면, NFG 신세틱을 이용해 원래 바이트를 추적해요. 이 말은 UTF-8 Clean-8으로 다시 인코딩하면 원래 바이트를 그대로 복원할 수 있다는 뜻이에요. 신세틱은 네 개의 코드포인트로 구성돼요.
- 코드포인트 0x10FFFD (비공개 영역 코드포인트)
- 코드포인트 'x'
- 디코딩 불가능한 바이트의 상위 4비트를 16진수 문자로 (0..9A..F)
- 해당 바이트의 하위 4비트를 16진수 문자로 (0..9A..F)
일반 UTF-8 인코딩에서는 표현 불가능한 문자가 대략 ?xFF처럼 나와요.
UTF-8 Clean-8은 MoarVM이 환경, 명령행 인자, 파일시스템 조회에서 문자열을 받을 때 쓰여요. 예를 들어 버퍼를 디코딩할 때죠:
say Buf.new(ord('A'), 0xFE, ord('Z')).decode('utf8-c8');
# OUTPUT: «AxFEZ»
UTF8-C8이 쓰는 두 개의 시작 코드포인트가 'FE' 바로 앞에 나타나는 걸 볼 수 있어요. 이런 인코딩으로 인코딩을 모르는 파일도 읽을 수 있어요:
my $test-file = "/tmp/test";
given open($test-file, :w, :bin) {
.write: Buf.new(ord('A'), 0xFA, ord('B'), 0xFB, 0xFC, ord('C'), 0xFD);
.close;
}
say slurp($test-file, enc => 'utf8-c8');
# OUTPUT: «(65 250 66 251 252 67 253)»
이 인코딩으로 읽고 다시 UTF8-C8로 인코딩하면 원래 바이트가 그대로 돌아와요. 기본 UTF-8 인코딩으로는 그럴 수 없죠.
이 인코딩은 아직 Rakudo의 JVM 구현에서는 지원되지 않아요.
유니코드 코드포인트와 코드포인트 시퀀스로 문자 정의하기
코드포인트를 숫자로 문자열에 넣기
유니코드 코드포인트를 숫자(10진수·16진수·8진수)로 넣을 수 있어요. 예를 들어 "latin capital letter ae with macron"이라는 문자의 10진 코드포인트는 482, 16진 코드포인트는 0x1E2, 8진 코드포인트는 0o742예요:
say "\c[482]"; # OUTPUT: «Ǣ» (기억팁: 'c - character name or decimal')
say "\x1E2"; # OUTPUT: «Ǣ» (기억팁: 'x - hexadecimal')
say "\o742"; # OUTPUT: «Ǣ» (기억팁: 'o - octal')
16진수·8진수 뒤에 오는 문자는 해당 숫자 형식에 유효하지 않을 때만 분리돼요. 예를 들면:
say "\x1E2"; # OUTPUT: «Ǣ»
say "\x1E2P"; # OUTPUT: «ǢP» ('P'는 16진 숫자가 아님, 결과: 2글자)
say "\x1E2 "; # OUTPUT: «Ǣ » (' '(공백)은 16진 숫자가 아님, 결과: 2글자)
say "\x1E20"; # OUTPUT: «Ḡ» ('0'은 16진 숫자, 결과: 1글자)
8진수 버전도 끝나는 문자가 (0..7) 집합에 없으면 같은 방식으로 동작해요.
코드포인트를 이름으로 문자열에 넣기
유니코드 코드포인트를 이름으로 접근할 수도 있어요. Raku는 모든 유니코드 이름을 지원해요.
say "\c[PENGUIN]"; # OUTPUT: «🐧»
say "\c[BELL]"; # OUTPUT: «🔔» (U+1F514 BELL)
모든 유니코드 코드포인트 이름과 이름 시퀀스는 이제 대소문자를 가리지 않아요([Rakudo 2017.02부터]):
say "\c[latin capital letter ae with macron]"; # OUTPUT: «Ǣ»
say "\c[latin capital letter E]"; # OUTPUT: «E» (U+0045)
(다만 문자의 uniname을 조회하면 그 이름은 대문자로 보여요.)
하나의 \c[] 안에 쉼표로 구분된 목록으로 여러 문자를 지정할 수 있고, 숫자 방식과 이름 방식도 섞을 수 있어요:
say "\c[482,PENGUIN]"; # OUTPUT: «Ǣ🐧»
문자열 안에서 \c[]를 쓰는 것 외에, uniparse로 문자를 보여줄 수도 있어요:
say "DIGIT ONE".uniparse; # OUTPUT: «1»
say uniparse("DIGIT ONE"); # OUTPUT: «1»
반대 방향(단일·복수 코드포인트로 이름 얻기)은 uniname, uninames 루틴을 참고하세요.
코드포인트 시퀀스
코드포인트 시퀀스는 하나의 문자열 안에서 다른 코드포인트 항목 뒤에 이어서, 또는 평범한 문자·단어·공백과 섞어서 넣을 수 있어요. 이런 입력 방식은 유니코드 입력이 가능한 키보드가 없는 사용자에게 특히 유용해요. 문자열에는 유니코드 삽입 방법 중 아무거나(전부) 쓸 수 있어요. 예를 들어 프랑스어의 악센트를 만들려면:
say "Le Caf\xe9 Marly"; # OUTPUT: «Le Café Marly»
마지막으로, 여러 방법을 한 문자열에 모두 쓰는 좀 더 완전한 예시예요:
say "\c[PENGUIN] went looking for \x1F41F but found only \o[375274,40,373046]."
# Output: «🐧 went looking for 🐟 but found only 🪼 😦.»
이름 별칭 (Name aliases)
이름 별칭은 주로 공식 이름이 없는 코드포인트, 약어, (유니코드 이름은 절대 바뀌지 않아서) 교정을 위해 쓰여요. 전체 목록은 여기를 참고하세요.
공식 이름이 없는 제어 코드:
say "\c[ALERT]"; # 표시 안 됨 (U+0007 제어 코드, \a로도 접근 가능)
say "\c[LINE FEED]"; # 표시 안 됨 (U+000A, "\n"과 같음)
교정:
# 올바른 이름을 입력으로:
say "\c[LATIN CAPITAL LETTER GHA]"; # OUTPUT: «Ƣ»
# 원래의(잘못된) 이름이 출력으로:
say "Ƣ".uniname; # OUTPUT: «LATIN CAPITAL LETTER OI»
# 이것은 Name Alias에서 교정된 철자 오류이에요:
# 올바른 이름을 입력으로:
say "\c[PRESENTATION FORM FOR VERTICAL RIGHT WHITE LENTICULAR BRACKET]".uniname;
# 원래의(잘못된) 이름이 출력으로:
# OUTPUT: «PRESENTATION FORM FOR VERTICAL RIGHT WHITE LENTICULAR BRAKCET»
약어:
say "\c[ZWJ]".uniname; # OUTPUT: «ZERO WIDTH JOINER»
say "\c[NBSP]".uniname; # OUTPUT: «NO-BREAK SPACE»
say "\c[NNBSP]".uniname; # OUTPUT: «NARROW NO-BREAK SPACE»
이름 붙은 시퀀스 (Named sequences)
이름 붙은 시퀀스(Named Sequences)도 사용할 수 있어요. 이것은 단일 코드포인트가 아니라 코드포인트들의 시퀀스예요([Rakudo 2017.02부터]).
say "\c[LATIN CAPITAL LETTER E WITH VERTICAL LINE BELOW AND ACUTE]"; # OUTPUT: «É̩»
say "\c[LATIN CAPITAL LETTER E WITH VERTICAL LINE BELOW AND ACUTE]".ords; # OUTPUT: «(201 809)»
이모지 시퀀스
Raku는 이모지 시퀀스를 지원해요. 전체 목록은 Emoji ZWJ Sequences와 Emoji Sequences를 참고하세요. 이름에 쉼표가 있는 것은 쉼표를 제거해야 하는데, Raku가 같은 \c 안의 서로 다른 코드포인트/시퀀스를 구분하기 위해 쉼표를 쓰기 때문이에요.
say "\c[woman gesturing OK]"; # OUTPUT: «🙆♀️»
say "\c[family: man woman girl boy]"; # OUTPUT: «👨👩👧👦»
혼동 가능성 (Confusability)
유니코드에는 문자가 워낙 많아서 서로 혼동하기 쉬운(confusable) 문자도 있어요. 문제를 피하는 일반적인 요령은 unicode.org의 Confusability 문서를 참고하세요.