UTF-16이란? 이모지 한 글자가 JavaScript에서 2글자인 이유

AI에게 웹사이트나 앱을 만들어 달라고 부탁한 뒤, 글자 수를 세는 기능에서 이상한 문제를 발견할 수 있습니다. 분명 화면에는 웃는 이모지 😀 하나만 입력했는데 프로그램이 2글자라고 계산하는 것입니다. 반면 한글 '가'는 한 글자로 계산됩니다. 왜 이런 일이 생길까요? AI가 코드를 잘못 작성한 것일까요? 반드시 그렇지는 않습니다. 컴퓨터가 문자를 저장하고 계산할 때 사용하는 방식과, 사람이 눈으로 보는 글자 수가 서로 다르기 때문입니다. 그 차이를 이해하는 데 중요한 개념이 UTF-16입니다. 이번 글에서는 UTF-16이 무엇인지부터 시작해, 이모지가 두 개로 계산되는 이유, JavaScript의 length가 실제로 세는 것, 문자열을 자를 때 생길 수 있는 문제를 차례로 알아보겠습니다. 이 글의 코드·출력 예시는 2026년 10월 16일 Unicode와 MDN 공식 자료에 근거한 교육용 예시입니다. 작성 과정에서 실제 브라우저나 Node.js를 실행해 테스트한 결과를 보여 주는 것은 아닙니다.

UTF-16이란 · 핵심 흐름UTF-16이란의 주요 개념과 확인 순서를 네 단계로 정리한 자체 제작 도식.UTF-16이란 · 핵심 흐름1코드 유닛UTF-16은 16비트 단위로저장합니다2length 측정JavaScript는 코드 단위를셉니다3서로게이트 쌍일부 이모지는 두 단위를 씁니다4글자 수 구분코드 포인트와 화면 글자를나눕니다설명용 요약 도식입니다. 최신 기능과 이용 조건은 본문 및 공식 문서에서 확인하세요.EMFLS AI · 자체 도식
자체 제작 개념도 · EMFLS AI

1. UTF-16은 무엇인가요?

UTF-16은 유니코드(Unicode) 문자를 표현하는 방식 중 하나입니다. 컴퓨터는 사람처럼 '가', 'A', '😀'라는 모양 자체를 저장하는 것이 아니라, 문자에 대응하는 숫자와 그 숫자를 표현한 데이터를 처리합니다. 유니코드는 세계 여러 언어의 문자와 기호에 번호를 부여하는 체계입니다. 예를 들어 대문자 A나 한글 음절, 이모지에도 각자 고유한 코드 포인트(code point)가 있습니다. 그 숫자를 실제 데이터로 표현하려면 규칙이 필요합니다. UTF-8과 UTF-16은 그런 유니코드 인코딩 방식입니다. 이 중 UTF-16은 16비트 크기의 코드 단위(code unit)를 기본으로 사용합니다. 여기서 16비트는 2바이트입니다. 하지만 UTF-16이 모든 문자를 언제나 2바이트로 표현한다는 뜻은 아닙니다. 한 개의 코드 단위로 표현되는 문자도 있고, 두 개의 코드 단위가 필요한 문자도 있기 때문입니다.

2. 왜 문자에 번호가 필요한가요?

우리에게는 '가'와 '나'가 명확히 다른 글자입니다. 하지만 컴퓨터에 정보를 주고받으려면 서로 다른 기기와 프로그램이 같은 번호를 같은 문자로 해석할 수 있어야 합니다. 가령 대문자 A의 유니코드 코드 포인트는 U+0041입니다. '가'의 코드 포인트는 U+AC00입니다. 웃는 얼굴 😀은 U+1F600입니다. 이 번호는 단순한 화면상의 글자 순번이 아닙니다. 문자에 부여된 고유한 식별 번호입니다. UTF-16은 이런 코드 포인트를 16비트 단위 하나 또는 둘로 표현하는 규칙입니다. 따라서 유니코드 자체와 UTF-16이라는 표현 방식은 구분해야 합니다. 유니코드는 문자의 이름표를 정하는 체계에 가깝고, UTF-16은 그 이름표를 컴퓨터가 처리할 수 있는 규칙에 따라 표현하는 방식이라고 생각하면 됩니다.

