Why string.length Lies: Grapheme Clusters and Emoji Length

The Lie Your Runtime Tells You

'👨‍👩‍👧‍👦'.length  // 11

That is not 11 characters. It is one family emoji. The .length property in JavaScript counts UTF-16UTF-16
문자당 2 또는 4바이트를 사용하는 가변 너비 유니코드 인코딩으로, JavaScript와 Java에서 사용됩니다.
code units, not visible characters. For most ASCII text this distinction is invisible, but emoji expose the gap brutally.

Understanding why this happens — and how to fix it — requires knowing what a grapheme cluster is.

Three Levels of "Character"

Unicode defines multiple levels of text unit, each useful for different purposes:

Level Unit JavaScript Python
Byte Raw storage unit — len(s.encode('utf-8UTF-8
문자당 1~4바이트를 사용하는 가변 너비 유니코드 인코딩으로, 웹의 98% 이상에서 사용됩니다.
'))
Code unit Encoding unit s.length —
Code point Unicode scalar value [...s].length len(s)
Grapheme cluster User-perceived character Intl.Segmenter grapheme library

A grapheme cluster is what a human thinks of as "one character" — one visible glyph on screen. For emoji, this is always at least one code point, but often several combined.

What Makes an Emoji "Long"?

Basic Emoji: 1 Code Point, 2 Code Units

😀 = U+1F600
UTF-16: 0xD83D 0xDE00 (surrogate pair)
.length in JS: 2
Python len(): 1

Emoji + Variation Selector: 2 Code Points

❤️ = U+2764 + U+FE0F
.length in JS: 2
Python len(): 2
Grapheme clusters: 1

Emoji + Skin Tone: 2 Code Points

👍🏽 = U+1F44D + U+1F3FD
.length in JS: 4 (two surrogate pairs)
Python len(): 2
Grapheme clusters: 1

ZWJ영폭 결합자 (ZWJ)
여러 이모지를 하나의 합성 이모지로 결합하는 데 사용되는 보이지 않는 유니코드 문자(U+200D).
Sequence: 3+ Code Points

👩‍💻 = U+1F469 + U+200D + U+1F4BB
.length in JS: 5
Python len(): 3
Grapheme clusters: 1

Family Emoji: 7 Code Points

👨‍👩‍👧‍👦 = 👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦
.length in JS: 11
Python len(): 7
Grapheme clusters: 1

Correct Counting in JavaScript

Using Intl.Segmenter (Modern, Built-In)

Intl.Segmenter is available in all modern browsers and Node.js 16+. It segments text by grapheme clusters:

function countGraphemes(str) {
  const segmenter = new Intl.Segmenter();
  return [...segmenter.segment(str)].length;
}

countGraphemes('Hello')     // 5 — correct
countGraphemes('Hello 😀') // 7 — correct
countGraphemes('👨‍👩‍👧‍👦')   // 1 — correct!
countGraphemes('👩🏽‍💻')    // 1 — correct!

Slicing Text Correctly

Slicing with .slice() on emoji strings corrupts surrogate pairs:

const s = 'Hello 👋';

// WRONG: may cut through a surrogate pair
s.slice(0, 7)  // 'Hello \uD83D' — broken!

// CORRECT: spread to code points first
[...s].slice(0, 7).join('')  // 'Hello 👋' — correct

// EVEN BETTER: slice by grapheme clusters
function sliceByGrapheme(str, start, end) {
  const segmenter = new Intl.Segmenter();
  const segments = [...segmenter.segment(str)];
  return segments.slice(start, end).map(s => s.segment).join('');
}

sliceByGrapheme('Hello 👋 World', 0, 7)  // 'Hello 👋'

String Reversal

Reversing an emoji string with .split('').reverse().join('') produces garbage:

// WRONG: reverses code units, breaks surrogates
'Hello 😀'.split('').reverse().join('')  // '😀 olleH' (actually garbled)

// CORRECT: reverse by code points
[...'Hello 😀'].reverse().join('')  // '😀 olleH'

// BEST: reverse by grapheme clusters
function reverseGraphemes(str) {
  const segmenter = new Intl.Segmenter();
  return [...segmenter.segment(str)]
    .map(s => s.segment)
    .reverse()
    .join('');
}

reverseGraphemes('Hello 👨‍👩‍👧‍👦')  // '👨‍👩‍👧‍👦 olleH'

Correct Counting in Python

Python 3's len() counts code points, not grapheme clusters. For most ASCII text this is identical to grapheme count, but for emoji it is not.

text = '👨‍👩‍👧‍👦'
len(text)  # 7 — code points (wrong for "visible characters")

Using the grapheme Library

import grapheme

grapheme.length('👨‍👩‍👧‍👦')   # 1 — correct
grapheme.length('Hello 😀')  # 7 — correct

# Slice by grapheme clusters
grapheme.slice('Hello 👨‍👩‍👧‍👦', 0, 7)  # 'Hello 👨‍👩‍👧‍👦'

# Iterate graphemes
list(grapheme.graphemes('Hi 👋'))
# ['H', 'i', ' ', '👋']

Using the regex Module with \X

The \X pattern in the regex module matches grapheme clusters:

import regex

def count_graphemes(text):
    return len(regex.findall(r'\X', text))

count_graphemes('👨‍👩‍👧‍👦')   # 1
count_graphemes('Hello 👨‍👩‍👧‍👦')  # 7

Other Languages

Java

// Java uses UTF-16 internally like JavaScript
"👨‍👩‍👧‍👦".length()  // 11 (code units)

// Count code points
"👨‍👩‍👧‍👦".codePointCount(0, "👨‍👩‍👧‍👦".length())  // 7

// Count grapheme clusters (requires ICUICU (ICU)
이모지 처리를 포함한 유니코드 및 국제화 지원을 제공하는 널리 사용되는 오픈소스 라이브러리.
) BreakIterator bi = BreakIterator.getCharacterInstance(); bi.setText("👨‍👩‍👧‍👦"); int count = 0; while (bi.next() != BreakIterator.DONE) count++; // count = 1

Swift

Swift's String is designed around grapheme clusters — count returns grapheme count by default:

"👨‍👩‍👧‍👦".count  // 1 — correct out of the box!

This is one area where Swift's string model is superior to most other languages.

Rust

// Rust len() returns bytes
"👨‍👩‍👧‍👦".len()  // 25 (bytes)

// chars() iterates code points
"👨‍👩‍👧‍👦".chars().count()  // 7

// For grapheme clusters, use the unicode-segmentation crate
use unicode_segmentation::UnicodeSegmentation;
"👨‍👩‍👧‍👦".graphemes(true).count()  // 1

Practical Implications

Input Validation / Character Limits

If you show users "max 280 characters" and count by .length in JavaScript, users can enter far fewer emoji than expected (or you silently truncate their input). Count by grapheme clusters for user-facing limits.

Database Column Sizing

Ensure column size limits account for multi-code-point emoji. A VARCHAR(10) that accepts emoji-heavy input needs to be measured in code points (PostgreSQL) or bytes (MySQL with utf8mb4), not graphemes.

Text Rendering

Layout engines that use pixel widths handle emoji correctly by nature — they measure bounding boxes, not code units. But manual text layout (canvas, PDF generation, terminal output) needs grapheme-aware iteration.

Use our Sequence Analyzer to see exactly how many code points, code units, and grapheme clusters any emoji string contains — making these abstract concepts concrete.

관련 도구

🔍 시퀀스 분석기 시퀀스 분석기
ZWJ 시퀀스, 피부톤 수정자, 키캡 시퀀스, 국기 쌍을 개별 구성 요소로 분해합니다.

용어 사전 항목

이모지 이모지
일본어 '그림 문자(絵文字)'를 뜻하며, 디지털 커뮤니케이션에서 감정과 개념을 표현하는 작은 그래픽 기호.
유니코드 유니코드
모든 문자 체계와 이모지를 포함하여 각 문자에 고유한 번호를 부여하는 범용 문자 인코딩 표준.
서로게이트 쌍 서로게이트 쌍
U+FFFF 이상의 문자를 나타내기 위해 사용되는 두 개의 UTF-16 코드 유닛.
코드 유닛 코드 유닛
문자 인코딩의 최소 비트 조합: UTF-8은 8비트, UTF-16은 16비트, UTF-32는 32비트.
변형 선택자 (VS) 변형 선택자 (VS)
문자를 텍스트(단색) 또는 이모지(컬러) 표현으로 렌더링할지 결정하는 유니코드 문자.
영폭 결합자 (ZWJ) 영폭 결합자 (ZWJ)
여러 이모지를 하나의 합성 이모지로 결합하는 데 사용되는 보이지 않는 유니코드 문자(U+200D).
코드 포인트 코드 포인트
유니코드 표준에서 각 문자에 부여되는 고유한 숫자 값으로, U+XXXX 형식으로 표기됩니다.
자소 클러스터 자소 클러스터
여러 유니코드 코드 포인트로 구성될 수 있지만 하나의 시각적 단위로 표시되는 사용자가 인식하는 문자.
ICU (ICU) ICU (ICU)
이모지 처리를 포함한 유니코드 및 국제화 지원을 제공하는 널리 사용되는 오픈소스 라이브러리.
UTF-16 UTF-16
문자당 2 또는 4바이트를 사용하는 가변 너비 유니코드 인코딩으로, JavaScript와 Java에서 사용됩니다.
UTF-8 UTF-8
문자당 1~4바이트를 사용하는 가변 너비 유니코드 인코딩으로, 웹의 98% 이상에서 사용됩니다.

관련 스토리