JavaScript에서 숫자를 0.05 또는 다른 단위로 반올림하는 방법

소액 동전이 없는 국가에서 0.05 또는 0.10과 같은 특정 단위로 통화 및 숫자를 반올림하는 방법 알아보기

소개

캐나다의 고객이 장바구니에 상품을 추가하여 총 $11.23이 되었지만, 최종 청구 금액은 $11.25로 표시됩니다. 이는 캐나다가 2013년에 1센트 동전을 폐지하여 현금 거래가 가장 가까운 0.05 단위로 반올림되기 때문입니다. 애플리케이션은 고객이 실제로 지불하는 금액과 일치하는 가격을 표시해야 합니다.

특정 단위로 반올림하면 이 문제를 해결할 수 있습니다. 숫자를 가장 가까운 5센트(0.05), 10센트(0.10) 또는 기타 단위로 반올림할 수 있습니다. 이는 통화 형식 지정, 측정 시스템 및 값이 특정 단위에 맞춰야 하는 모든 시나리오에 적용됩니다.

이 가이드에서는 수동 구현과 최신 Intl.NumberFormat API를 모두 포함하여 JavaScript를 사용해 사용자 정의 단위로 숫자를 반올림하는 방법을 보여줍니다.

특정 단위로 반올림하는 이유

많은 국가에서 실용적인 이유로 소액 동전을 폐지했습니다. 이러한 동전을 생산하고 처리하는 비용이 액면가보다 더 많이 듭니다. 이러한 국가의 현금 거래는 사용 가능한 가장 가까운 동전 단위로 반올림됩니다.

현금 결제 시 0.05 단위 반올림을 사용하는 국가:

  • 캐나다(2013년 1센트 동전 폐지)
  • 호주(1센트 및 2센트 동전 폐지)
  • 네덜란드(가장 가까운 0.05유로로 반올림)
  • 벨기에(가장 가까운 0.05유로로 반올림)
  • 아일랜드(가장 가까운 0.05유로로 반올림)
  • 이탈리아(가장 가까운 0.05유로로 반올림)
  • 핀란드(가장 가까운 0.05유로로 반올림)
  • 스위스(최소 동전 단위 0.05 CHF)

0.10 단위 반올림을 사용하는 국가:

  • 뉴질랜드(2006년 5센트 동전 폐지)

미국은 2026년 초까지 페니 생산을 중단할 계획이며, 이로 인해 현금 거래에 0.05 단위 반올림이 필요하게 됩니다.

현금 반올림은 개별 항목이 아닌 최종 거래 합계에만 적용됩니다. 전자 결제는 실물 동전이 오가지 않기 때문에 정확한 금액을 유지합니다.

통화 외에도 다음과 같은 경우 사용자 정의 증분이 필요할 수 있습니다:

  • 특정 정밀도 요구사항이 있는 측정 시스템
  • 기기 정밀도로 반올림된 과학 계산
  • 특정 값으로 스냅되는 사용자 인터페이스 컨트롤
  • 심리적 가격대를 사용하는 가격 전략

증분 반올림의 수학적 개념

증분으로 반올림한다는 것은 해당 증분의 가장 가까운 배수를 찾는 것을 의미합니다. 공식은 숫자를 증분으로 나누고, 가장 가까운 정수로 반올림한 다음, 증분을 다시 곱합니다.

roundedValue = Math.round(value / increment) * increment

예를 들어, 11.23을 가장 가까운 0.05로 반올림하면:

  1. 나누기: 11.23 / 0.05 = 224.6
  2. 반올림: Math.round(224.6) = 225
  3. 곱하기: 225 * 0.05 = 11.25

나누기는 증분을 정수 단계로 변환합니다. 224.6을 225로 반올림하면 가장 가까운 단계를 찾습니다. 곱하기는 원래 스케일로 다시 변환합니다.

11.27을 가장 가까운 0.05로 반올림하면:

  1. 나누기: 11.27 / 0.05 = 225.4
  2. 반올림: Math.round(225.4) = 225
  3. 곱하기: 225 * 0.05 = 11.25

11.23과 11.27은 모두 해당 값의 0.025 범위 내에 있기 때문에 11.25로 반올림됩니다. 중간값 11.25는 11.25로 유지됩니다.

증분 반올림 수동 구현

지정된 증분으로 숫자를 반올림하는 함수를 만드세요:

function roundToIncrement(value, increment) {
  return Math.round(value / increment) * increment;
}

