재우니의 블로그

 

아래는 자바스크립트에서 부동소수점 연산으로 인해 발생하는 미세 오차 문제와, 이를 해결하기 위한 여러 방법을 이론과 함께 설명한 예시입니다.


자바스크립트 부동소수점 연산 오차 문제와 해결 방법

자바스크립트는 IEEE 754 방식의 부동소수점 숫자 표현을 사용합니다. 이로 인해 0.1 + 0.2와 같이 간단해 보이는 연산에서도 미세한 오차가 발생할 수 있습니다. 예를 들어, 여러 소수점 값을 누적하여 계산하면 100.00000000000006과 같이 100과 정확히 일치하지 않는 결과가 나타날 수 있습니다. 이번 글에서는 이러한 부동소수점 오차 문제의 원인과 함께, 이를 해결하기 위한 다양한 자바스크립트 코드 예제 및 외부 라이브러리를 활용한 해결 방법을 소개합니다.


1. 부동소수점 연산의 원인

부동소수점 방식은 컴퓨터 내부에서 이진수로 실수를 표현하는 방식입니다. 이 과정에서 일부 소수값은 정확하게 표현되지 않으며, 계산 결과에 아주 미세한 오차가 발생하게 됩니다. 예를 들어,

console.log(0.1 + 0.2); // 0.30000000000000004

 

와 같이 예상과 다른 결과가 나타날 수 있습니다. 이러한 문제는 특히 합산이나 누적 연산에서 누적되어 의도치 않은 결과를 초래할 수 있습니다.

 

 


2. 오차를 고려한 비교 방법

실무에서는 여러 소수점 값을 합산하여 목표값(예: 100)과 비교할 때, 부동소수점 연산으로 인한 미세 오차를 감안하여 비교하는 방법을 사용합니다.
아래에서는 네 가지 방법을 소개합니다.

 

2.1. Epsilon을 활용한 비교

자바스크립트에서는 Number.EPSILON을 사용하여 두 수의 차이가 아주 작은 경우(예: 1e-16 정도)에는 동일하다고 판단할 수 있습니다.


원리: 두 값의 차이가 Number.EPSILON보다 작으면 오차 범위 내에서 동일하다고 간주합니다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>소수점 합계 비교 (Epsilon 활용)</title>
  <style>
    input { margin: 5px; }
  </style>
</head>
<body>
  <h2>소수점 값 입력 후 합계가 100인지 확인</h2>
  <input type="text" id="val1" placeholder="값 입력 (예: 24.913)">
  <input type="text" id="val2" placeholder="값 입력 (예: 19.380)">
  <input type="text" id="val3" placeholder="값 입력 (예: 28.971)">
  <input type="text" id="val4" placeholder="값 입력 (예: 18.088)">
  <input type="text" id="val5" placeholder="값 입력 (예: 8.648)">
  <br>
  <button onclick="checkSum()">합계 확인</button>
  <p id="result"></p>

  <script>
    function getValues() {
      const inputs = [
        document.getElementById('val1').value,
        document.getElementById('val2').value,
        document.getElementById('val3').value,
        document.getElementById('val4').value,
        document.getElementById('val5').value
      ];
      // 입력값을 Number로 변환 (빈 값은 0으로 처리)
      return inputs.map(val => parseFloat(val) || 0);
    }

    // Epsilon을 이용한 비교 함수
    function isApproximatelyEqual(a, b) {
      return Math.abs(a - b) < Number.EPSILON;
    }

    function checkSum() {
      const values = getValues();
      const sum = values.reduce((acc, num) => acc + num, 0);
      const target = 100.0;
      const resultEl = document.getElementById('result');
      
      const epsilonEqual = isApproximatelyEqual(sum, target);
      
      resultEl.innerHTML = `<strong>입력 값 합계:</strong> ${sum}<br>
                              <strong>Epsilon 비교:</strong> ${epsilonEqual ? '100과 동일함' : '100과 동일하지 않음'}`;
    }
  </script>
</body>
</html>

 

 

위 코드에서는 사용자가 입력한 값들을 parseFloat()로 숫자로 변환한 후 reduce()를 이용하여 합산합니다. 그 후 Number.EPSILON을 이용하여 미세한 차이를 무시하고 100과 동일한지 비교합니다.


2.2. 정수 변환(배수 환산 후 반올림)으로 비교

소수점 값을 일정 배수로 확대하여 정수로 변환한 후 반올림하여 비교하는 방식입니다.


