글자 수 제한을 맞췄는데 이모지가 깨지거나 서버에서 길이 초과가 난다면, 먼저 제한의 단위가 UTF-8 바이트인지, UTF-16 코드 단위인지, 코드 포인트인지, 그래핌인지 확인해야 한다. 화면에서 한 글자처럼 보이는 요소와 JavaScript의 length는 같은 수를 세지 않을 수 있다.
아래는 문서 AI가 내보낸 문자열을 입력 칸이나 API에 전달하기 전 점검하는 방법이다. 모든 입력은 합성 문자열이다. 로컬 Node.js v24.15.0, Unicode 17.0, ICU 78.2에서 확인했으며 외부 API·폰트·모든 기기의 표시를 시험하지 않았다. 공식 UAX #29는 2026년 10월 30일 읽은 Unicode 18.0.0 revision 49다. 로컬 예제를 그 판본 전체의 적합성 결과로 보지는 않는다.
같은 네 요소를 세면 21·10·7·4가 나온다
ECMAScript의 String 정의에서 문자열의 원소와 위치는 UTF-16 코드 단위다. 문자열 이터레이터로 세는 코드 포인트와도 구별해야 한다. Unicode의 그래핌 경계는 여러 코드 포인트로 이뤄진 요소를 다루는 기준이지만 사용자가 느끼는 글자에 대한 근사이며 화면 폭 측정은 아니다.
복사한 코드에서도 이모지 문자열이 유지되도록 Unicode escape 표기를 사용했으며, 실행하면 같은 이모지 문자열이 된다.
const segmenter = new Intl.Segmenter("ko", { granularity: "grapheme" });
const samples = ["가", "e\u0301", "\u{1F600}", "\u{1F469}\u200D\u{1F4BB}"];
const count = text => ({
text,
utf8Bytes: Buffer.byteLength(text, "utf8"),
utf16Units: text.length,
codePoints: [...text].length,
graphemes: [...segmenter.segment(text)].length
});
console.log(JSON.stringify(samples.map(count)));
console.log(JSON.stringify(count(samples.join(""))));
| 입력 | UTF-8 바이트 | UTF-16 코드 단위 | 코드 포인트 | 그래핌 |
|---|---|---|---|---|
| 가 | 3 | 1 | 1 | 1 |
| e + U+0301 결합 악센트 | 3 | 2 | 2 | 1 |
| 😀 | 4 | 2 | 1 | 1 |
| 👩💻 | 11 | 5 | 3 | 1 |
| 네 입력을 순서대로 붙인 문자열 | 21 | 10 | 7 | 4 |
표는 위 코드의 실제 로컬 결과다. e\u0301는 e와 결합 악센트 두 코드 포인트이며, 👩💻는 사람·ZWJ·노트북의 세 코드 포인트다. 그래핌이 한 개라는 결과를 UTF-8 한 바이트나 한 코드 포인트라는 뜻으로 바꾸면 안 된다. Buffer.byteLength는 지정 인코딩의 바이트 길이를 계산한다.
slice와 코드 포인트 자르기는 서로 다른 것을 가른다
String.prototype.slice의 위치는 JavaScript 문자열의 코드 단위 기준이다. 다음 결과를 정상 이모지처럼 화면에 출력하지 않고 JSON 이스케이프와 well-formed 여부로 확인했다.
const broken = "\u{1F600}".slice(0, 1);
console.log(JSON.stringify(broken), broken.isWellFormed());
console.log(JSON.stringify("e\u0301".slice(0, 1)));
console.log(JSON.stringify([..."\u{1F469}\u200D\u{1F4BB}"].slice(0, 1).join("")));
const segmenter = new Intl.Segmenter("ko", { granularity: "grapheme" });
const text = "가e\u0301\u{1F600}\u{1F469}\u200D\u{1F4BB}";
const prefix = [...segmenter.segment(text)]
.slice(0, 3).map(part => part.segment).join("");
console.log(JSON.stringify(prefix), Buffer.byteLength(prefix, "utf8"));
첫 출력은 "\ud83d" false였다. 서로게이트 쌍의 앞 코드 단위만 남겼기 때문이다. 다음 두 출력은 "e"와 "👩"였다. 코드 포인트 단위라 해도 이모지 결합열의 의미 있는 한 요소를 보존한다는 보장은 없다. 그래핌 기준 앞 세 개는 "가é😀", UTF-8 10바이트였다.