console.log(roundToIncrement(11.23, 0.05)); // 11.25
console.log(roundToIncrement(11.27, 0.05)); // 11.25
console.log(roundToIncrement(11.28, 0.05)); // 11.30
console.log(roundToIncrement(4.94, 0.10)); // 4.90
console.log(roundToIncrement(4.96, 0.10)); // 5.00

이는 통화뿐만 아니라 모든 증분에 작동합니다:

console.log(roundToIncrement(17, 5)); // 15
console.log(roundToIncrement(23, 5)); // 25
console.log(roundToIncrement(2.7, 0.5)); // 2.5

JavaScript 부동 소수점 연산은 작은 정밀도 오류를 발생시킬 수 있습니다. 정밀도가 중요한 통화 계산의 경우 값에 10의 거듭제곱을 곱하여 정수로 작업하세요:

function roundToIncrementPrecise(value, increment) {
  // Find the number of decimal places in the increment
  const decimals = (increment.toString().split('.')[1] || '').length;
  const multiplier = Math.pow(10, decimals);

  // Convert to integers
  const valueInt = Math.round(value * multiplier);
  const incrementInt = Math.round(increment * multiplier);

  // Round and convert back
  return Math.round(valueInt / incrementInt) * incrementInt / multiplier;
}

console.log(roundToIncrementPrecise(11.23, 0.05)); // 11.25

Math.round()는 반올림(half-up rounding)을 사용하며, 두 증분의 정확히 중간 값은 위로 올립니다. 일부 애플리케이션에서는 다른 반올림 모드가 필요합니다:

function roundToIncrementWithMode(value, increment, mode = 'halfUp') {
  const divided = value / increment;

  let rounded;
  switch (mode) {
    case 'up':
      rounded = Math.ceil(divided);
      break;
    case 'down':
      rounded = Math.floor(divided);
      break;
    case 'halfDown':
      rounded = divided % 1 === 0.5 ? Math.floor(divided) : Math.round(divided);
      break;
    case 'halfUp':
    default:
      rounded = Math.round(divided);
      break;
  }

  return rounded * increment;
}

console.log(roundToIncrementWithMode(11.225, 0.05, 'halfUp')); // 11.25
console.log(roundToIncrementWithMode(11.225, 0.05, 'halfDown')); // 11.20
console.log(roundToIncrementWithMode(11.23, 0.05, 'up')); // 11.25
console.log(roundToIncrementWithMode(11.23, 0.05, 'down')); // 11.20

Intl.NumberFormat에서 roundingIncrement 사용하기

최신 브라우저는 Intl.NumberFormat에서 roundingIncrement 옵션을 지원하며, 숫자를 포맷할 때 증분 반올림을 자동으로 처리합니다.

캐나다 달러 니켈 반올림의 기본 사용법:

const formatter = new Intl.NumberFormat('en-CA', {
  style: 'currency',
  currency: 'CAD',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  roundingIncrement: 5
});

console.log(formatter.format(11.23)); // CA$11.25
console.log(formatter.format(11.27)); // CA$11.25
console.log(formatter.format(11.28)); // CA$11.30

roundingIncrement 옵션은 특정 값만 허용합니다:

  • 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000

0.05 반올림의 경우 maximumFractionDigits: 2roundingIncrement: 5를 설정합니다. 0.10 반올림의 경우 maximumFractionDigits: 1roundingIncrement: 1를 설정하거나, maximumFractionDigits: 2를 유지하고 roundingIncrement: 10를 사용합니다.

스위스 프랑 0.05 반올림:

const chfFormatter = new Intl.NumberFormat('de-CH', {
  style: 'currency',
  currency: 'CHF',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  roundingIncrement: 5
});

console.log(chfFormatter.format(8.47)); // CHF 8.45
console.log(chfFormatter.format(8.48)); // CHF 8.50

뉴질랜드 달러 0.10 반올림:

const nzdFormatter = new Intl.NumberFormat('en-NZ', {
  style: 'currency',
  currency: 'NZD',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  roundingIncrement: 10
});

console.log(nzdFormatter.format(15.94)); // $15.90
console.log(nzdFormatter.format(15.96)); // $16.00

roundingIncrement를 다양한 반올림 모드와 결합할 수 있습니다:

const formatterUp = new Intl.NumberFormat('en-CA', {
  style: 'currency',
  currency: 'CAD',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  roundingIncrement: 5,
  roundingMode: 'ceil'
});

console.log(formatterUp.format(11.21)); // CA$11.25 (rounded up)