3. '코드 단위'는 무엇인가요?

UTF-16을 이해하는 핵심은 코드 단위(code unit)라는 개념입니다. 코드 단위는 UTF-16이 데이터를 나누어 다룰 때 사용하는 기본 단위입니다. UTF-16에서 이 단위는 16비트입니다. 예를 들어 문자 A는 UTF-16 코드 단위 하나로 표현됩니다. 일반적인 완성형 한글 음절인 가도 코드 단위 하나로 표현됩니다. 그런데 모든 유니코드 문자가 단위 하나에 들어가는 것은 아닙니다. 일부 이모지나 문자에는 코드 단위 두 개가 필요합니다. 이것이 웹사이트에서 '글자 수를 계산했더니 이모지가 2로 나온다'는 현상을 이해하는 첫걸음입니다.

입력한 문자 유니코드 코드 포인트 UTF-16 코드 단위 수
A U+0041 1
가 U+AC00 1
😀 U+1F600 2
A😀 두 코드 포인트 3

표의 마지막 예시는 A의 단위 하나 + 이모지의 단위 두 개를 합한 결과입니다. 여기서 '단위 수'와 '사람 눈에 보이는 글자 수'를 혼동하면 안 됩니다.

4. 이모지 하나를 두 단위로 표현하는 이유

UTF-16의 한 코드 단위는 16비트이므로 표현 가능한 값의 범위가 제한돼 있습니다. 자주 사용하는 많은 문자는 기본 다국어 평면(BMP)에 포함되어 한 단위로 표현할 수 있습니다. 반면 😀처럼 코드 포인트가 U+FFFF를 넘는 문자는 단위 하나로 직접 표현할 수 없습니다. UTF-16은 이런 문자를 두 개의 코드 단위 조합으로 나타냅니다. 이 조합을 서로게이트 쌍(surrogate pair)이라고 부릅니다. 중요한 점은 이 두 코드 단위가 각각 별개의 두 문자라는 뜻은 아니라는 것입니다. 한 이모지를 표현하기 위해 협력하는 두 개의 구성 단위라고 이해하면 됩니다. 서로게이트 쌍은 앞쪽 단위인 높은 서로게이트(high surrogate)와 뒤쪽 단위인 낮은 서로게이트(low surrogate)로 구성됩니다. 유니코드 규격은 이 두 단위가 짝을 이뤄 보충 코드 포인트를 표현하도록 정의합니다. 제대로 짝지어지지 않은 단위는 올바른 UTF-16 인코딩이 아닙니다. 처음부터 코드 단위의 정확한 숫자값을 외울 필요는 없습니다. '어떤 문자는 UTF-16에서 한 단위, 어떤 문자는 두 단위'라는 사실만 이해해도 글자 수 오류의 상당 부분을 파악할 수 있습니다.

5. JavaScript에서 이모지가 두 글자로 나오는 이유

이제 실제 웹사이트에서 생기는 현상을 살펴보겠습니다. JavaScript는 웹페이지에서 자주 사용되는 언어입니다. 문자열의 길이를 알아볼 때 length 속성을 자주 사용합니다. 다음 코드를 보세요.

console.log("A".length);    // 1
console.log("가".length);   // 1
console.log("😀".length);   // 2
console.log("A😀".length);  // 3

이 코드의 주석에 적힌 숫자는 표준 동작에 따른 예상 결과입니다. 왜 "😀".length는 2일까요? JavaScript의 문자열 length는 일반적으로 사람이 보는 글자 수가 아니라 UTF-16 코드 단위 개수를 알려 주기 때문입니다. 따라서 😀는 코드 포인트 하나로 된 이모지지만, UTF-16 단위가 두 개라서 길이가 2로 나옵니다. 반대로 한글 가는 UTF-16 코드 단위 하나로 표현되므로 길이가 1입니다. 이 결과만으로 JavaScript가 한글이나 이모지를 잘못 인식한다고 결론 내릴 수는 없습니다. 서로 다른 단위로 길이를 세고 있는 것입니다.

