형식화된 출력 결과를 스타일별로 분할하는 방법
formatToParts()를 사용하여 형식화된 출력의 개별 구성 요소에 접근하고 맞춤 스타일을 적용하세요
소개
JavaScript 포매터에서 제공하는 format() 메서드는 "$1,234.56" 혹은 "2025년 1월 15일"처럼 완성된 문자열을 반환합니다. 단순한 표시에는 효과적이지만, 개별 요소에 각각 다른 스타일을 적용할 수 없습니다. 예를 들어, 통화 기호만 굵게 표시하거나, 월 이름에만 색상을 지정하거나, 특정 부분에만 커스텀 마크업을 넣을 수 없습니다.
이 문제를 해결하기 위해 JavaScript에서는 formatToParts() 메서드를 제공합니다. 이 메서드는 하나의 문자열 대신, 형식화된 출력의 각 부분을 나타내는 객체 배열을 반환합니다. 각 객체는 currency, month, element 같은 타입과 실제 값을 포함합니다. 이러한 부분별 데이터를 활용하면 원하는 대로 스타일을 지정하거나, 복잡한 레이아웃을 만들거나, 다양한 사용자 인터페이스에 적용할 수 있습니다.
formatToParts() 메서드는 다양한 Intl 포매터, 즉 NumberFormat, DateTimeFormat, ListFormat, RelativeTimeFormat, DurationFormat 등에 모두 적용됩니다. 따라서 JavaScript의 국제화 포매팅에서 일관적으로 사용할 수 있는 패턴입니다.
왜 형식화된 문자열은 쉽게 스타일링할 수 없는가
“$1,234.56”처럼 형식화된 문자열을 받으면 통화 기호와 숫자가 어디서 끝나고 시작되는지 쉽게 알 수 없습니다. 로케일에 따라 기호의 위치가 다르며, 구분 기호도 다르게 사용됩니다. 이러한 문자열을 신뢰성 있게 파싱하려면 이미 Intl API에서 구현한 복잡한 포맷 규칙을 다시 구현해야 하므로 매우 까다롭습니다.
예를 들어, 대시보드에서 통화 기호를 다른 색상으로 표시해야 한다고 가정해봅시다. format()를 사용할 경우 아래와 같이 처리해야 합니다:
- 통화 기호가 어느 문자에 해당하는지 판별
- 기호와 숫자 사이의 공백 처리
- 로케일마다 다른 기호 위치에 대응
- 숫자를 손상시키지 않도록 문자열을 신중하게 파싱
이 방법은 불안정하고 오류가 발생하기 쉽습니다. 로케일의 형식 규칙이 조금만 변경되어도 파싱 로직이 쉽게 깨질 수 있습니다.
날짜, 리스트, 기타 형식화된 출력에서도 동일한 문제가 존재합니다. 로케일별 형식 규칙을 다시 구현하지 않는 이상, 포맷된 문자열에서 각 구성 요소를 신뢰성 있게 파싱할 수 없습니다.
formatToParts() 메서드는 각 구성 요소를 분리해서 제공함으로써 이러한 문제를 해결합니다. 이 방식으로 로케일에 관계없이 어떤 부분이 무엇인지 명확하게 알려주는 구조화된 데이터를 받을 수 있습니다.
formatToParts 작동 방식
formatToParts() 메서드는 반환값만 제외하면 format()와 동일하게 동작합니다. 동일한 옵션으로 포매터를 만든 후, format() 대신 formatToParts()를 호출하면 됩니다.
이 메서드는 객체 배열을 반환합니다. 각각의 객체는 두 개의 속성을 포함합니다:
type: 해당 부분이 무엇을 나타내는지 식별 (예:currency,month,literal)value: 그 부분에 해당하는 실제 문자열
각 부분은 실제 포맷 결과에서 나오는 순서와 동일하게 배열되어 있습니다. 모든 값을 합쳐서 확인하면 format()를 호출했을 때와 완전히 같은 결과가 출력됩니다.
formatToParts()를 지원하는 모든 포매터에서 이 패턴은 동일하게 적용됩니다. 세부 파트 유형은 포매터에 따라 다를 수 있지만, 구조 자체는 항상 같습니다.
포맷된 숫자를 부분으로 나누기
NumberFormat 포매터는 포맷된 숫자를 분리하는 데 사용할 수 있는 formatToParts()를 제공합니다. 이 방법은 일반 숫자, 통화, 백분율 등 다양한 숫자 스타일에 적용할 수 있습니다.
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
});
const parts = formatter.formatToParts(1234.56);
console.log(parts);
이 코드는 객체 배열을 출력합니다:
[
{ type: "currency", value: "$" },
{ type: "integer", value: "1" },
{ type: "group", value: "," },
{ type: "integer", value: "234" },
{ type: "decimal", value: "." },
{ type: "fraction", value: "56" }
]
각 객체는 해당 부분이 무엇을 나타내는지와 값을 제공합니다. currency 타입은 통화 기호를 뜻합니다. integer 타입은 정수 부분의 숫자를 의미합니다. group 타입은 천 단위 구분자를 뜻합니다. decimal 타입은 소수점을, fraction 타입은 소수점 이하 숫자를 나타냅니다.
각 부분이 형식화된 출력과 일치하는지 확인할 수 있습니다:
const formatted = parts.map(part => part.value).join("");
console.log(formatted);
// Output: "$1,234.56"
이렇게 연결된 부분들은 format()을(를) 호출했을 때와 완전히 동일한 결과를 만듭니다.
형식화된 숫자에서 통화 기호 스타일링하기
formatToParts()의 주요 사용 사례는 서로 다른 구성 요소마다 스타일을 다르게 적용하는 것입니다. parts 배열을 활용해서 원하는 타입을 HTML 요소로 감쌀 수 있습니다.
통화 기호를 굵게 만들기:
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
});
const parts = formatter.formatToParts(1234.56);
const html = parts
.map(part => {
if (part.type === "currency") {
return `<strong>${part.value}</strong>`;
}
return part.value;
})
.join("");
console.log(html);
// Output: "<strong>$</strong>1,234.56"
이 방법은 모든 마크업 언어에서 동작합니다. parts 배열을 처리해 HTML, JSX 등 어떤 형식이든 생성할 수 있습니다.
소수점 부분을 다르게 스타일링하기:
const formatter = new Intl.NumberFormat("en-US", {
minimumFractionDigits: 2
});
const parts = formatter.formatToParts(1234.5);
const html = parts
.map(part => {
if (part.type === "decimal" || part.type === "fraction") {
return `<span class="text-gray-500">${part.value}</span>`;
}
return part.value;
})
.join("");
console.log(html);
// Output: "1,234<span class="text-gray-500">.50</span>"
이 패턴은 소수점 부분이 더 작거나 연하게 표시되는 가격 표시에서 흔히 사용됩니다.
형식화된 날짜를 부분으로 나누기
DateTimeFormat 포매터는 형식화된 날짜와 시간을 분해할 수 있도록 formatToParts()을(를) 제공합니다.
const formatter = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric"
});
const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);
console.log(parts);
이렇게 하면 객체 배열이 출력됩니다:
[
{ type: "month", value: "January" },
{ type: "literal", value: " " },
{ type: "day", value: "15" },
{ type: "literal", value: ", " },
{ type: "year", value: "2025" }
]
month 타입은 월 이름이나 숫자를, day 타입은 일을, year 타입은 연도를, literal 타입은 포매터가 추가한 공백, 구두점 또는 기타 텍스트를 나타냅니다.
형식화된 날짜에서 월 이름 스타일링하기
숫자에서와 동일한 방식으로 날짜 구성 요소에도 맞춤 스타일을 적용할 수 있습니다.
월 이름을 굵게 만들기:
const formatter = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric"
});
const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);
const html = parts
.map(part => {
if (part.type === "month") {
return `<strong>${part.value}</strong>`;
}
return part.value;
})
.join("");
console.log(html);
// Output: "<strong>January</strong> 15, 2025"
여러 날짜 구성 요소 스타일링하기:
const formatter = new Intl.DateTimeFormat("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric"
});
const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);
const html = parts
.map(part => {
switch (part.type) {
case "weekday":
return `<span class="font-bold">${part.value}</span>`;
case "month":
return `<span class="text-blue-600">${part.value}</span>`;
case "year":
return `<span class="text-gray-500">${part.value}</span>`;
default:
return part.value;
}
})
.join("");
console.log(html);
// Output: "<span class="font-bold">Wednesday</span>, <span class="text-blue-600">January</span> 15, <span class="text-gray-500">2025</span>"
이렇게 세밀하게 제어하면 각 구성 요소마다 원하는 스타일을 정확하게 적용할 수 있습니다.
형식화된 목록을 부분으로 나누기
ListFormat 포매터는 포맷된 목록을 분할할 수 있도록 formatToParts()를 제공합니다.
const formatter = new Intl.ListFormat("en-US", {
style: "long",
type: "conjunction"
});
const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);
console.log(parts);
이 코드는 객체 배열을 출력합니다:
[
{ type: "element", value: "apples" },
{ type: "literal", value: ", " },
{ type: "element", value: "oranges" },
{ type: "literal", value: ", and " },
{ type: "element", value: "bananas" }
]
element 타입은 목록의 각 항목을, literal 타입은 포매터가 추가한 구분자와 접속사를 나타냅니다.
목록 항목별로 스타일 지정하기
동일한 방식으로 목록 요소에 사용자 정의 스타일을 적용할 수 있습니다.
목록 항목을 굵게 만들기:
const formatter = new Intl.ListFormat("en-US", {
style: "long",
type: "conjunction"
});
const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);
const html = parts
.map(part => {
if (part.type === "element") {
return `<strong>${part.value}</strong>`;
}
return part.value;
})
.join("");
console.log(html);
// Output: "<strong>apples</strong>, <strong>oranges</strong>, and <strong>bananas</strong>"
특정 목록 항목에 스타일 적용하기:
const formatter = new Intl.ListFormat("en-US", {
style: "long",
type: "conjunction"
});
const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);
let itemIndex = 0;
const html = parts
.map(part => {
if (part.type === "element") {
const currentIndex = itemIndex++;
if (currentIndex === 0) {
return `<span class="text-green-600">${part.value}</span>`;
}
return part.value;
}
return part.value;
})
.join("");
console.log(html);
// Output: "<span class="text-green-600">apples</span>, oranges, and bananas"
이 방법을 사용하면 적절한 현지화된 포맷을 유지하면서 특정 항목을 강조할 수 있습니다.
포매팅된 상대 시간을 파트로 분할하기
RelativeTimeFormat 포매터는 상대 시간 표현을 분할할 수 있도록 formatToParts()를 제공합니다.
const formatter = new Intl.RelativeTimeFormat("en-US", {
numeric: "auto"
});
const parts = formatter.formatToParts(-1, "day");
console.log(parts);
이 코드는 객체 배열을 출력합니다:
[
{ type: "literal", value: "yesterday" }
]
숫자로 표현된 상대 시간의 경우:
const formatter = new Intl.RelativeTimeFormat("en-US", {
numeric: "always"
});
const parts = formatter.formatToParts(-3, "day");
console.log(parts);
// [
// { type: "integer", value: "3" },
// { type: "literal", value: " days ago" }
// ]
integer 타입은 숫자 값을, literal 타입은 시간 단위와 방향을 나타냅니다.
포매팅된 기간을 파트로 분할하기
DurationFormat 포매터는 포맷된 기간을 분할할 수 있도록 formatToParts()를 제공합니다.
const formatter = new Intl.DurationFormat("en-US", {
style: "long"
});
const parts = formatter.formatToParts({
hours: 2,
minutes: 30,
seconds: 15
});
console.log(parts);
이 코드는 다음과 유사한 객체 배열을 출력합니다:
[
{ type: "integer", value: "2" },
{ type: "literal", value: " hours, " },
{ type: "integer", value: "30" },
{ type: "literal", value: " minutes, " },
{ type: "integer", value: "15" },
{ type: "literal", value: " seconds" }
]
integer 타입은 숫자 값을 나타냅니다. literal 타입은 단위 이름과 구분자를 나타냅니다.
포맷된 요소로 HTML 만들기
부분 요소를 처리하고 일관된 스타일 규칙을 적용하는 재사용 가능한 함수를 만들 수 있습니다.
function formatWithStyles(parts, styleMap) {
return parts
.map(part => {
const style = styleMap[part.type];
if (style) {
return `<span class="${style}">${part.value}</span>`;
}
return part.value;
})
.join("");
}
const numberFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
});
const parts = numberFormatter.formatToParts(1234.56);
const html = formatWithStyles(parts, {
currency: "font-bold text-gray-700",
integer: "text-2xl",
fraction: "text-sm text-gray-500"
});
console.log(html);
// Output: "<span class="font-bold text-gray-700">$</span><span class="text-2xl">1</span>,<span class="text-2xl">234</span>.<span class="text-sm text-gray-500">56</span>"
이 패턴은 스타일 규칙을 포맷팅 로직으로부터 분리해 유지보수와 재사용을 더 쉽게 만들어줍니다.
로케일별 요소 순서 이해하기
parts 배열은 로케일별 포맷 규칙을 자동으로 유지합니다. 로케일마다 구성요소의 순서나 형식이 다르지만, formatToParts()가 이런 차이를 처리해줍니다.
const usdFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
});
console.log(usdFormatter.formatToParts(1234.56));
// [
// { type: "currency", value: "$" },
// { type: "integer", value: "1" },
// { type: "group", value: "," },
// { type: "integer", value: "234" },
// { type: "decimal", value: "." },
// { type: "fraction", value: "56" }
// ]
const eurFormatter = new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR"
});
console.log(eurFormatter.formatToParts(1234.56));
// [
// { type: "integer", value: "1" },
// { type: "group", value: "." },
// { type: "integer", value: "234" },
// { type: "decimal", value: "," },
// { type: "fraction", value: "56" },
// { type: "literal", value: " " },
// { type: "currency", value: "€" }
// ]
독일어 포맷에서는 통화 기호가 숫자 뒤에 공백과 함께 옵니다. 그룹 구분자는 마침표(.)이고, 소수점 구분자는 쉼표(,)입니다. 스타일링 코드는 로케일과 상관없이 parts 배열을 동일하게 처리하며, 포맷팅은 자동으로 적응됩니다.
접근성 있는 포맷 표시 만들기
formatToParts()를 사용해 포맷된 출력에 접근성 속성을 추가할 수 있습니다. 이를 통해 화면 읽기 프로그램이 값을 올바르게 안내할 수 있게 됩니다.
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
});
function formatAccessible(number) {
const parts = formatter.formatToParts(number);
const formatted = parts.map(part => part.value).join("");
return `<span aria-label="${number} US dollars">${formatted}</span>`;
}
console.log(formatAccessible(1234.56));
// Output: "<span aria-label="1234.56 US dollars">$1,234.56</span>"
이렇게 하면 화면 읽기 프로그램이 포맷된 표시 값과 실제 숫자 값을 모두 적절한 맥락으로 안내할 수 있습니다.
formatToParts와 프레임워크 컴포넌트 결합하기
React 같은 최신 프레임워크에서 formatToParts()를 사용해 컴포넌트를 효율적으로 만들 수 있습니다.
function CurrencyDisplay({ value, locale, currency }) {
const formatter = new Intl.NumberFormat(locale, {
style: "currency",
currency: currency
});
const parts = formatter.formatToParts(value);
return (
<span className="currency-display">
{parts.map((part, index) => {
if (part.type === "currency") {
return <strong key={index}>{part.value}</strong>;
}
if (part.type === "fraction" || part.type === "decimal") {
return <span key={index} className="text-sm text-gray-500">{part.value}</span>;
}
return <span key={index}>{part.value}</span>;
})}
</span>
);
}
이 컴포넌트는 각 파트마다 다른 스타일을 적용하면서도, 어떤 로케일과 통화에서도 올바른 포맷을 유지합니다.
formatToParts를 사용해야 하는 경우
커스터마이즈 없이 그냥 간단한 포맷 문자열이 필요할 때는 format()를 사용하세요. 대부분의 표시 상황에서는 이것이 가장 일반적입니다.
다음과 같은 경우에는 formatToParts()를 사용하세요:
- 포맷된 출력의 각 부분마다 서로 다른 스타일을 적용해야 할 때
- 포맷된 내용을 이용해 HTML 또는 JSX를 생성할 때
- 특정 요소에 속성이나 메타데이터를 추가해야 할 때
- 포맷된 출력을 복잡한 레이아웃에 통합할 때
- 포맷된 출력을 프로그램적으로 처리해야 할 때
- 세밀한 제어가 필요한 커스텀 시각 디자인을 만들 때
formatToParts() 메서드는 객체 배열을 생성하기 때문에 format()보다 약간 더 많은 오버헤드가 발생합니다. 반면 뒤쪽은 하나의 문자열만 생성합니다. 일반적인 애플리케이션에서 이 차이는 거의 무시할 수 있지만, 초당 수천 개의 값을 포맷해야 한다면 format() 쪽이 성능이 더 좋습니다.
대부분의 애플리케이션에서는 성능 걱정보다 스타일링 요구에 맞춰서 선택하면 됩니다. 출력 결과를 커스터마이즈할 필요가 없다면 format()를 사용하세요. 만약 커스텀 스타일이나 마크업이 필요하다면 formatToParts()를 사용하세요.
포매터에서 공통적으로 사용되는 파트 타입
포매터 종류에 따라 생성하는 파트 타입이 다르지만, 몇몇 타입은 여러 포매터에서 공통적으로 나타납니다:
literal: 포매팅 과정에서 추가된 공백, 구두점, 기타 텍스트. 날짜, 숫자, 목록, 기간 등에 나타납니다.integer: 정수 숫자. 숫자, 상대 시간, 기간 등에 나타납니다.decimal: 소수 구분자. 숫자에 나타납니다.fraction: 소수 자릿수. 숫자에 나타납니다.
포매터별로 고유한 타입은 다음과 같습니다:
- 숫자:
currency,group,percentSign,minusSign,plusSign,unit,compact,exponentInteger - 날짜:
weekday,era,year,month,day,hour,minute,second,dayPeriod,timeZoneName - 목록:
element - 상대 시간: 숫자 값은
integer로, 텍스트는literal로 나타납니다.
이러한 타입을 이해하면 어떤 포매터의 출력에도 대응할 수 있는 스타일링 코드를 작성하는 데 도움이 됩니다.