Intl.ListFormat API

배열을 로케일 인식 가능한 읽기 쉬운 목록으로 형식화

소개

여러 항목을 사용자에게 표시할 때, 개발자들은 종종 배열을 쉼표로 연결하고 마지막 항목 앞에 "and"를 추가합니다:

const users = ["Alice", "Bob", "Charlie"];
const message = users.slice(0, -1).join(", ") + ", and " + users[users.length - 1];
// "Alice, Bob, and Charlie"

이 접근 방식은 영어 구두점 규칙을 하드코딩하며 다른 언어에서는 작동하지 않습니다. 일본어는 다른 조사를 사용하고, 독일어는 다른 띄어쓰기 규칙을 가지며, 중국어는 다른 구분 기호를 사용합니다. Intl.ListFormat API는 각 로케일의 관례에 따라 목록을 형식화하여 이 문제를 해결합니다.

Intl.ListFormat의 기능

Intl.ListFormat은 배열을 모든 언어의 문법 및 구두점 규칙을 따르는 사람이 읽을 수 있는 목록으로 변환합니다. 모든 언어에 나타나는 세 가지 유형의 목록을 처리합니다:

  • 접속 목록은 "and"를 사용하여 항목을 연결합니다 ("A, B, and C")
  • 선택 목록은 "or"를 사용하여 대안을 제시합니다 ("A, B, or C")
  • 단위 목록은 접속사 없이 측정값을 형식화합니다 ("5 ft, 2 in")

API는 구두점부터 단어 선택, 띄어쓰기까지 각 언어가 이러한 목록 유형을 어떻게 형식화하는지 알고 있습니다.

기본 사용법

로케일과 옵션으로 포매터를 생성한 다음, 배열과 함께 format()를 호출합니다:

const formatter = new Intl.ListFormat("en", {
  type: "conjunction",
  style: "long"
});

const items = ["bread", "milk", "eggs"];
console.log(formatter.format(items));
// "bread, milk, and eggs"

포매터는 엣지 케이스를 포함하여 모든 길이의 배열을 처리합니다:

formatter.format([]);              // ""
formatter.format(["bread"]);       // "bread"
formatter.format(["bread", "milk"]); // "bread and milk"

목록 유형이 접속사를 제어

type 옵션은 포맷된 목록에 표시되는 접속사를 결정합니다.

접속 목록

모든 항목이 함께 적용되는 목록에는 type: "conjunction"를 사용합니다. 이것이 기본 타입입니다:

const formatter = new Intl.ListFormat("en", { type: "conjunction" });

console.log(formatter.format(["HTML", "CSS", "JavaScript"]));
// "HTML, CSS, and JavaScript"

일반적인 사용 사례로는 선택된 항목 표시, 기능 나열, 모두 적용되는 여러 값 표시 등이 있습니다.

선택 목록

대안이나 선택지를 제시하는 목록에는 type: "disjunction"를 사용합니다:

const formatter = new Intl.ListFormat("en", { type: "disjunction" });

console.log(formatter.format(["credit card", "debit card", "PayPal"]));
// "credit card, debit card, or PayPal"

이는 옵션 목록, 여러 해결책이 있는 오류 메시지, 사용자가 하나의 항목을 선택하는 모든 컨텍스트에 나타납니다.

단위 목록

접속사 없이 표시되어야 하는 측정값과 기술적 값에는 type: "unit"를 사용합니다:

const formatter = new Intl.ListFormat("en", { type: "unit" });

console.log(formatter.format(["5 feet", "2 inches"]));
// "5 feet, 2 inches"

단위 목록은 측정값, 기술 사양 및 복합 값에 사용할 수 있습니다.

목록 스타일로 상세도 제어

style 옵션은 포맷이 얼마나 상세하게 표시되는지 조정합니다. 세 가지 스타일이 있습니다: long, short, narrow.

const items = ["Monday", "Wednesday", "Friday"];

const long = new Intl.ListFormat("en", { style: "long" });
console.log(long.format(items));
// "Monday, Wednesday, and Friday"

const short = new Intl.ListFormat("en", { style: "short" });
console.log(short.format(items));
// "Monday, Wednesday, and Friday"

const narrow = new Intl.ListFormat("en", { style: "narrow" });
console.log(narrow.format(items));
// "Monday, Wednesday, Friday"

영어에서 longshort는 대부분의 목록에서 동일한 출력을 생성합니다. narrow 스타일은 접속사를 생략합니다. 다른 언어들은 스타일 간에 더 많은 차이를 보이며, 특히 선택 목록에서 그렇습니다.