6. UTF-16의 2는 2바이트인가요, 2글자인가요?

여기서는 세 종류의 단위를 나누어 생각해야 합니다. 첫째, 16비트 코드 단위 UTF-16 내부에서 데이터를 구성하는 기본 단위입니다. 한 단위는 16비트, 즉 2바이트 크기입니다. 둘째, 코드 포인트 유니코드가 문자에 부여한 고유 번호입니다. 이모지 😀는 코드 포인트 하나입니다. 셋째, 사람이 눈으로 보는 글자 표시 화면에서 하나로 느끼는 문자입니다. 때로는 코드 포인트 하나와 일치하지만, 여러 코드 포인트가 합쳐져 한 글자처럼 보일 수도 있습니다. 표로 정리해 보겠습니다.

무엇을 세나요? 예시: 😀 의미
UTF-16 코드 단위 2 내부 표현 단위 수
유니코드 코드 포인트 1 문자 번호 개수
보이는 이모지 보통 1 화면에 표시된 글자 느낌
UTF-16 코드 단위의 데이터 크기 4바이트 두 단위의 데이터 크기, 별도의 저장 형식 부가 정보 제외

여기서 한 가지 주의해야 합니다. length가 2라고 해서 문자열 전체가 파일에 정확히 4바이트만 저장된다는 뜻은 아닙니다. 프로그램의 내부 자료 구조, 파일 인코딩, 네트워크 전송 형식 등에 따라 추가 데이터가 있거나 UTF-8처럼 다른 표현 방식으로 변환될 수 있기 때문입니다. 그래서 글자 수 제한, 파일 크기, API 요청 크기를 설계할 때는 각각 어떤 단위를 요구하는지 확인해야 합니다.

7. length 대신 Array.from()을 쓰면 해결되나요?

JavaScript에는 문자열을 구성하는 코드 포인트 단위로 순회하는 방법이 있습니다. 예를 들어:

const text = "😀";

console.log(text.length);             // 2
console.log(Array.from(text).length); // 1
console.log([...text].length);        // 1

이 예제에서 Array.from()과 전개 문법(...)은 서로게이트 쌍을 하나의 유니코드 코드 포인트로 다루는 데 도움이 됩니다. 그래서 "😀" 한 개의 길이를 사람이 예상한 1에 가깝게 셀 수 있습니다. 그렇다면 모든 문제가 해결될까요? 아닙니다. 눈에 보이는 이모지 하나가 코드 포인트 여러 개의 조합으로 만들어질 수도 있기 때문입니다. 예를 들어 가족 이모지 👨‍👩‍👧‍👦는 여러 사람 이모지와 연결 문자가 함께 있는 시퀀스입니다.

const family = "👨‍👩‍👧‍👦";

console.log(family.length);        // 11
console.log([...family].length);   // 7

화면에서는 보통 가족 이모지 하나처럼 보이지만, UTF-16 코드 단위로는 11개, 코드 포인트로는 7개가 됩니다. 이는 디스플레이의 글자 모양이 코드 포인트 하나와 반드시 같지 않다는 예입니다. 운영체제나 글꼴에 따라 화면 표현은 달라질 수 있습니다. 따라서 Array.from(text).length가 항상 사람이 보는 글자 수를 구해 주는 공식이라고 말하면 안 됩니다.

8. 사람이 보는 한 글자를 기준으로 세려면?

실제로 사용자가 보는 단어·이모지 수와 가까운 값을 세고 싶다면 문자소 클러스터(grapheme cluster)라는 개념을 알아야 합니다. 예를 들어 어떤 글자가 기본 문자 하나와 뒤에 붙는 결합 기호로 이루어진다면, 내부적으로는 여러 코드 포인트여도 화면에서는 한 글자처럼 보일 수 있습니다. JavaScript에서는 지원되는 환경에서 Intl.Segmenter를 이용해 사용자에게 보이는 문자 단위에 가까운 분할을 할 수 있습니다. 다음은 개념을 보여 주는 예제입니다.