바이트 제한에는 바이트 예산을 함께 검사한다
입력 칸은 그래핌 수, 수신 API는 UTF-8 바이트를 제한할 수 있다. 이 경우 그래핌 경계를 유지하면서 바이트 예산 안에 드는 접두 문자열을 고르는 것이 한 후보가 된다. 아래 함수는 작성자의 작은 처리 예다. 유효한 문자열과 0 이상의 안전한 정수 예산만 받으며, 큰 요소를 건너뛰어 뒤 글자를 끼워 넣지 않는다.
function prefixWithinUtf8(text, budget) {
if (typeof text !== "string" || !text.isWellFormed()) {
throw new TypeError("well-formed string required");
}
if (!Number.isSafeInteger(budget) || budget < 0) {
throw new RangeError("nonnegative byte budget required");
}
const segmenter = new Intl.Segmenter("ko", { granularity: "grapheme" });
let result = "", used = 0;
for (const { segment } of segmenter.segment(text)) {
const cost = Buffer.byteLength(segment, "utf8");
if (used + cost > budget) break;
result += segment;
used += cost;
}
return result;
}
const text = "가e\u0301\u{1F600}\u{1F469}\u200D\u{1F4BB}";
console.log(JSON.stringify(prefixWithinUtf8(text, 4)));
console.log(JSON.stringify(prefixWithinUtf8(text, 10)));
console.log(JSON.stringify(prefixWithinUtf8("\u{1F469}\u200D\u{1F4BB}끝", 4)));
실제 출력은 순서대로 "가", "가é😀", ""였다. 첫 이모지 요소가 11바이트라서 마지막 예의 4바이트 예산에는 아무 접두 요소도 들어가지 않았다. 강제로 쪼개지 않은 결과이지 오류가 없어진 모든 경우의 증명은 아니다.
이 함수가 세는 것은 전달할 텍스트 자체의 UTF-8 바이트다. 말줄임표를 뒤에 붙이면 그 바이트도 예산에 넣어야 한다. 제한이 JSON 요청 전체나 별도 인코딩 결과에 걸리면 직렬화·인코딩한 최종 데이터도 해당 계약대로 측정해야 한다. 서버가 문자를 세는 방식을 모르면 이 함수가 그 서버의 규칙과 같다고 추정하지 않는다.
길이 계약을 정할 때 남길 판단
| 목적 | 확인할 단위 | 따로 남는 문제 |
|---|---|---|
| 사람이 편집하는 짧은 입력 | 그래핌 수 등 화면의 명시된 규칙 | 표시 폭·줄바꿈·실제 폰트/기기 |
| 전송·저장 바이트 제한 | 인코딩과 바이트 수 | 요청 봉투·이스케이프·잘린 경계 |
| 외부 API의 문자 제한 | 그 API가 정한 단위와 예외 | 클라이언트와 서버의 규칙/판본 일치 |
| LLM 입력 한도 | 해당 모델/토크나이저의 토큰 계약 | 메시지 구조 등 별도 입력 비용 |
Intl.Segmenter 명세를 확인하고, 운영 환경의 지원 여부와 Unicode/ICU 판본을 함께 기록하는 편이 좋다. 그래핌 분할을 코드 포인트 수나 토큰 수의 대용으로 쓰지 않는다. 또 하나의 그래핌에도 여러 결합 문자가 들어갈 수 있으므로 그래핌 개수만 제한하면 데이터 크기가 제한된다고 보장할 수 없다. 실제 입력 크기와 업무 의미의 손실은 별도로 점검한다.
문자를 읽는 단계부터 어긋났다면 인코딩과 BOM 확인이 먼저다. 잘린 텍스트를 AI 답변의 근거로 쓸 때는 출처가 주장을 실제로 뒷받침하는지도 확인한다. 글자 경계를 유지했다고 중요한 조건이나 문장 의미까지 보존되는 것은 아니다.
자주 묻는 질문
문자열을 펼쳐서 세면 모든 이모지가 한 개가 되나요?
아니다. 문자열 이터레이터가 묶어 주는 서로게이트 쌍과 여러 코드 포인트의 결합열은 다르다. 이 글의 사람·ZWJ·노트북은 세 코드 포인트지만 시험 환경에서는 한 그래핌이었다.
그래핌 기준으로 자르면 화면 폭에도 맞나요?
경계를 결정하는 일과 픽셀 폭을 측정하는 일은 다르다. 폰트·화면·표시 환경에서 실제 결과를 확인해야 한다. 모든 언어의 단어 수나 LLM 토큰 수도 그래핌 수와 같지 않다.
서버가 이미 길이 초과를 반환했다면 같은 텍스트를 조금씩 줄이면 되나요?
먼저 제한 단위와 최종 전송 데이터를 확인한다. 임의로 줄이면 결합열·중요한 조건이 사라질 수 있다. 알려진 규칙으로 예산과 경계를 맞추고, 줄인 결과가 업무 목적에 충분한지도 따로 판단한다.