사용 가능한 반올림 모드:

  • ceil: 양의 무한대 방향으로 반올림
  • floor: 음의 무한대 방향으로 반올림
  • expand: 0에서 멀어지는 방향으로 반올림
  • trunc: 0 방향으로 반올림
  • halfCeil: 절반은 양의 무한대 방향으로 반올림
  • halfFloor: 절반은 음의 무한대 방향으로 반올림
  • halfExpand: 절반은 0에서 멀어지는 방향으로 반올림 (기본값)
  • halfTrunc: 절반은 0 방향으로 반올림
  • halfEven: 절반은 짝수로 반올림 (은행가 반올림)

roundingIncrement의 중요한 제약 사항:

  • minimumFractionDigitsmaximumFractionDigits는 동일한 값을 가져야 합니다
  • 유효 숫자 반올림과 결합할 수 없습니다
  • roundingPriorityauto (기본값)일 때만 작동합니다

브라우저가 roundingIncrement를 지원하는지 확인:

function supportsRoundingIncrement() {
  try {
    const formatter = new Intl.NumberFormat('en-US', {
      roundingIncrement: 5,
      minimumFractionDigits: 2,
      maximumFractionDigits: 2
    });
    const options = formatter.resolvedOptions();
    return options.roundingIncrement === 5;
  } catch (e) {
    return false;
  }
}

if (supportsRoundingIncrement()) {
  // Use Intl.NumberFormat with roundingIncrement
} else {
  // Fall back to manual rounding
}

실용적인 구현 예제

로케일에 따라 현금 반올림을 처리하는 통화 포맷터 생성:

function createCashFormatter(locale, currency) {
  // Define cash rounding rules by currency
  const cashRoundingRules = {
    'CAD': { increment: 5, digits: 2 },
    'AUD': { increment: 5, digits: 2 },
    'EUR': { increment: 5, digits: 2 }, // Some eurozone countries
    'CHF': { increment: 5, digits: 2 },
    'NZD': { increment: 10, digits: 2 }
  };

  const rule = cashRoundingRules[currency];

  if (!rule) {
    // No special rounding needed
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency: currency
    });
  }

  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency: currency,
    minimumFractionDigits: rule.digits,
    maximumFractionDigits: rule.digits,
    roundingIncrement: rule.increment
  });
}

const cadFormatter = createCashFormatter('en-CA', 'CAD');
console.log(cadFormatter.format(47.83)); // CA$47.85

const usdFormatter = createCashFormatter('en-US', 'USD');
console.log(usdFormatter.format(47.83)); // $47.83 (no rounding)

현금 및 카드 결제 모두 처리:

function formatPaymentAmount(amount, currency, locale, paymentMethod) {
  if (paymentMethod === 'cash') {
    const formatter = createCashFormatter(locale, currency);
    return formatter.format(amount);
  } else {
    // Electronic payments use exact amounts
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency: currency
    }).format(amount);
  }
}

console.log(formatPaymentAmount(11.23, 'CAD', 'en-CA', 'cash')); // CA$11.25
console.log(formatPaymentAmount(11.23, 'CAD', 'en-CA', 'card')); // CA$11.23

청구할 실제 금액 계산:

function calculateCashTotal(items, currency) {
  const subtotal = items.reduce((sum, item) => sum + item.price, 0);

  // Apply cash rounding only to the final total
  const cashRoundingRules = {
    'CAD': 0.05,
    'AUD': 0.05,
    'CHF': 0.05,
    'NZD': 0.10
  };

  const increment = cashRoundingRules[currency];

  if (!increment) {
    return subtotal;
  }

  return roundToIncrement(subtotal, increment);
}

function roundToIncrement(value, increment) {
  return Math.round(value / increment) * increment;
}

const items = [
  { price: 5.99 },
  { price: 3.49 },
  { price: 1.75 }
];

console.log(calculateCashTotal(items, 'CAD')); // 11.25
console.log(calculateCashTotal(items, 'USD')); // 11.23

비통화 값에 대해 증분 반올림 사용:

// Temperature display rounded to nearest 0.5 degrees
function formatTemperature(celsius) {
  const rounded = roundToIncrement(celsius, 0.5);
  return new Intl.NumberFormat('en-US', {
    style: 'unit',
    unit: 'celsius',
    minimumFractionDigits: 1,
    maximumFractionDigits: 1
  }).format(rounded);
}

console.log(formatTemperature(22.3)); // 22.5°C
console.log(formatTemperature(22.6)); // 22.5°C

// Slider values snapped to increments
function snapToGrid(value, gridSize) {
  return roundToIncrement(value, gridSize);
}

console.log(snapToGrid(47, 5)); // 45
console.log(snapToGrid(53, 5)); // 55