const segmenter = new Intl.Segmenter("ko", {
  granularity: "grapheme"
});

const text = "😀";
const count = [...segmenter.segment(text)].length;

console.log(count); // 1

이 역시 예상 결과입니다. 사용하는 JavaScript 런타임에서 Intl.Segmenter를 지원하는지 확인해야 합니다. 여기서 가장 중요한 것은 UTF-16 코드 단위 수와 사용자 눈에 보이는 글자 수는 다른 문제라는 사실입니다. 문자소 클러스터는 별도의 깊이 있는 주제이므로, 이번 글에서는 UTF-16과의 관계만 간단히 살펴보겠습니다.

9. 한글도 글자 수가 달라질 수 있나요?

일반적인 완성형 한글 음절인 가, 나 등은 UTF-16에서 코드 단위 하나로 표현됩니다. 하지만 한글 역시 문자 조합 방식에 따라 같은 글자처럼 보이면서 내부 표현이 다를 수 있습니다. 예를 들어 어떤 문자가 미리 조합된 단일 코드 포인트로 표현될 수도 있고, 기본 글자와 결합 기호가 따로 나타나는 경우도 있습니다. 라틴 문자를 예로 들면 더 이해하기 쉽습니다. é라는 글자는 미리 조합된 한 코드 포인트로 표현할 수도 있고, e와 결합 악센트 기호를 따로 적어 표현할 수도 있습니다. 눈에는 비슷하게 보이지만 코드 포인트 수와 length가 달라질 수 있습니다. 이런 차이는 텍스트 검색, 이름 비교, 사용자 입력 제한 등을 설계할 때 영향을 줄 수 있습니다. 겉으로 똑같이 보인다는 이유만으로 내부 코드 단위도 동일하다고 가정하면 안 됩니다. 이 문제는 문자 정규화와 결합 문자라는 다른 기술 주제와 연결되지만, 이번 글에서 꼭 기억할 점은 화면과 코드의 글자 구성이 같지 않을 수 있다는 것입니다.

10. 문자열을 중간에서 자를 때도 문제가 생기나요?

네. 이것이 실제 개발에서 중요한 부분입니다. 예를 들어 다음과 같은 코드가 있다고 해 보겠습니다.

const text = "😀";
const cut = text.slice(0, 1);

console.log(cut.length); // 1

text는 이모지 하나이지만 UTF-16 코드 단위는 두 개입니다. slice(0, 1)은 첫 번째 코드 단위까지만 잘라내므로, 이모지를 표현하는 서로게이트 쌍의 절반만 남길 수 있습니다. 이렇게 잘린 값은 원래 문자로 올바르게 표시되지 않을 수 있습니다. 따라서 이모지·결합 문자가 포함된 문자열을 사용자에게 보여 줄 목적으로 자를 때는 어떤 단위로 자르는지를 명확히 해야 합니다. 코드 포인트 단위 분할만 필요하다면 전개 문법이나 Array.from()을 검토할 수 있습니다. 하지만 눈에 보이는 글자 하나를 절대로 중간에서 자르지 않으려면 문자소 클러스터 단위의 처리를 고려해야 합니다. 또 문자열을 길이 제한에 맞춰 잘랐다고 해도 데이터베이스나 외부 서비스의 저장·전송 제한이 자동으로 충족되는 것은 아닙니다.

11. 글자 수 제한 기능에서는 어떤 점을 확인해야 하나요?

AI와 함께 웹사이트를 만들면 다음과 같은 기능을 쉽게 요청하게 됩니다.

