블로그 목록
frontend

Intl.ListFormat으로 목록 문장 만들기 — "사과, 바나나 및 포도" 직접 join 그만

선택한 태그 목록을 "React, TypeScript 및 Next.js"처럼 문장으로 보여줘야 했다. 처음엔 별거 아니라고 생각했다. arr.join(', ') 하면 되잖아.

그런데 막상 해보니 애매한 게 많았다. 마지막 항목 앞에는 쉼표가 아니라 "및" 같은 접속사를 넣어야 자연스럽고, 항목이 두 개일 땐 쉼표 없이 그냥 "React 및 TypeScript", 하나일 땐 접속사도 없어야 한다. 이걸 직접 분기로 짜면 생각보다 지저분해진다.

function formatList(items) {
  if (items.length === 0) return '';
  if (items.length === 1) return items[0];
  if (items.length === 2) return `${items[0]} 및 ${items[1]}`;
  return `${items.slice(0, -1).join(', ')} 및 ${items.at(-1)}`;
}

돌아가긴 하는데, 언어가 늘어나면 지옥이다. 영어는 "A, B, and C"처럼 마지막 쉼표(옥스퍼드 콤마) 규칙이 또 다르고, 언어마다 접속사 위치와 띄어쓰기가 전부 제각각이다.

Intl.ListFormat 한 줄로 끝난다

이런 로케일별 목록 조합은 Intl.ListFormat이 알아서 해준다.

const items = ['React', 'TypeScript', 'Next.js'];

const ko = new Intl.ListFormat('ko', { type: 'conjunction' });
ko.format(items);
// → "React, TypeScript 및 Next.js"

const en = new Intl.ListFormat('en', { type: 'conjunction' });
en.format(items);
// → "React, TypeScript, and Next.js"

위에서 직접 짠 분기 로직이 전부 사라졌다. 항목이 1개든 2개든 알아서 처리하고, 로케일만 바꾸면 영어 규칙도 공짜로 따라온다.

type 옵션이 핵심

type에 따라 접속사가 달라진다.

// "그리고" 계열 — 모두 포함
new Intl.ListFormat('ko', { type: 'conjunction' }).format(items);
// → "React, TypeScript 및 Next.js"

// "또는" 계열 — 택일
new Intl.ListFormat('ko', { type: 'disjunction' }).format(items);
// → "React, TypeScript 또는 Next.js"

// 접속사 없이 — 단위 나열
new Intl.ListFormat('ko', { type: 'unit' }).format(['3시간', '30분']);
// → "3시간 30분"

필터 UI에서 "선택된 조건 중 하나"를 보여줄 땐 disjunction, 단순 나열엔 unit. 용도에 맞게 고르면 된다.

style: 'short'를 주면 더 짧게 줄여주고, formatToParts()를 쓰면 접속사 부분만 따로 스타일을 입히는 것도 된다.

new Intl.ListFormat('ko', { type: 'conjunction' })
  .formatToParts(items)
  .filter((p) => p.type === 'element');
// → 실제 항목만 골라내서 각각 <a> 태그로 감쌀 때 유용

Intl.ListFormat은 이제 모든 최신 브라우저에서 기본으로 쓸 수 있다. 목록을 문장으로 바꿀 일이 생기면, join과 if 분기부터 짜지 말고 이걸 먼저 떠올리자. 다국어까지 공짜로 따라오니 손해 볼 게 없다.