원리: 예를 들어 소수점 6자리까지 비교하고 싶다면 모든 값을 1,000,000배하여 정수로 만든 뒤 반올림하여 비교합니다.

 

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>소수점 합계 비교 (정수 변환)</title>
  <style>
    input { margin: 5px; }
  </style>
</head>
<body>
  <h2>소수점 값 입력 후 합계가 100인지 확인</h2>
  <input type="text" id="val1" placeholder="값 입력 (예: 24.913)">
  <input type="text" id="val2" placeholder="값 입력 (예: 19.380)">
  <input type="text" id="val3" placeholder="값 입력 (예: 28.971)">
  <input type="text" id="val4" placeholder="값 입력 (예: 18.088)">
  <input type="text" id="val5" placeholder="값 입력 (예: 8.648)">
  <br>
  <button onclick="checkSumByRounding()">합계 확인</button>
  <p id="result"></p>

  <script>
    function getValues() {
      const inputs = [
        document.getElementById('val1').value,
        document.getElementById('val2').value,
        document.getElementById('val3').value,
        document.getElementById('val4').value,
        document.getElementById('val5').value
      ];
      return inputs.map(val => parseFloat(val) || 0);
    }

    function isEqualByRounding(a, b, multiplier = 1000000) {
      return Math.round(a * multiplier) === Math.round(b * multiplier);
    }

    function checkSumByRounding() {
      const values = getValues();
      const sum = values.reduce((acc, num) => acc + num, 0);
      const target = 100.0;
      const resultEl = document.getElementById('result');
      
      const roundedEqual = isEqualByRounding(sum, target);
      
      resultEl.innerHTML = `<strong>입력 값 합계:</strong> ${sum}<br>
                              <strong>정수 변환 비교:</strong> ${roundedEqual ? '100과 동일함' : '100과 동일하지 않음'}`;
    }
  </script>
</body>
</html>

이 방식은 소수점 이하의 정밀도를 보장하기 위해 입력된 숫자에 일정 배수를 곱한 후 반올림하여 정수끼리 비교하는 방법입니다.


2.3. toFixed()를 활용한 비교

toFixed() 메서드는 소수점 이하를 지정한 자리수만큼 고정된 문자열로 반환합니다. 이 문자열을 비교하면 미세한 오차를 무시할 수 있습니다.


원리: 지정한 자릿수까지의 값을 문자열로 만든 후 비교하여, 부동소수점 오차를 제거합니다.


예제 코드:

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>소수점 합계 비교 (toFixed 사용)</title>
  <style>
    input { margin: 5px; }
  </style>
</head>
<body>
  <h2>소수점 값 입력 후 합계가 100인지 확인</h2>
  <input type="text" id="val1" placeholder="값 입력 (예: 24.913)">
  <input type="text" id="val2" placeholder="값 입력 (예: 19.380)">
  <input type="text" id="val3" placeholder="값 입력 (예: 28.971)">
  <input type="text" id="val4" placeholder="값 입력 (예: 18.088)">
  <input type="text" id="val5" placeholder="값 입력 (예: 8.648)">
  <br>
  <button onclick="checkSumByFixed()">합계 확인</button>
  <p id="result"></p>

  <script>
    function getValues() {
      const inputs = [
        document.getElementById('val1').value,
        document.getElementById('val2').value,
        document.getElementById('val3').value,
        document.getElementById('val4').value,
        document.getElementById('val5').value
      ];
      return inputs.map(val => parseFloat(val) || 0);
    }

    function isEqualByFixed(a, b, decimals = 10) {
      return a.toFixed(decimals) === b.toFixed(decimals);
    }

    function checkSumByFixed() {
      const values = getValues();
      const sum = values.reduce((acc, num) => acc + num, 0);
      const target = 100.0;
      const resultEl = document.getElementById('result');
      
      const fixedEqual = isEqualByFixed(sum, target);
      
      resultEl.innerHTML = `<strong>입력 값 합계:</strong> ${sum}<br>
                              <strong>toFixed 비교:</strong> ${fixedEqual ? '100과 동일함' : '100과 동일하지 않음'}`;
    }
  </script>
</body>
</html>

 

위 코드는 지정한 소수점 이하 자릿수(여기서는 10자리)로 문자열 변환 후 비교하여 오차 문제를 완화합니다.


2.4. Big.js 라이브러리를 활용한 비교 (CDN 사용)