중요 고려사항 및 엣지 케이스

현금 반올림은 개별 항목이 아닌 최종 거래 총액에만 적용하세요. 각 항목을 개별적으로 반올림하면 합계를 반올림하는 것과 다른 결과가 나옵니다:

const items = [1.23, 1.23, 1.23];

// Wrong: rounding each item
const wrongTotal = items
  .map(price => roundToIncrement(price, 0.05))
  .reduce((sum, price) => sum + price, 0);
console.log(wrongTotal); // 3.75

// Correct: rounding the total
const correctTotal = roundToIncrement(
  items.reduce((sum, price) => sum + price, 0),
  0.05
);
console.log(correctTotal); // 3.70

사용자에게는 반올림된 금액을 표시하되, 데이터베이스에는 정확한 금액을 저장하세요. 이렇게 하면 회계를 위한 정밀도가 유지되고 정보 손실 없이 반올림 규칙을 변경할 수 있습니다:

const transaction = {
  items: [
    { id: 1, price: 5.99 },
    { id: 2, price: 3.49 },
    { id: 3, price: 1.75 }
  ],
  subtotal: 11.23, // Store exact value
  currency: 'CAD',
  paymentMethod: 'cash'
};

// Calculate display amount
const displayAmount = roundToIncrement(transaction.subtotal, 0.05); // 11.25

동일한 통화를 사용하는 국가라도 현금 반올림 규칙이 다를 수 있습니다. 유로는 현금 반올림이 있는 국가와 없는 국가 모두에서 사용됩니다. 통화가 아닌 특정 국가의 규정을 확인하세요.

음수 금액은 절댓값에 대해 0 방향으로 반올림한 다음 음수 부호를 적용합니다:

console.log(roundToIncrement(-11.23, 0.05)); // -11.25
console.log(roundToIncrement(-11.22, 0.05)); // -11.20

매우 큰 숫자는 부동 소수점 연산으로 정밀도를 잃을 수 있습니다. 큰 금액을 처리하는 금융 애플리케이션의 경우 최소 통화 단위로 정수 연산을 사용하는 것을 고려하세요:

function roundToIncrementCents(cents, incrementCents) {
  return Math.round(cents / incrementCents) * incrementCents;
}

// Work in cents
const amountCents = 1123; // $11.23
const roundedCents = roundToIncrementCents(amountCents, 5); // 1125
const dollars = roundedCents / 100; // 11.25

엣지 케이스로 반올림 구현을 테스트하세요:

// Test cases for 0.05 rounding
const testCases = [
  { input: 0.00, expected: 0.00 },
  { input: 0.01, expected: 0.00 },
  { input: 0.02, expected: 0.00 },
  { input: 0.025, expected: 0.05 },
  { input: 0.03, expected: 0.05 },
  { input: 0.99, expected: 1.00 },
  { input: -0.03, expected: -0.05 },
  { input: 11.225, expected: 11.25 }
];

testCases.forEach(({ input, expected }) => {
  const result = roundToIncrement(input, 0.05);
  console.log(`${input} -> ${result} (expected ${expected})`);
});

Intl.NumberFormatroundingIncrement 옵션은 최신 브라우저에서 지원됩니다. Firefox는 버전 93에서 지원을 추가했습니다. 이전 브라우저를 지원하는 애플리케이션의 경우 폴백을 구현하세요:

function formatCurrency(amount, locale, currency, increment) {
  try {
    const formatter = new Intl.NumberFormat(locale, {
      style: 'currency',
      currency: currency,
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
      roundingIncrement: increment
    });

    // Check if roundingIncrement was actually applied
    if (formatter.resolvedOptions().roundingIncrement === increment) {
      return formatter.format(amount);
    }
  } catch (e) {
    // roundingIncrement not supported
  }

  // Fallback: manual rounding
  const rounded = roundToIncrement(amount, increment / 100);
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency: currency
  }).format(rounded);
}

사용자 대면 인터페이스에서 반올림 동작을 명확하게 문서화하세요. 사용자는 표시된 금액이 항목 합계와 다른 이유를 이해해야 합니다:

<div class="cart-summary">
  <div class="line-item">Subtotal: $11.23</div>
  <div class="line-item total">Total (cash): $11.25</div>
  <div class="note">Cash payments rounded to nearest $0.05</div>
</div>

관할 지역마다 반올림으로 인한 이익의 귀속 주체(고객 또는 판매자) 및 엣지 케이스 처리 방법에 대한 규정이 다릅니다. 특정 사용 사례에 대해서는 현지 규정을 참조하세요.