Kenapa emoji bisa bikin database error — dan kenapa "panjang string" itu pertanyaan yang salah
Coba tebak: berapa "panjang" string "😀"? Jawabannya tergantung siapa yang ditanya — dan di situlah bug-nya bersembunyi.
Satu karakter, empat jawaban berbeda
"😀" itu 1 karakter kalau kamu tanya ke manusia, tapi:
| Yang dihitung | Hasil |
|---|---|
| Codepoint (Unicode) | 1 |
| Byte UTF-8 | 4 |
| Byte UTF-16 | 4 (2 unit × 2 byte) |
Kolom VARCHAR(1) di MySQL utf8 lama |
error / terpotong |
Saya cek langsung barusan:
codepoints 1
utf8 bytes 4 f0 9f 98 80
utf16 units 2 3d d8 00 de
Empat byte. Sementara huruf A cuma 1 byte di UTF-8.
Akar masalahnya: UTF-8 itu panjang variabel
UTF-8 pakai 1–4 byte per codepoint:
A,z,0→ 1 byte (ASCII legacy, makanya UTF-8 backward-compatible)é,ñ,ü→ 2 byte中,€,→→ 3 byte😀,🔥,𝄞→ 4 byte
UTF-16 pakai 2 atau 4 byte. Karakter di atas U+FFFF (kebanyakan emoji) butuh surrogate pair — dua unit 16-bit. Makanya "😀" jadi 2 unit UTF-16. Di JavaScript dan Java, "😀".length mengembalikan 2, bukan 1. Itu bukan bug JS — itu konsekuensi historis UTF-16.
Di Python 3 len("😀") = 1 karena string dihitung per codepoint. Jadi kode yang sama, bahasa berbeda, angka berbeda. Kalau kamu port logika validasi panjang dari JS ke Go tanpa mikir, kamu akan dapat off-by-one yang cuma muncul kalau user ketik emoji. Bug yang lolos QA 11 bulan.
Yang lebih parah: emoji keluarga
family codepoints 7 utf8 bytes 25 utf16 units 11
👨👩👧👦 — yang terlihat seperti satu emoji — sebenarnya 7 codepoint: 4 emoji orang + 3 zero-width joiner (U+200D). Di UTF-8 jadi 25 byte.
Artinya:
- Kolom
VARCHAR(20)di MySQL utf8mb4 → gagal insert. User pilih emoji keluarga, form crash. Tidak reproducible di laptop developer yang ngetes pakai"test". text[:20]di Python memotong di tengah urutan ZWJ → hasilnya emoji orang berdiri sendiri, bukan keluarga. Kadang malah muncul karakter rusak�.substringdi Java bisa memotong surrogate pair di tengah →StringIndexOutOfBoundsExceptionatau karakter sampah.
Tiga jebakan operasional yang paling sering kena
1. MySQL utf8 bukan UTF-8.
utf8 di MySQL = UTF-8 3-byte maksimum. Emoji 4-byte ditolak atau jadi ???. Yang benar: utf8mb4. Ini jebakan paling klasik dan masih hidup di 2026 karena nama kolomnya menipu. Cek:
SHOW VARIABLES LIKE 'character_set_%';
Kalau ada utf8 (bukan utf8mb4), itu bom waktu.
2. Potong string berdasarkan byte, bukan codepoint.
Preview 100 karakter dengan substr($text, 0, 100) di PHP (byte-based) akan memotong di tengah karakter multibyte → � di akhir preview. Solusi: pakai fungsi yang sadar encoding (mb_substr), atau lebih baik potong per grapheme cluster.
3. Normalisasi Unicode.
"é" bisa ditulis dua cara: satu codepoint (U+00E9) atau e + combining accent (U+0065 U+0301). Keduanya terlihat identik, tapi "é" != "e\u0301" di perbandingan string. Ini bikin:
- User tidak bisa login karena password di-hash dari bentuk berbeda
- Duplikat akun dengan nama yang "sama persis"
- Pencarian gagal
Solusi: unicodedata.normalize('NFC', s) sebelum simpan/bandingkan. Saya verifikasi: setelah NFC, panjangnya jadi 1.
Urutan konsep yang bener (dari kecil ke besar)
byte < codepoint < grapheme cluster
- Byte: unit penyimpanan. 4 untuk 😀.
- Codepoint: unit Unicode. 1 untuk 😀, 7 untuk 👨👩👧👦.
- Grapheme: apa yang mata manusia lihat sebagai "satu karakter". 1 untuk 😀, 1 untuk 👨👩👧👦, 1 untuk
é(kombinasi).
Aturan praktis: validasi dan potong pakai grapheme, simpan pakai byte, hitung "jumlah karakter" untuk user pakai grapheme. Kalau tidak mau ribet, minimal pakai codepoint — jangan pernah byte.
Satu hal yang bisa langsung dipakai
Sebelum bikin kolom teks baru, jalankan ini sekali:
ALTER TABLE your_table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Dan di aplikasi, jangan pernah pakai strlen-setara untuk validasi panjang teks user. Ganti dengan hitungan grapheme:
- JS:
[...str].length(codepoint) atauIntl.Segmenter(grapheme, lebih akurat) - Python:
len(s)aman untuk codepoint;graphemelib untuk cluster - Go:
utf8.RuneCountInString(s)— janganlen(s)yang byte-based
Kalau hari ini kamu cuma sempat satu: cari kata utf8 (tanpa mb4) di config database-mu. Itu 5 menit, dan menyelamatkan kamu dari bug yang bakal muncul 6 bulan lagi saat user pertama kali pakai emoji.
Takeaway
"Panjang string" bukan satu angka — dia byte, codepoint, dan grapheme sekaligus. Bug emoji bukan soal emoji; itu soal kode yang mengasumsikan satu karakter = satu byte. ASCII membuat kita malas selama 30 tahun, dan Unicode menagihnya sekarang.