서비스에 꽤 심각한 UI 버그가 있었다. 상품 상세페이지에서 가격이 간헐적으로 실제 가격과 다르게 보이는 이슈였다. $13.61짜리 상품이 $1361로 보이는 식이었다. 가격이 이상하다는 CS가 지속적으로 인입되는데, 전혀 재현이 되지 않았다. 다행히 장바구니에 넣거나 주문 페이지로 가면 정상적인 가격으로 보이고 결제되었는데, 대체 왜 서버에서 내려준 동일한 가격이 유저마다 다르게 보여지는 건지 알 수 없었다.
그래서 혹시 프론트엔드에서 서버가 내려주는 가격을 가공하는 부분이 있나 살펴봤다. 일반적으로 프론트엔드에서는 가격이나 수량 같은 숫자를 보여줄 때 적당한 곳에 쉼표를 찍어 가독성을 높여주기 위해 javascript의 toLocaleString()
메서드를 사용한다. 서버에서 가격으로 13000
을 내려주면 13,000
이라는 문자열로 바꿔주는 식이다. 단순하게 숫자를 읽기 쉽게 바꿔줌이라는 식으로 생각하고 관습적으로 사용했던 메서드가 의심스럽기 시작했다. $13.61이 $1361이 되는 기적… toLocaleString
과 연관이 있을까?
🔎 toLocaleString의 반환값 살펴보기
toLocaleString
이라는 메서드명에서도 알 수 있듯 이 메서드는 주어진 매개변수를 유저의 locale에 적합한 문자열로 바꿔주는 함수다. 매개변수가 동일해도 locale이 달라진다면 반환값이 달라지리라는 것을 유추해볼 수 있다. 지금까지는 유저의 locale이 default locale로 들어가게 하기 위해서 매개변수에 아무것도 넣지 않았는데(toLocaleString
메서드에 아무 locale도 넣지 않으면 유저 OS의 default locale(Intl.DateTimeFormat().resolvedOptions().locale
)이 감지된다), 테스트로 매개변수에 여러 locale값을 삽입하며 아래와 같은 충격적인 결과를 얻게 된다. (MDN)
const number = 123456.789;
// 영국을 제외한 유럽에서는 천단위 절삭을 위해 쉼표를 사용한다
console.log(number.toLocaleString("de-DE"));
// 123.456,789
// 아랍어를 사용하는 대부분의 아랍 국가에서는 동부 아라비아 숫자를 사용한다
console.log(number.toLocaleString("ar-EG"));
// ١٢٣٤٥٦٫٧٨٩
// 인도에서는 천/십만/천만 단위 절삭을 위해 쉼표를 사용한다
console.log(number.toLocaleString("en-IN"));
// 1,23,456.789
// nu 확장키로 중국 십진법과 같은 수체계를 반환할 수 있다
console.log(number.toLocaleString("zh-Hans-CN-u-nu-hanidec"));
// 一二三,四五六.七八九
// 발리어 같이 지원하지 않을 수 있는 언어를 요청할 때는 fallback 언어를 포함할 수 있다
console.log(number.toLocaleString(["ban", "id"]));
// 123.456,789
toLocaleString()
의 반환값이 위와 같이 다양한데, 서비스에서는 특정 상점에서 소수점 아래 단위를 두 자리까지 보여주기 위해 다시 해당 문자열을 숫자로 변환하는 포맷팅을 실행하고 있던 것이다. toLocaleString
을 통해 변환된 문자열에서 단지 쉼표만 제거하면 다시 숫자로 만들 수 있을 것이라 가정하고 만든 로직이었기 때문에 당연히 다양한 반환값을 커버하지 못했고 버그가 생기게 됐다.
const price = 16.02;
const priceString = price.toLocaleString(); // '16.02' | '16,02' | etc..
// 🚨 string을 다시 number로 변환하는 과정에서 자릿수가 변경되어 가격이 잘못 표시됨
const priceStringToNumber = Number(priceString.replaceAll(',', '')); // '16.02' | '1602' | etc..
해당 버그는 크롬 브라우저 개발자도구의 Sensor 탭을 통해 Location을 Berlin으로 변경해주었더니 재현됐다. 제목처럼, 베를린에서는 10000이 우리에게 익숙한 ‘10,000’
이 아닌 ‘10.000’
으로 변환된다. 만 원이 십 원이 됐다고 당황하지 말고 지역에 따라 다르게 읽자. 영국을 제외한 유럽을 포함해 꽤 광범위하게 사용되는 표기법이라, 가격을 포함한 숫자를 많이 다루어야 하는 글로벌 커머스에서는 꼭 인지해야 하는 부분임을 알게 된 놀랍고 즐거운 디버깅 경험이었다.