암호화폐 거래소에서 마주친 숫자 표시 문제
암호화폐 거래소 프로젝트를 진행하다 보면 누구나 한 번쯤 마주치는 문제가 있습니다. 사용자가 화면에서 보는 "1,234.56789"라는 숫자가 단순해 보이지만, 이를 정확하게 표시하는 것은 생각보다 복잡합니다. JavaScript의 기본 toFixed() 메서드로는 천 단위 구분자를 자동으로 추가할 수 없고, 부동소수점 연산의 한계로 인해 정밀도 문제가 발생하기 때문입니다.
왜 일반 JavaScript의 toFixed()로는 부족한가?
JavaScript의 부동소수점 연산은 악명 높은 정밀도 문제를 가지고 있습니다. 가장 유명한 예시가 바로 이것입니다:
console.log(0.1 + 0.2); // 0.30000000000000004
console.log((0.1 + 0.2).toFixed(1)); // "0.3"
문제는 여기서 끝이 아닙니다. toFixed()는 천 단위 구분자를 지원하지 않아서, 큰 금액을 표시할 때 가독성이 떨어집니다:
const amount = 1234567.89;
console.log(amount.toFixed(2)); // "1234567.89"
// 우리가 원하는 것: "1,234,567.89"
이런 이유로 암호화폐 거래소에서는 BigNumber.js 같은 라이브러리를 사용해 정밀한 숫자 처리와 포맷팅을 구현합니다.
BigNumber.js 핵심 설정값 해부
실전에서 사용하는 BigNumber 설정을 하나씩 뜯어보겠습니다:
BigNumber.config({
EXPONENTIAL_AT: 39,
DECIMAL_PLACES: 20,
ROUNDING_MODE: BigNumber.ROUND_DOWN,
POW_PRECISION: 21, // 20 까지하면 소수점 아래 19까지만 표현됩니다.
});
EXPONENTIAL_AT: 39
이 설정은 숫자가 지수 표기법(e.g., 1.23e+10)으로 표시되는 임계값을 정의합니다. 기본값은 [-7, 20]인데, 이를 39로 설정하면 10^39 미만의 매우 큰 숫자도 일반 표기법으로 표시됩니다.
// EXPONENTIAL_AT: 2일 때
new BigNumber(123).toString(); // "1.23e+2"
// EXPONENTIAL_AT: 39일 때
new BigNumber(123).toString(); // "123"
암호화폐 시장에서는 비트코인처럼 가격이 수천만 원대인 코인부터, 사토시(0.00000001 BTC) 같은 극소 단위까지 다뤄야 하므로 충분히 큰 임계값이 필요합니다.
DECIMAL_PLACES: 20
나눗셈, 제곱근, 거듭제곱 같은 연산 결과의 최대 소수점 자릿수를 제한합니다. 암호화폐 중에는 소수점 이하 18자리까지 지원하는 경우(예: Ethereum의 wei 단위)가 있어서, 20자리면 대부분의 케이스를 커버할 수 있습니다.
BigNumber.config({ DECIMAL_PLACES: 20 });
const result = new BigNumber(1).div(3);
console.log(result.toString()); // "0.33333333333333333333"
ROUNDING_MODE: ROUND_DOWN
반올림 방식을 내림으로 설정합니다. 거래소에서 사용자에게 불리한 방향으로 반올림하면 민원이 발생할 수 있기 때문에, 보통 내림(ROUND_DOWN)을 사용합니다.
const value = new BigNumber(1.23456);
console.log(value.toFixed(2, BigNumber.ROUND_DOWN)); // "1.23"
console.log(value.toFixed(2, BigNumber.ROUND_UP)); // "1.24"
POW_PRECISION: 21
거듭제곱 연산의 정밀도를 결정합니다. 주석에 나와 있듯이 20으로 설정하면 소수점 이하 19자리까지만 정확하게 표현되므로, 21로 설정해서 20자리를 보장합니다.
BigNumber.config({ POW_PRECISION: 21 });
const result = new BigNumber(2).pow(0.5); // √2
console.log(result.toString()); // 20자리 정밀도 유지
toFormat()으로 천 단위 구분 쉽게 하기
toFixed()와 toFormat()의 가장 큰 차이점은 천 단위 구분자와 통화 기호를 자동으로 추가해준다는 점입니다. 다음 설정 객체를 보겠습니다:
const fmt = {
prefix: "",
decimalSeparator: ".",
groupSeparator: ",",
groupSize: 3,
secondaryGroupSize: 0,
fractionGroupSeparator: " ",
fractionGroupSize: 0,
suffix: "",
};
각 옵션 상세 설명
groupSeparator: ","
천 단위 구분자를 지정합니다. 한국과 미국은 쉼표(,)를 사용하지만, 유럽 일부 국가는 공백이나 점(.)을 사용하기도 합니다.
groupSize: 3
그룹의 크기를 정의합니다. 3으로 설정하면 1,234,567처럼 세 자리마다 구분자가 삽입됩니다.
decimalSeparator: "."
소수점 구분자입니다. 영미권은 점(.)을, 유럽 일부 국가는 쉼표(,)를 사용합니다.
prefix/suffix
통화 기호를 추가할 수 있습니다. 예를 들어 prefix: "$"로 설정하면 "$1,234.56" 형태로 표시됩니다.
실제 사용 예시
const value = new BigNumber(1234567.89);
// toFixed는 천 단위 구분자 없음
console.log(value.toFixed(2)); // "1234567.89"
// toFormat은 자동으로 천 단위 구분 추가
console.log(value.toFormat(2, fmt)); // "1,234,567.89"
// 통화 기호 추가
const usdFmt = { ...fmt, prefix: "$" };
console.log(value.toFormat(2, usdFmt)); // "$1,234,567.89"
zeroPadding 파라미터의 실전 활용
암호화폐 UI에서 가장 신경 쓰이는 부분 중 하나가 바로 불필요한 trailing zero입니다. "123.45000"보다는 "123.45"가 훨씬 깔끔해 보입니다. 이를 처리하기 위한 정규표현식을 분석해보겠습니다:
return zeroPadding ? ret.replace(/(\.[0-9]*[1-9])0+$|\.0*$/, "$1") : ret;
정규표현식 완전 분석
이 정규표현식은 두 가지 패턴을 OR 연산자(|)로 연결합니다:
패턴 1: (\.[0-9]*[1-9])0+$
의미 있는 숫자 뒤의 0들을 제거합니다.
\.- 소수점으로 시작[0-9]*- 0개 이상의 숫자[1-9]- 0이 아닌 마지막 의미 있는 숫자 (캡처 그룹에 포함)0+$- 문자열 끝의 1개 이상의 0 (제거 대상)$1- 첫 번째 캡처 그룹(의미 있는 부분)만 유지
패턴 2: \.0*$
소수점 이하가 모두 0인 경우 전체를 제거합니다.
\.- 소수점0*- 0개 이상의 0$- 문자열 끝
Before/After 비교
const examples = [
"123.45000", // → "123.45" (패턴 1 적용)
"100.00", // → "100" (패턴 2 적용)
"1.2300", // → "1.23" (패턴 1 적용)
"0.10", // → "0.1" (패턴 1 적용)
"50.00000", // → "50" (패턴 2 적용)
];
처음에는 단순히 replace(/0+$/, "")로 구현했는데, "100"이 "1"로 변하는 버그가 발생했습니다. 그래서 소수점이 있는 경우만 처리하도록 패턴을 수정했더니 문제가 해결되더군요.
특수 케이스 처리: 0일 때는 소수점 무시
잔액이 0일 때 "0.00 BTC"보다는 "0 BTC"로 표시하는 것이 더 깔끔합니다. 이를 위한 로직입니다:
if (value.isEqualTo(0)) {
decimalPlace = undefined;
}
UI 일관성과 가독성 트레이드오프
이 부분에서 고민이 있었습니다. 모든 금액을 동일한 소수점 자릿수로 맞추면 표가 깔끔해 보이지만, 0인 경우에는 "0.00000000"처럼 불필요하게 길어집니다.
// Before: 일관성은 있지만 지저분함
보유 잔액: 0.00000000 BTC
// After: 0은 간단하게 표시
보유 잔액: 0 BTC
실제로 적용해보니 사용자들이 훨씬 만족스러워했습니다. 특히 모바일 화면에서는 공간이 제한적이기 때문에 이런 최적화가 중요합니다.
완성된 함수: toFixedZeroPadding
지금까지 설명한 모든 로직을 통합한 최종 함수입니다:
const toFixedZeroPadding = (
value: BigNumber | string | number,
decimalPlace: number | undefined = undefined,
zeroPadding: boolean = false,
): string => {
if (!value) {
return "0";
}
value = new BigNumber(value);
if (value.isEqualTo(0)) {
decimalPlace = undefined;
}
const ret =
decimalPlace !== undefined
? value.toFormat(decimalPlace, fmt)
: value.toFormat(fmt);
return zeroPadding ? ret.replace(/(\.[0-9]*[1-9])0+$|\.0*$/, "$1") : ret;
};
함수 파라미터 설명
value: BigNumber | string | number
포맷팅할 숫자를 받습니다. 타입에 관계없이 BigNumber로 변환되므로 유연하게 사용할 수 있습니다.
decimalPlace: number | undefined
소수점 자릿수를 지정합니다. undefined면 자동으로 필요한 만큼만 표시합니다.
zeroPadding: booleantrue로 설정하면 trailing zero를 제거합니다. false면 지정된 자릿수만큼 0을 채웁니다.
실전 사용 예시
// 케이스 1: 일반적인 금액 표시
console.log(toFixedZeroPadding(1234567.89, 2, false));
// → "1,234,567.89"
// 케이스 2: trailing zero 제거
console.log(toFixedZeroPadding(1234567.89000, 5, true));
// → "1,234,567.89"
// 케이스 3: 0은 소수점 없이
console.log(toFixedZeroPadding(0, 2, false));
// → "0"
// 케이스 4: 극소 단위 표시 (사토시)
console.log(toFixedZeroPadding(0.00001234, 8, true));
// → "0.00001234"
// 케이스 5: 자동 소수점
console.log(toFixedZeroPadding(123.456789, undefined, true));
// → "123.456789"
마치며
암호화폐 거래소 UI를 개발하면서 가장 많이 고민했던 숫자 포맷팅 문제를 정리해봤습니다. BigNumber.js의 toFormat()과 커스텀 정규표현식을 조합하면 대부분의 요구사항을 깔끔하게 처리할 수 있습니다.
이 글에서 다룬 함수들은 실제 프로덕션 환경에서 수개월간 검증된 코드입니다. 여러분의 프로젝트에 바로 복사해서 사용하셔도 좋습니다. 다만 프로젝트의 디자인 가이드와 사용자 요구사항에 맞게 fmt 객체나 zeroPadding 로직을 조정하는 것을 잊지 마세요.
혹시 다른 엣지 케이스를 발견하시거나 더 나은 해결 방법이 있다면 댓글로 공유해주시면 감사하겠습니다!
'Front-end > UI UX' 카테고리의 다른 글
| DeFi 서비스에서 작은 숫자 축약 표시하기 (0) | 2026.01.27 |
|---|