언어별 목록 형식 지정 방식

각 언어는 고유한 목록 형식 지정 규칙을 가지고 있습니다. Intl.ListFormat은 이러한 차이를 자동으로 처리합니다.

영어는 쉼표, 공백 및 접속사를 사용합니다:

const en = new Intl.ListFormat("en");
console.log(en.format(["Tokyo", "Paris", "London"]));
// "Tokyo, Paris, and London"

독일어는 동일한 쉼표 구조를 사용하지만 다른 접속사를 사용합니다:

const de = new Intl.ListFormat("de");
console.log(de.format(["Tokyo", "Paris", "London"]));
// "Tokyo, Paris und London"

일본어는 다른 구분 기호와 조사를 사용합니다:

const ja = new Intl.ListFormat("ja");
console.log(ja.format(["東京", "パリ", "ロンドン"]));
// "東京、パリ、ロンドン"

중국어는 완전히 다른 구두점을 사용합니다:

const zh = new Intl.ListFormat("zh");
console.log(zh.format(["东京", "巴黎", "伦敦"]));
// "东京、巴黎和伦敦"

이러한 차이는 구두점을 넘어 띄어쓰기 규칙, 접속사 배치 및 문법적 조사까지 확장됩니다. 단일 접근 방식을 하드코딩하면 다른 언어에서 작동하지 않습니다.

사용자 정의 렌더링을 위한 formatToParts 사용

formatToParts() 메서드는 문자열 대신 객체 배열을 반환합니다. 각 객체는 포맷된 목록의 한 부분을 나타냅니다:

const formatter = new Intl.ListFormat("en");
const parts = formatter.formatToParts(["red", "green", "blue"]);

console.log(parts);
// [
//   { type: "element", value: "red" },
//   { type: "literal", value: ", " },
//   { type: "element", value: "green" },
//   { type: "literal", value: ", and " },
//   { type: "element", value: "blue" }
// ]

각 부분은 typevalue를 가집니다. type는 목록 항목의 경우 "element"이거나 포맷 구두점 및 접속사의 경우 "literal"입니다.

이 구조는 요소와 리터럴에 다른 스타일이 필요한 사용자 정의 렌더링을 가능하게 합니다:

const formatter = new Intl.ListFormat("en");
const items = ["Alice", "Bob", "Charlie"];

