'๐'.length === 2? ์๋ฐ์คํฌ๋ฆฝํธ์์ ์ด๋ชจ์ง ๋ฌธ์์ด ๊ธธ์ด์ ํจ์ ๊ณผ ํด๊ฒฐ๋ฒ
ํ๋ก ํธ์๋์์ ๋ฌธ์์ด ๊ธธ์ด๋ฅผ ๊ฒ์ฌํ๋ค ๋ณด๋ฉด ์ด๋ฐ ๊ฒฝํ์ด ์์ ๊ฑฐ์์.
"์ด๋ชจ์ง ํ๋๋ฅผ ๋ฃ์๋๋ฐ ๊ธธ์ด๊ฐ 2๋ก ๊ณ์ฐ๋ผ?"
'๐'.length // 2
์ด๊ฑด ์๋ฐ์คํฌ๋ฆฝํธ์ ๋ฌธ์์ด ๋ชจ๋ธ์ด ์ ๋์ฝ๋์ ๋ง๋ฌผ๋ฆฌ๋ ์ง์ ์์ ์๊ธฐ๋, ์์ฃผ ํํ์ง๋ง ํท๊ฐ๋ฆฌ๋ ๋ฌธ์ ์์.
์ '๐'.length๋ 2์ผ๊น?
์๋ฐ์คํฌ๋ฆฝํธ ๋ฌธ์์ด์ UTF-16 ์ฝ๋ ์ ๋ ๋จ์๋ก ์ ์ฅ๋ผ์. ์ฆ, String.prototype.length๋ ์ค์ "๋ณด์ด๋ ๊ธ์ ์"๊ฐ ์๋๋ผ "16๋นํธ ์กฐ๊ฐ ๊ฐ์"๋ฅผ ๋ฐํํ๋ ๊ฑฐ์ฃ .
"๐" (U+1F344)์ BMP(๊ธฐ๋ณธ ๋ค๊ตญ์ด ํ๋ฉด) ๋ฐ์ ๋ฌธ์๋ผ 16๋นํธ ๋ ์กฐ๊ฐ(์๋ก๊ฒ์ดํธ ํ์ด)๋ก ํํ๋ผ์. BMP๋ U+0000๋ถํฐ U+FFFF๊น์ง์ ๋ฒ์์ธ๋ฐ, ์ด ์์ ๋๋ ๋ฌธ์๋ ์ฝ๋ ์ ๋ ํ๋๋ก ํํ๋์ง๋ง ๊ทธ ๋ฐ์ ๋ฌธ์๋ ๋ ๊ฐ์ ์ฝ๋ ์ ๋, ์ฆ ์๋ก๊ฒ์ดํธ ํ์ด๋ก ํํํด์ผ ํ๊ฑฐ๋ ์. ์ด๋ชจ์ง ๋๋ถ๋ถ์ด ์ด ๋ฒ์ ๋ฐ์ ์์ด์ .length๊ฐ 2๊ฐ ๋๋ ๊ฑฐ์์.
"๐".length; // 2
"๐".charCodeAt(0).toString(16); // "d83c"
"๐".charCodeAt(1).toString(16); // "df44"
"๐".codePointAt(0).toString(16); // "1f344"
๊ฒฐ๊ตญ '๐'์ ์ฝ๋ ์ ๋ 2๊ฐ๋ก ๊ตฌ์ฑ๋๋๊น .length === 2๊ฐ ๋๋ ๊ฑฐ์์.
"๊ธธ์ด"๋ ๋ฌด์์ ์ธ๋๋์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋ค
์๋ฐ์คํฌ๋ฆฝํธ์์ ๋ฌธ์์ด์ "๊ธธ์ด"๋ ์๊ฐ๋ณด๋ค ์ฌ๋ฌ ์ธต์๋ก ๋๋์ด์. ๊ฐ์ ๐ ๋ฌธ์๋ผ๋ ๋ฌด์์ ๊ธฐ์ค์ผ๋ก ์ธ๋๋์ ๋ฐ๋ผ ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ ธ์.
๋จผ์ ๋ฐ์ดํธ ์ ๊ธฐ์ค์ผ๋ก ๋ณด๋ฉด, ๐์ UTF-8์์ 4๋ฐ์ดํธ๋ฅผ ์ฐจ์งํด์. ์ด๊ฑด ์ ์ฅ ๊ณต๊ฐ์ด๋ ๋คํธ์ํฌ ์ ์ก ์ ์ฉ๋์ ๊ณ์ฐํ ๋์ ๊ด์ ์ด์ฃ .
์ฝ๋ ์ ๋ ์ ๊ธฐ์ค์ผ๋ก ๋ณด๋ฉด 2์์. UTF-16์ 16๋นํธ ๋จ์ ์กฐ๊ฐ์ ์ธ๋ ๊ฒ์ด๊ณ , ์๋ฐ์คํฌ๋ฆฝํธ์ String.prototype.length๊ฐ ๋ฐ๋ก ์ด ๊ฐ์ ๋ฐํํด์.
์ฝ๋ ํฌ์ธํธ ์ ๊ธฐ์ค์์๋ 1์ด ๋ผ์. ์ ๋์ฝ๋์์ ๐์ ํ๋์ ๊ณ ์ ํ ์ฝ๋ ํฌ์ธํธ(U+1F344)๋ก ์๋ณ๋๋๊น์.
๋ง์ง๋ง์ผ๋ก ๊ทธ๋ํ ์, ์ฆ ํ๋ฉด์์ ์ฌ์ฉ์๊ฐ ์ธ์ํ๋ "๋ณด์ด๋ ๊ธ์ ์"๋ก ๋ณด๋ฉด ์ญ์ 1์ด์์. ์ด๊ฒ ์ฐ๋ฆฌ๊ฐ ์ผ๋ฐ์ ์ผ๋ก "๋ฌธ์ ํ๋"๋ผ๊ณ ๋๋ผ๋ UX ๊ธฐ์ค์ ๋จ์์ฃ .
์ ๋ฆฌํ์๋ฉด, ๐ ํ๋๋ ์ ์ฅ ์ 4๋ฐ์ดํธ, JS์ .length๋ก๋ 2, ์ ๋์ฝ๋ ๋ฌธ์ ๊ธฐ์ค์ผ๋ก๋ 1, ๊ทธ๋ฆฌ๊ณ ์๊ฐ์ ์ผ๋ก๋ 1๊ธ์์์. ์ฆ, "๊ธธ์ด"๋ ๊ธฐ์ ์ ๊ด์ ์ ๋ฐ๋ผ ์ ํ ๋ค๋ฅด๊ฒ ์ ์๋ ์ ์์ด์.
์ค๋ฌด์์ "์ง์ง ๊ธ์ ์" ์ธ๋ ๋ฐฉ๋ฒ
์์ฆ ์ด๋ชจ์ง๋ค์ ์คํจํค, ๊ฐ์กฑ, ์ฑ๋ณ ์กฐํฉ ๊ฐ์ ZWJ ์ํ์ค๊ฐ ํํด์. ๋์ ๋ณด์ด๋ ๊ธ์ ํ๋๊ฐ ์ฌ๋ฌ ์ฝ๋ ์ ๋ ๋๋ ์ฌ๋ฌ ์ฝ๋ ํฌ์ธํธ๋ก ์ด๋ฃจ์ด์ง ์ ์๊ฑฐ๋ ์.
๊ทธ๋์ "๋ณด์ด๋ ๊ธ์ ์"๋ฅผ ์ธ๋ ค๋ฉด ์๋ ๋ ๊ฐ์ง ๋ฐฉ์ ์ค ํ๋๋ฅผ ์จ์ผ ํด์.
์ฝ๋ ํฌ์ธํธ ๊ธฐ์ค
[..."๐๐๐ฝ"].length; // 2
const sliceByCodePoints = (str, n) => Array.from(str).slice(0, n).join("");
ES2015 ์ด์์ด๋ฉด ์ด๋์๋ ์๋ํ๋ค๋ ์ฅ์ ์ด ์์ง๋ง, ZWJ ์กฐํฉ(๐จโ๐ฉโ๐งโ๐ฆ)์ ์ฌ๋ฌ ๊ธ์๋ก ์๋ฆด ์ ์๋ค๋ ํ๊ณ๊ฐ ์์ด์.
๊ทธ๋ํ ๊ธฐ์ค (์ฌ์ฉ์ ๊ด์ ์ ์ง์ง ๊ธ์ ์)
const countGraphemes = (str) => {
const seg = new Intl.Segmenter("ko", { granularity: "grapheme" });
return [...seg.segment(str)].length;
};
const sliceByGraphemes = (str, n) => {
const seg = new Intl.Segmenter("ko", { granularity: "grapheme" });
return [...seg.segment(str)]
.slice(0, n)
.map((s) => s.segment)
.join("");
};
countGraphemes("๐๐ฝ"); // 1
์ต์ ๋ธ๋ผ์ฐ์ ์ Node(18+) ๋๋ถ๋ถ์์ ์ง์ํด์. ๋ฏธ์ง์ ํ๊ฒฝ์์๋ grapheme-splitter ํด๋ฆฌํ์ ์ฐ๋ฉด ๋ผ์.
๊ณตํต ์ ํธ ํ ํ๋ฆฟ (ํ๋ก ํธ/์๋ฒ ๊ณต์ฉ)
// utils/charLength.ts
export type LengthMode = "grapheme" | "codePoint" | "codeUnit";
export function lengthOf(str: string, mode: LengthMode = "grapheme"): number {
switch (mode) {
case "codeUnit":
return str.length;
case "codePoint":
return Array.from(str).length;
case "grapheme": {
const AnyIntl: any = Intl as any;
if (AnyIntl?.Segmenter) {
const seg = new AnyIntl.Segmenter("ko", { granularity: "grapheme" });
return [...seg.segment(str)].length;
}
const { default: GraphemeSplitter } = require("grapheme-splitter");
return new GraphemeSplitter().countGraphemes(str);
}
}
}
ํ๋ก ํธ์ ์๋ฒ๊ฐ ๊ฐ์ ์ ํธ์ ์ฐ๋ฉด ๊ธธ์ด ๊ณ์ฐ ๋ถ์ผ์น ๋ฌธ์ ๋ฅผ ์์ฒ ์ฐจ๋จํ ์ ์์ด์.
MySQL์์ ๋ฌธ์์ด ๊ธธ์ด๋ฅผ ์ ๋์ ํจ์ ๊ณผ ๋์ฒ๋ฒ
์๋ฐ์คํฌ๋ฆฝํธ์ ๋ฌธ์์ด ๊ธธ์ด๋ UTF-16 ์ฝ๋ ์ ๋ ์์ง๋ง, MySQL์ ๋ฌธ์์ด์ ๋ฐ์ดํธ(byte) ๋๋ ๋ฌธ์(character) ๊ธฐ์ค์ผ๋ก ๊ณ์ฐํด์.
์ด ์ฐจ์ด๋ฅผ ํผ๋ํ๋ฉด ํนํ ์ด๋ชจ์งยทํ๊ธยทํน์๋ฌธ์ ์ ๋ ฅ ์ ์ ์ฝ ์กฐ๊ฑด ์ค๋ฅ๋ ๋ฐ์ดํฐ ์๋ฆผ ๊ฐ์ ๋ฌธ์ ๊ฐ ์ฝ๊ฒ ๋ฐ์ํด์.
LENGTH() โ ๋ฐ์ดํธ ์ ๊ธฐ์ค
LENGTH()๋ ๋ฌธ์์ด์ด ์ค์ ๋ก ์ ์ฅ์์์ ์ฐจ์งํ๋ ๋ฐ์ดํธ ํฌ๊ธฐ๋ฅผ ๋ฐํํด์. ์ฆ, ๊ฐ์ ๋ฌธ์์ด์ด๋ผ๋ ์ธ์ฝ๋ฉ์ ๋ฐ๋ผ ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ ธ์.
MySQL์ด utf8mb4๋ฅผ ์ธ ๋์ ์์๋ ์ด๋์.
SELECT LENGTH('๐'); -- 4
SELECT LENGTH('ํ'); -- 3
SELECT LENGTH('A'); -- 1
์ด๋ชจ์ง๋ 4๋ฐ์ดํธ, ํ๊ธ์ 3๋ฐ์ดํธ, ASCII ๋ฌธ์๋ 1๋ฐ์ดํธ๋ฅผ ์ฐจ์งํด์. LENGTH()๋ ์ปฌ๋ผ ํฌ๊ธฐ(VARCHAR(255) ๋ฑ)๋ฅผ ๊ณ์ฐํ๊ฑฐ๋ ์ ์ฅ ์ฉ๋์ ํ์ธํ ๋๋ ์ ์ฉํ๋ฐ, ์ฌ์ฉ์ ์
๋ ฅ ๊ธธ์ด ์ ํ์ฒ๋ผ "๊ธ์ ์"๊ฐ ์ค์ํ ๋ก์ง์๋ ์ ํฉํ์ง ์์์.
CHAR_LENGTH() โ ๋ฌธ์ ์ ๊ธฐ์ค
CHAR_LENGTH() (๋๋ CHARACTER_LENGTH())๋ ๋ฌธ์ ๊ฐ์(์ฝ๋ ํฌ์ธํธ ์) ๊ธฐ์ค์ผ๋ก ๊ธธ์ด๋ฅผ ๋ฐํํด์.
SELECT CHAR_LENGTH('๐'); -- 1
SELECT CHAR_LENGTH('ํ'); -- 1
SELECT CHAR_LENGTH('A'); -- 1
์ด ํจ์๋ ์ธ์ฝ๋ฉ ๋ฐฉ์(UTF-8, UTF-16, UTF-32)์ ์๊ด์์ด "์ ๋์ฝ๋ ๋ฌธ์ ํ๋"๋ฅผ 1๋ก ๊ณ์ฐํด์. ๋ฐ๋ผ์ ๋๋ค์, ๋๊ธ, ์ฌ์ฉ์ ์
๋ ฅ ํ๋ ๋ฑ UX ๊ธฐ์ค์ ๊ธ์ ์ ๊ฒ์ฆ์๋ CHAR_LENGTH()๋ฅผ ์ฐ๋ ๊ฒ ์ฌ๋ฐ๋ฅธ ์ ํ์ด์์.
๊ทธ๋ํ ๋จ์๋ ์ฌ์ ํ ๋ณ๊ฐ๋ค
์ฃผ์ํ ์ ์, ๋ณด์ด๋ ๊ธ์(๊ทธ๋ํ) ์ ์ ๋์ฝ๋ ๋ฌธ์(์ฝ๋ ํฌ์ธํธ) ๊ฐ ํญ์ ์ผ์นํ์ง ์๋๋ค๋ ๊ฑฐ์์. ์๋ฅผ ๋ค์ด ๊ฐ์กฑ ์ด๋ชจ์ง ๐จโ๐ฉโ๐งโ๐ฆ ๋ ์ฌ๋ฌ ์ฝ๋ ํฌ์ธํธ๋ฅผ ZWJ(Zero Width Joiner) ๋ก ์ฐ๊ฒฐํ ์กฐํฉ์ด๊ฑฐ๋ ์.
SELECT CHAR_LENGTH('๐จโ๐ฉโ๐งโ๐ฆ'); -- 7
๋์ผ๋ก๋ "ํ ๊ธ์"์ง๋ง, MySQL ์ ์ฅ์์๋ 7๊ฐ์ ๋ฌธ์๋ก ๊ณ์ฐ๋ผ์. MySQL์ ๊ทธ๋ํ ๋จ์(์๊ฐ์ ์ผ๋ก ๋ณด์ด๋ ๊ธ์ ์) ๋ฅผ ์ดํดํ์ง ๋ชปํ๋๊น์.
๊ฒฐ๊ตญ LENGTH()๋ ์ ์ฅ ์ฉ๋(๋ฐ์ดํธ) ๊ธฐ์ค์ด๋ผ ์ธ์ฝ๋ฉ์ ๋ฐ๋ผ ๊ฐ์ด ๋ฌ๋ผ์ง๊ณ , CHAR_LENGTH()๋ ์ฝ๋ ํฌ์ธํธ ๊ธฐ์ค์ผ๋ก ์ผ๋ฐ์ ์ธ "๋ฌธ์ ์" ๊ณ์ฐ์ ์จ์. ํ์ง๋ง ๊ทธ๋ํ ๊ธฐ์ค์ ๋ณด์ด๋ ๊ธ์ ์๋ DB์์ ์ง์ํ์ง ์์ผ๋๊น JS๋ ์๋ฒ์์ ๊ณ์ฐํด์ผ ํด์. DB์ ์ปฌ๋ผ ์ ์ฝ(VARCHAR, TEXT)์ ์ ์ฅ ์ฉ๋ ๊ธฐ์ค์ผ๋ก, ์
๋ ฅ ๊ฒ์ฆ์ "๋ณด์ด๋ ๊ธ์ ์" ๊ธฐ์ค์ผ๋ก ๋๋์ด ์ค๊ณํ๋ ๊ฒ์ด UX ๊ธฐ์ค์ ๊ธ์ ์ ์ ํ์ ์ ํํ ์ ์ฉํ๋ ๋ฐฉ๋ฒ์ด์์.