본문으로 건너뛰기
기술스택 · stack/javascript/문자열 다루기: 템플릿 리터럴, 메서드, 길이의 함정

JavaScript · 5장 자료구조와 자료형

문자열 다루기: 템플릿 리터럴, 메서드, 길이의 함정

화면에 보이는 글자는 거의 다 문자열이에요. 문장을 조립하고, 검색어를 다듬고, 날짜를 쪼개고, 글자 수를 세요. 이 글에서는 실무에서 매일 쓰는 문자열 도구를 모으고, 이모지 하나가 길이 2로 세어지는 함정까지 짚어요.

#5.2

01. 문자열 조립은 템플릿 리터럴로

백틱(`)으로 감싼 문자열 안에서는 ${ }에 변수뿐 아니라 계산식, 함수 호출, 삼항 연산자까지 넣을 수 있어요. 줄바꿈도 그대로 들어가요. +로 이어 붙이는 것보다 빈칸과 따옴표 실수가 훨씬 적어요.

const name = '하늘';
const count = 3;
const price = 12900;

console.log(`${name}님, 장바구니에 ${count}개 · ${(price * count).toLocaleString('ko-KR')}원`);
console.log(`${count > 0 ? `${count}개 담김` : '비어 있어요'}`);
console.log(`첫 줄
둘째 줄`);

const user = { name: '하늘' };
console.log(`안녕, ${user.nickname}님`); // 없는 값도 그대로 글자가 돼요

마지막 줄처럼 ${ } 안의 값이 undefined나 null이면 오류 없이 "undefined"라는 글자가 화면에 찍혀요. 없을 수 있는 값은 ${user.nickname ?? '손님'}처럼 기본값을 붙여요(?.와 ??).

02. 자주 쓰는 메서드

문자열은 바꿀 수 없는 값이에요. str[0] = 'H'는 아무 일도 하지 않아요(엄격 모드에서는 오류). 그래서 문자열 메서드는 모두 원래 문자열을 그대로 두고 새 문자열을 돌려줘요. 결과를 변수에 받아야 바뀐 값을 쓸 수 있어요.

// 검색어 다듬기: 앞뒤 공백 없애고 소문자로
const query = '  JavaScript  ';
const keyword = query.trim().toLowerCase();
console.log(keyword, query.length, keyword.length);   // javascript 14 10

// 들어 있는지 · 시작 · 끝
console.log('JavaScript 노트'.includes('Script'));   // true
console.log('report.pdf'.endsWith('.pdf'));          // true
console.log('javascript'.indexOf('z'));              // -1 — 없으면 -1

// 잘라 내기: slice(시작, 끝 앞까지), 음수는 뒤에서부터
const word = 'JavaScript';
console.log(word.slice(0, 4), word.slice(-6), word.at(-1)); // Java Script t
// 쪼개고 붙이기
const date = '2026-11-06';
const [y, m, d] = date.split('-');
console.log(y, m, d);                                // 2026 11 06
console.log(['서울', '부산', '대구'].join(' · '));     // 서울 · 부산 · 대구

// 바꾸기: replace는 첫 번째만, replaceAll은 전부
console.log('010-1234-5678'.replace('-', ''));       // 0101234-5678
console.log('010-1234-5678'.replaceAll('-', ''));    // 01012345678

// 자리 맞추기: 시 · 분 · 주문번호
console.log(String(7).padStart(2, '0'));             // 07
console.log('42'.padStart(6, '0'));                  // 000042

const [y, m, d] = …는 배열을 변수 여러 개로 나눠 받는 문법이에요(구조 분해). 문자열끼리 크기를 비교하는 규칙은 문자열 비교에서 봤어요. 한글 이름 목록을 가나다순으로 정렬할 때는 a.localeCompare(b)로 비교해요.

03. length는 글자 수가 아니에요

length는 글자 수가 아니라 문자열을 이루는 UTF-16 단위의 개수예요. 한글 · 영어는 한 글자가 한 단위라서 같아 보이지만, 이모지 대부분은 두 단위를 쓰고, 피부색이 붙은 👍🏽나 가족 이모지처럼 여러 문자를 합쳐 하나로 보이는 글자는 더 많아요.

const text = '안녕👍🏽';

console.log(text.length);         // 6 — UTF-16 단위
console.log([...text].length);    // 4 — 코드 포인트(👍 + 피부색 기호 U+1F3FD)

const seg = new Intl.Segmenter('ko', { granularity: 'grapheme' });
console.log([...seg.segment(text)].length); // 3 — 사람이 보는 글자 수

console.log('😀'.length, '👨‍👩‍👧'.length);  // 2 8
①'안녕👍🏽'을 세는 세 가지 방법이에요. length는 UTF-16 단위 6개, 펼치기([...])는 코드 포인트 4개, Intl.Segmenter는 눈에 보이는 글자 3개를 세요. 글자 수 제한과 말줄임은 맨 아래 줄을 기준으로 해야 해요.

이 차이가 버그가 되는 곳은 글자 수 세기와 자르기예요. "20자까지" 같은 입력 제한에 length를 쓰면 이모지를 넣은 사람이 손해를 보고, slice로 자르면 이모지가 반쪽으로 잘려 깨진 글자(�)가 보여요. 펼치기([...text])는 반쪽은 막지만 👍🏽에서 피부색을 떼어 낼 수 있어요. 눈에 보이는 글자 단위가 필요하면 Intl.Segmenter를 써요.

한글에도 비슷한 함정이 있어요. macOS에서 올린 파일 이름처럼 '한'이 자모 'ㅎ+ㅏ+ㄴ' 세 개로 나뉘어 저장된 문자열(NFD)은 화면에선 같아 보여도 length가 3이고 === 비교도 실패해요. 비교하거나 검색하기 전에 str.normalize('NFC')로 맞춰요.

const fromMac = '한'.normalize('NFD');   // 자모로 나뉜 '한'
console.log(fromMac.length, fromMac === '한');                  // 3 false
console.log(fromMac.normalize('NFC') === '한');                 // true

긴 글을 max글자까지만 보여 주고 …을 붙이는 함수예요. 이모지가 들어간 글에서 끝 글자가 깨져요. 눈에 보이는 글자 기준으로 잘라서 좋아요👍🏽…가 나오게 고쳐 보세요.

function truncate(text, max) {
  if (text.length <= max) return text;
  return text.slice(0, max) + '…';
}

console.log(truncate('좋아요👍🏽👍🏽', 4));
console.log(truncate('짧은 글', 10));
해답 보기

length와 slice는 UTF-16 단위로 세고 잘라서, 4단위째에서 👍의 반쪽이 잘렸어요. Intl.Segmenter로 눈에 보이는 글자 배열을 만든 뒤 개수를 세고 잘라요. [...text]로 바꾸면 깨진 글자는 사라지지만 좋아요👍…처럼 피부색이 떨어져요.

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

function truncate(text, max) {
  const chars = [...graphemes.segment(text)].map(s => s.segment);
  if (chars.length <= max) return text;
  return chars.slice(0, max).join('') + '…';
}
// 좋아요👍🏽…
// 짧은 글

04. 실전 포인트

  • 언제 쓰나요? 문자열 조립은 템플릿 리터럴, 입력값은 trim() 먼저, 검색은 toLowerCase() + includes(), 형식 맞추기는 padStart, 전부 바꾸기는 replaceAll을 써요.
  • 대신 쓰는 것: 글자 수 제한 · 말줄임은 Intl.Segmenter로 세요. 복잡한 패턴 찾기(이메일 · 전화번호 형식 검사)는 정규 표현식, 숫자와 날짜 표시는 Intl.NumberFormat · Intl.DateTimeFormat이 더 정확해요.
  • 흔한 실수: 메서드 결과를 받지 않고 원래 변수가 바뀌었다고 생각하는 것, replace로 전부 바뀔 거라 생각하는 것, ${없는값}이 "undefined"로 찍히는 것, 이모지가 든 문자열을 length로 세고 slice로 자르는 것이에요.

정리

  • 템플릿 리터럴 ${ }에는 식을 무엇이든 넣을 수 있어요. 없는 값은 "undefined" 글자가 되니 기본값을 붙여요.
  • 문자열은 바꿀 수 없어요. 메서드는 새 문자열을 돌려줘요.
  • trim · includes · slice · split · replaceAll · padStart가 실무의 기본 도구예요.
  • length는 UTF-16 단위 수예요. 눈에 보이는 글자 수는 Intl.Segmenter로 세요.
const s = ' hi '; s.trim();을 실행한 뒤 s는 무엇일까요?

그대로 ' hi '예요. 문자열은 바꿀 수 없어서 trim()은 새 문자열을 돌려줄 뿐이에요. const t = s.trim()처럼 결과를 받아서 써요.

'a-b-c'.replace('-', '')의 결과는?

'ab-c'예요. 문자열을 넣은 replace는 첫 번째만 바꿔요. 전부 바꾸려면 replaceAll을 써요.

👍🏽 하나의 length가 4인 이유는?

👍와 피부색 기호(U+1F3FD) 두 문자가 합쳐져 한 글자로 보이는데, 둘 다 UTF-16 단위를 두 개씩 써서 모두 4단위이기 때문이에요. 눈에 보이는 글자 수는 Intl.Segmenter로 세요.

SIMPLE · 기술스택 / JavaScript — 5장 자료구조와 자료형