const html = formatter.formatToParts(items)
  .map(part => {
    if (part.type === "element") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// "<strong>Alice</strong>, <strong>Bob</strong>, and <strong>Charlie</strong>"

이 접근 방식은 실제 목록 항목에 사용자 정의 표현을 적용하면서 로케일에 맞는 구두점을 유지합니다.

성능을 위한 포매터 재사용

Intl.ListFormat 인스턴스를 생성하는 것은 오버헤드가 있습니다. 포매터를 한 번 생성하고 재사용하세요:

// Create once
const listFormatter = new Intl.ListFormat("en", { type: "conjunction" });

// Reuse many times
function displayUsers(users) {
  return listFormatter.format(users.map(u => u.name));
}

function displayTags(tags) {
  return listFormatter.format(tags);
}

여러 로케일을 사용하는 애플리케이션의 경우 포매터를 맵에 저장하세요:

const formatters = new Map();

function getListFormatter(locale, options) {
  const key = `${locale}-${options.type}-${options.style}`;
  if (!formatters.has(key)) {
    formatters.set(key, new Intl.ListFormat(locale, options));
  }
  return formatters.get(key);
}

const formatter = getListFormatter("en", { type: "conjunction", style: "long" });
console.log(formatter.format(["a", "b", "c"]));

이 패턴은 여러 로케일과 구성을 지원하면서 반복적인 초기화 비용을 줄입니다.

오류 메시지 포맷팅

폼 유효성 검사는 종종 여러 오류를 생성합니다. 선택지를 제시하기 위해 선언 목록으로 포맷하세요:

const formatter = new Intl.ListFormat("en", { type: "disjunction" });

function validatePassword(password) {
  const errors = [];

  if (password.length < 8) {
    errors.push("at least 8 characters");
  }
  if (!/[A-Z]/.test(password)) {
    errors.push("an uppercase letter");
  }
  if (!/[0-9]/.test(password)) {
    errors.push("a number");
  }

  if (errors.length > 0) {
    return `Password must contain ${formatter.format(errors)}.`;
  }

  return null;
}

console.log(validatePassword("weak"));
// "Password must contain at least 8 characters, an uppercase letter, or a number."

선언 목록은 사용자가 이러한 문제 중 하나를 수정해야 한다는 것을 명확히 하며, 포맷팅은 각 로케일의 규칙에 맞게 조정됩니다.

선택된 항목 표시

사용자가 여러 항목을 선택할 때 접속 목록으로 선택 항목을 포맷하세요:

const formatter = new Intl.ListFormat("en", { type: "conjunction" });

function getSelectionMessage(selectedFiles) {
  if (selectedFiles.length === 0) {
    return "No files selected";
  }

  if (selectedFiles.length === 1) {
    return `${selectedFiles[0]} selected`;
  }

  return `${formatter.format(selectedFiles)} selected`;
}

console.log(getSelectionMessage(["report.pdf", "data.csv", "notes.txt"]));
// "report.pdf, data.csv, and notes.txt selected"

이 패턴은 파일 선택, 필터 선택, 카테고리 선택 및 모든 다중 선택 인터페이스에 적용됩니다.

긴 목록 처리

항목이 많은 목록의 경우 포맷하기 전에 잘라내는 것을 고려하세요:

const formatter = new Intl.ListFormat("en", { type: "conjunction" });

function formatUserList(users) {
  if (users.length <= 3) {
    return formatter.format(users);
  }

  const visible = users.slice(0, 2);
  const remaining = users.length - 2;

  return `${formatter.format(visible)}, and ${remaining} others`;
}

console.log(formatUserList(["Alice", "Bob", "Charlie", "David", "Eve"]));
// "Alice, Bob, and 3 others"

이는 전체 개수를 표시하면서 가독성을 유지합니다. 정확한 임계값은 인터페이스 제약 조건에 따라 다릅니다.

브라우저 지원 및 폴백

Intl.ListFormat은 2021년 4월부터 모든 최신 브라우저에서 작동합니다. Chrome 72+, Firefox 78+, Safari 14.1+, Edge 79+를 지원합니다.

기능 감지로 지원 여부를 확인하세요:

if (typeof Intl.ListFormat !== "undefined") {
  const formatter = new Intl.ListFormat("en");
  return formatter.format(items);
} else {
  // Fallback for older browsers
  return items.join(", ");
}

더 넓은 호환성을 위해 @formatjs/intl-listformat와 같은 폴리필을 사용하세요. 필요한 환경에만 설치하세요:

if (typeof Intl.ListFormat === "undefined") {
  await import("@formatjs/intl-listformat/polyfill");
}

현재 브라우저 지원을 고려할 때, 대부분의 애플리케이션은 폴리필 없이 Intl.ListFormat을 직접 사용할 수 있습니다.

피해야 할 일반적인 실수

포매터를 반복적으로 생성하는 것은 리소스를 낭비합니다:

// Inefficient
function display(items) {
  return new Intl.ListFormat("en").format(items);
}

// Efficient
const formatter = new Intl.ListFormat("en");
function display(items) {
  return formatter.format(items);
}

사용자 대상 텍스트에 array.join()를 사용하면 로컬라이제이션 문제가 발생합니다:

// Breaks in other languages
const text = items.join(", ");

// Works across languages
const formatter = new Intl.ListFormat(userLocale);
const text = formatter.format(items);

영어 접속사 규칙이 보편적으로 적용된다고 가정하면 다른 로케일에서 잘못된 출력이 발생합니다. 항상 사용자의 로케일을 생성자에 전달하세요.

빈 배열을 처리하지 않으면 예상치 못한 출력이 발생할 수 있습니다:

// Defensive
function formatItems(items) {
  if (items.length === 0) {
    return "No items";
  }
  return formatter.format(items);
}

format([])는 빈 문자열을 반환하지만, 명시적인 빈 상태 처리는 사용자 경험을 개선합니다.

Intl.ListFormat을 사용해야 하는 경우

산문에서 여러 항목을 표시할 때마다 Intl.ListFormat을 사용하세요. 여기에는 탐색 경로, 선택된 필터, 유효성 검사 오류, 사용자 목록, 카테고리 태그 및 기능 목록이 포함됩니다.

테이블이나 옵션 메뉴와 같은 구조화된 데이터 표시에는 사용하지 마세요. 이러한 컴포넌트는 산문 목록 규칙 외부에 자체 포매팅 요구 사항이 있습니다.

이 API는 수동 문자열 연결 및 결합 패턴을 대체합니다. 사용자 대상 텍스트에 join(", ")를 작성할 때마다 Intl.ListFormat이 더 나은 로케일 지원을 제공하는지 고려하세요.