소개글은 100글자까지만 입력할 수 있게 만들어 줘. 그런데 '100글자'라는 말 자체가 모호할 수 있습니다. 개발자는 다음 중 무엇을 제한하려는지 결정해야 합니다. UTF-16 코드 단위 100개일까요? 유니코드 코드 포인트 100개일까요? 사용자가 눈으로 보는 글자 100개일까요? 또는 서버에 전송하는 파일·요청 데이터의 바이트 크기를 제한하는 것일까요? 이 값들은 같지 않을 수 있습니다. 특히 이모지를 많이 사용하는 프로필 입력창, 채팅 앱, 닉네임, SNS 글자 수 제한에서는 차이가 눈에 띌 수 있습니다. 사용자가 화면에서 99글자라고 느끼는데 코드에서는 101이라고 세면 혼란스러울 수 있습니다. 반대로 서버나 외부 API가 UTF-8의 바이트 수 제한을 요구한다면 화면상의 글자 수를 기준으로 허용해도 실제 제한을 넘길 수 있습니다. 따라서 요구사항에 적힌 '글자'가 어떤 단위인지 먼저 합의하는 것이 중요합니다.

12. UTF-16과 UTF-8은 어떤 차이가 있나요?

UTF-8과 UTF-16은 둘 다 유니코드 텍스트를 표현하는 방식입니다. 다만 사용하는 코드 단위와 데이터 표현 방식이 다릅니다. UTF-8은 8비트 단위를 바탕으로 유니코드 코드 포인트를 보통 1~4바이트로 표현합니다. UTF-16은 16비트 코드 단위를 사용하며, 코드 포인트에 따라 하나 또는 두 개 단위로 표현합니다. 웹 문서와 네트워크에서는 UTF-8이 널리 사용되지만, JavaScript 문자열의 length는 UTF-16 코드 단위 수를 기준으로 작동합니다. 따라서 웹페이지가 UTF-8로 저장되어 있어도 JavaScript에서 "😀".length가 2인 것은 모순이 아닙니다. HTML 파일이 어떤 인코딩으로 저장되는지와 JavaScript가 문자열을 어떤 단위로 다루는지는 구분해야 합니다. 두 기술의 다른 세부사항을 전부 외우기보다, UTF-16 코드 단위 수와 파일 바이트 크기를 같은 것으로 생각하면 안 된다는 점부터 기억하세요.

13. AI 코딩 도구에 UTF-16 오류를 수정해 달라고 할 때

웹앱에서 이모지 글자 수가 이상하게 나온다면 AI에게 단순히 “글자 수 버그 고쳐”라고 말하는 것보다 무엇을 세고 싶은지 알려 주는 편이 좋습니다. 아래 예시 프롬프트를 사용해 보세요.

내 웹앱의 글자 수 제한을 확인해 줘.

현재 문제:
- 화면에는 이모지 하나만 있는데 길이가 2로 계산됨
- JavaScript의 String.length를 사용한 것 같음

요구사항:
1. 현재 코드에서 length를 어디에 사용하는지 찾아줘.
2. UTF-16 코드 단위 수와 실제 사용자가 보는
   글자 수가 다른 이유를 설명해 줘.
3. 우리 기능이 어느 단위로 제한해야 하는지
   먼저 확인해야 할 질문을 제시해 줘.
4. 이모지, 결합 문자, 한글 조합 문자 등
   경계 사례를 포함해 테스트를 설계해 줘.
5. 기존 입력값 저장·전송 방식은 승인 없이 바꾸지 마.
6. 실제로 테스트하지 않은 결과는 PASS라고 쓰지 마.

코드를 고쳤다면 변경 파일과 남은 위험을 보고해 줘.

이 요청은 AI에게 곧바로 특정 코드 한 줄을 바꾸라고 하는 것이 아니라 현재 정책과 계산 단위가 일치하는지 확인하도록 돕습니다. 특히 이메일 제목, SMS, 결제 메시지처럼 전송 규칙과 비용이 따로 정해진 서비스에서는 문자 표시 수와 청구 기준을 혼동하지 않는 것이 중요합니다.

14. 자주 묻는 질문(FAQ)