부동소수점 오차 문제를 근본적으로 해결하려면, 외부 라이브러리를 활용하는 방법도 있습니다.
Big.js는 고정 소수점 산술을 지원하여, 아주 정밀한 계산을 가능하게 합니다. CDN을 이용해 쉽게 적용할 수 있습니다.

 

예제 코드:

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>소수점 합계 비교 (Big.js 사용)</title>
  <style>
    input { margin: 5px; }
  </style>
  <!-- Big.js CDN -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/big.js/6.1.1/big.min.js"></script>
</head>
<body>
  <h2>소수점 값 입력 후 합계가 100인지 확인 (Big.js)</h2>
  <input type="text" id="bval1" placeholder="값 입력 (예: 24.913)">
  <input type="text" id="bval2" placeholder="값 입력 (예: 19.380)">
  <input type="text" id="bval3" placeholder="값 입력 (예: 28.971)">
  <input type="text" id="bval4" placeholder="값 입력 (예: 18.088)">
  <input type="text" id="bval5" placeholder="값 입력 (예: 8.648)">
  <br>
  <button onclick="checkSumBig()">합계 확인</button>
  <p id="bresult"></p>

  <script>
    function getBigValues() {
      const inputs = [
        document.getElementById('bval1').value,
        document.getElementById('bval2').value,
        document.getElementById('bval3').value,
        document.getElementById('bval4').value,
        document.getElementById('bval5').value
      ];
      // 빈 값은 0으로 처리
      return inputs.map(val => new Big(val || 0));
    }

    function checkSumBig() {
      const values = getBigValues();
      let sum = new Big(0);
      values.forEach(num => {
        sum = sum.plus(num);
      });
      const target = new Big(100);
      const resultEl = document.getElementById('bresult');
      
      resultEl.innerHTML = `<strong>입력 값 합계:</strong> ${sum.toString()}<br>
                              <strong>Big.js 비교:</strong> ${sum.eq(target) ? '100과 동일함' : '100과 동일하지 않음'}`;
    }
  </script>
</body>
</html>

 

 

이 예제에서는 CDN을 통해 Big.js를 불러오고, 사용자가 입력한 값을 Big 객체로 변환하여 정확하게 합산합니다. Big.js의 eq() 메서드를 이용해 100과 동일한지를 확인합니다.


3. 결론

 

자바스크립트의 부동소수점 연산 특성 때문에 발생하는 미세 오차는 실제 계산에서 큰 문제를 일으킬 수 있습니다.
따라서,

  • Epsilon을 활용한 비교: 아주 작은 오차를 무시할 수 있으나, 극미세한 차이만 허용
  • 정수 변환 후 반올림: 소수점 이하 자릿수를 일정 배수로 확대한 후 정수로 변환하여 비교
  • toFixed() 활용: 지정한 소수점 이하 자릿수를 고정한 문자열로 비교하여 오차 제거
  • Big.js 같은 라이브러리 사용: 외부 라이브러리를 통해 정밀 산술 연산을 수행하여 오차 문제를 완벽하게 해결

 

위와 같은 방법들을 상황에 맞게 적용하면, 사용자로부터 입력받은 소수점 값들을 누적하여 100과 같은 결과를 비교할 때 발생하는 부동소수점 오차 문제를 효과적으로 해결할 수 있습니다.

이 글이 자바스크립트 부동소수점 오차 문제에 대한 이해와, 실무에서 이를 다루는 방법에 대해 도움이 되기를 바랍니다.

 

 

 

실무적용

var persentObj = $("#SelectDeptList_PopupSearchFinancialDept").find("input[name=Persent_PopupSearchFinancialDept]");
let persentSum = 0.0;
const epsilon = 0.0001; // 0.01% 오차 허용

for (let i = 0; i < persentObj.length; i++) {
    const currentInput = $(persentObj).eq(i);
    const inputVal = currentInput.val().trim();

    if (!inputVal || isNaN(inputVal)) {
        alert("비율을 확인하시기 바랍니다.");
        currentInput.focus();
        return false;
    }

    if (inputVal === "0") {
        alert("비율이 0%인 부서가 존재 합니다. 해당 부서를 삭제해 주시기 바랍니다.");
        currentInput.focus();
        return false;
    }

    persentSum += parseFloat(inputVal) * 100;
}

// 부동소수점 오차 고려한 비교
if (Math.abs(persentSum - 10000) > epsilon) {
    const displayedSum = (persentSum / 100).toFixed(2);
    alert(`비율의 합은 100%여야 합니다.\n현재 입력된 총비율은 ${displayedSum}% 입니다.`);
    return false;
}

return true;