Q1. UTF-16은 프로그래밍 언어인가요?

아닙니다. UTF-16은 유니코드 문자를 표현하는 인코딩 방식입니다. JavaScript 같은 언어는 문자열을 다룰 때 이와 관련된 코드 단위 규칙을 사용합니다.

Q2. UTF-16에서는 한글이 두 글자로 계산되나요?

일반적인 완성형 한글 음절은 코드 단위 하나로 표현됩니다. 하지만 결합 문자나 다른 문자 표현 방식이 있다면 화면에서 보이는 글자 수와 코드 단위 수가 달라질 수 있습니다.

Q3. 이모지는 모두 JavaScript에서 길이가 2인가요?

아닙니다. 많은 이모지는 서로게이트 쌍 두 단위로 표현되지만, 모든 이모지가 정확히 두 단위라는 뜻은 아닙니다. 여러 코드 포인트를 조합한 이모지는 더 길 수 있습니다.

Q4. String.length가 세는 것은 무엇인가요?

JavaScript 문자열의 UTF-16 코드 단위 개수입니다. 화면에서 보이는 글자 수를 항상 나타내지는 않습니다.

Q5. Array.from(text).length를 쓰면 눈에 보이는 글자 수가 나오나요?

항상 그렇지는 않습니다. 보통 유니코드 코드 포인트를 기준으로 순회하는 데 도움이 되지만, 여러 코드 포인트가 조합된 이모지는 화면에서 하나로 보일 수 있습니다.

Q6. UTF-16 코드 단위 두 개는 실제로 4바이트인가요?

코드 단위 두 개 자체는 16비트×2 = 32비트, 즉 4바이트에 해당합니다. 다만 문자열의 실제 파일 크기·메모리 사용량·네트워크 전송량은 별도 인코딩과 자료 구조에 따라 달라집니다.

Q7. 문자열을 잘랐더니 이상한 기호가 나오는 이유는 무엇인가요?

slice() 같은 함수가 UTF-16 코드 단위 중간에서 서로게이트 쌍을 분리했을 가능성이 있습니다. 코드 포인트 또는 문자소 클러스터 기준으로 잘라야 하는지 검토하세요.

Q8. UTF-8 파일을 쓰는데도 왜 JavaScript에서는 이모지 길이가 2인가요?

파일을 저장·전송할 때 사용하는 UTF-8 인코딩과 JavaScript의 문자열 length 계산 기준이 서로 다르기 때문입니다.

Q9. UTF-16의 글자 수와 AI 토큰 수는 같은가요?

아닙니다. AI 모델의 토큰 수는 모델마다 다른 토크나이저가 입력을 어떻게 나누는지에 따라 결정됩니다. UTF-16 코드 단위 수와 같다고 생각하면 안 됩니다.

마무리: 글자의 '모양'과 '코드 단위'는 다릅니다

UTF-16을 처음 접했을 때 가장 낯선 점은 눈으로 보는 한 글자가 컴퓨터 안에서는 두 단위로 표현될 수 있다는 사실입니다. 하지만 이제 다음 세 가지만 기억해 보세요. UTF-16은 유니코드 문자를 표현하는 방식이며, 16비트 코드 단위를 하나 또는 둘씩 사용합니다. JavaScript의 length는 UTF-16 코드 단위를 센다는 점 때문에 이모지가 한 글자로 보이더라도 2가 나올 수 있습니다. 코드 단위·코드 포인트·화면에 보이는 글자는 서로 다른 기준이므로 앱에서 글자 수 제한과 문자열 자르기를 만들 때 단위를 정확히 정해야 합니다. 다음에 AI가 만든 웹사이트에서 이모지 글자 수가 이상해 보인다면, 단순한 오류라고 보기 전에 프로그램이 지금 어떤 종류의 길이를 세고 있는지부터 확인해 보세요.

공식 자료

제품의 동작과 제한은 바뀔 수 있으므로 게시 시점에도 공식 문서에서 다시 확인하세요.

함께 읽으면 좋은 글