일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 원시자료형
- css in js
- 호스트인식
- cta버튼
- 개발자
- cta button
- html
- 객체지향
- OOP
- WAI-ARIA
- JavaScript
- self reliance
- 코드스테이스
- 참조자료형
- codestate
- codestates
- 자바스크립트
- JS
- 계산기
- Javascript #코드스테이츠
- 프로토타입
- condestates
- Prototype
- CDD
- 프론트엔드
- css
- frontend
- 코드스테이츠
- 회고
- Router
- Today
- Total
jh.nrtv
소수 데이터 핸들링하기 ( 부동 소수점 연산의 오차) 본문
개요
현재 회사에서 만들고 있는 서비스는 4가지 마켓을 대응해야 한다.
기존 우리 서비스는 일본, 한국 등의 정수 가격만을 핸들링 하면 됐으나 해당 서비스를 개선 및 확장하는 과정에서
중국의 위안, 미국의 달러 소수점 가격이 보편적인 화폐를 대응하기 위해서 기능 개발을 진행했다.
크게 개발한 것은 두 가지이다.
1. 원하는 자릿수 제한된 소수 input 대응
2. 수학 산식을 통해 나온 무한소수 원하는 자릿수 미만 처리
2번 개발의 경우 우리는 특정 계산(예를 들어 할인율 등)으로 인해서 소수점이 길어질 경우 (1.599999...) 플랫폼 공통 정책으로 반올림, 내림 등을 강제하지 않는다.
소수점 몇 번째 자리까지 남길 것인지(ex.1, 0.1, 0.01... ), 해당 소수점 이하는 반올림, 올림, 내림 중 어떤 방식으로 처리할 것인지를 사용자가 선택할 수 있도록한다.
따라서 공통 컴포넌트 내에서 위의 사항을 대응하며 개발이 진행되어야 했다.
소수 input 대응하기 (부동소수점 오차)
먼저 1번의 input을 개발하는 중에 문제점을 발견했다.
대략적인 개발 방향은 다음과 같다. input을 두고 input의 값이 변화할 때마다 이것이 사용자가 지정한 소수점 정책에 부합한지 확인하는 함수를 실행하는 것이다.
따라서 초기에는 아래와 같이 함수를 생성했다.
<s-input
v-model="formData.point"
:suffix="configs.point_name"
:reverseFillMask="isDecimalPolicy? false : true"
:mask="isDecimalPolicy? '' : '###,###,###,###,###,###'"
placeholder="0"
ref="changePointInputRef"
@update:model-value="
(val) => {
if( isDecimalPolicy ) {
const value = checkDecimalForm(val , configs.sales_price_unit)
formData.point = comma(value);
changePointInputRef.model = comma(value);
return;
}
}
"
/>
위와 같이 input의 업데이트시 확인하는 로직을 넣고
function checkDecimalForm(inputVal, salesPriceUnit) { //salesPriceUnit은 1, 0.1 , 0.01등의 판매가 단위 정책
const replacedValue = inputVal.replace(/[^\d.]/g, '');
const dotCount = (inputVal.match(/\./g) || []).length;
if ( dotCount == 0) return replacedValue
else if ( dotCount > 1) return replacedValue.slice(0, -1)
const priceUnitMultiple = 1 / salesPriceUnit
return replacedValue.endsWith('.') ? replacedValue : Math.floor((+replacedValue < 0 ? -1 : 1) * replacedValue *priceUnitMultiple ) / priceUnitMultiple;
}
위 checkDecimalForm이라는 함수로 검사를 진행했다.
코드가 매우 지저분하지만 기능이 빨리 나가야 하는 상황이었기에 어쩔 수 없었다. ㅠㅠ
하지만 위의 코드는 원하는대로 동작하지 않는다.
Math.floor의 내림처리가 정상적으로 동작하지 않아서 9.99 와 같이 .99로 끝나는 수를 입력할 수 없는 오류가 발생했다.
검색을 통해서 알아보니 컴퓨터가 소수를 처리하는 방식에 의해 오류가 발생했다.
바로 '부동 소수점의 오차'라는 것인데 간단하게 말하면 코드로 소수점 이하가 포함된 계산을 할 때에 정확하지 않은 결과가 계속해서 나오는 현상이다.
부동소수점의 오차 ?
컴퓨터가 실수를 표현한는 방식은 크게 두 가지가 있으며 아래와 같다.
1. 고정 소수점 방식
2. 부동 소수점 방식
1번인 고정 소수점 방식은 실수를 정수부, 소수부로 미리 나누어 고정된 자리수를 확보해서 실수를 표현한다. 따라서 정수와 실수가 얼마나 존재하는지와 무관하게 자리를 확보해야 하기 때문에 표현할 수 있는 범위가 적다는 단점이 존재한다.
따라서 보통 2번의 부동 소수점 방식을 활용한다.
Math.floor(78.99*100)/100
// 기대값 78.99
// 결과값 78.98
78.99*100
// 기대값 = 7899
// 결과값 = 7898.99999999999..
위 예시의 1번의 경우 Math.floor() 정적 메서드를 사용한 간단한 계산임에도 오류가 발생하는 것을 확인할 수 있다.
따라서 내가 쓴 코드에서도 .99 등을 입력할 수 없었던 것이다.
하지만 우리가 30,000원보다 29,990원 등의 가격에 익숙하듯 가격의 마지막 소수점을 .99로 맞추는 경우가 매우 빈번하기 때문에 위의 코드는 수정이 필요했다.
따라서 내림을 반올림으로 변경해서 임시로 업데이트 되었다.
그리고 최종적으로는 아래와 같이 업데이트 되었다.
function checkDecimalForm(inputVal, salesPriceUnit) {
inputVal = String(inputVal);
const replacedValue = inputVal.replace(/[^\d.]/g, '');
const dotCount = (inputVal.match(/\./g) || []).length;
if (dotCount == 0) return replacedValue
else if (dotCount > 1) return replacedValue.slice(0, -1)
const decimalSurvive = String(salesPriceUnit).split('.')[1].length
const int = String(replacedValue).split('.')[0];
const decimal = String(replacedValue).split('.')[1].slice(0, decimalSurvive);
return replacedValue.endsWith('.') ? replacedValue : `${int}.${decimal}`
}
위 코드는 input을 String으로 변경해서 처리하고 있다.
소수점 여부를 확인하고, 세팅에서 소수점 이하 몇자리까지 필요한지 확인한 후에 그 이후의 소수점은 잘라서 버리는 로직을 가지고 있다.
위와 같이 변경하면 수리적 연산이 전혀 들어가지 않고 단순히 문자열을 검사하고 자르는 등의 과정만을 거치기 때문에 '부동 소수점의 오류' 등의 오류는 전혀 발생하지 않는다.
( 다만 위의 연산은 인풋창에서 직접 순서대로 입력하는 것을 전제로 짜인 코드인데, 지금보니 복사 붙여넣기 등을 통해서 입력할 때에는 오류를 제대로 핸들링 하지 못하는 것으로 보인다. ㅠㅠ 조금 더 개선이 필요해 보인다 .. )
수학 산식을 통해 나온 무한소수 원하는 자릿수 미만 처리
이 문제는 비교적 간단하다.
세팅에서 원하는 소수점 아래 단위와, 원하는 처리방식( 내림, 올림, 반올림) 등을 받아서 그것에 따라서 동적으로 계산하고 보여주면 된다.
export function mathDecimal(num, salesPriceUnit, mathMethod) {
const multiple = 1 / salesPriceUnit
return Math[mathMethod](num*multiple)/multiple
}
물론 여기서도 부동소수점의 오류에 의해서 약간의 오차가 발생할 수 있으나 그냥 그대로 두기로 했다.
그 이유는 다음과 같다.
1. 이는 input이 아니다.
2. 계산 과정에서 발생하는 오차가 미미하다.
3. 사용자가 setting 변경으로 인해서 오차를 조정할 수 있다.
이번 개선 작업으로 인해 컴퓨터도 처리 방식에 인해서 오차가 발생할 수 있다는 것을 알았다.
소수 데이터를 원하는대로 다루는 작업은 사실 일상적으로 쓰이는 작업이고 표면적으로는 매우 간단한 작업이라고 생각했는데 생각지도 못한 곳에서 오류를 만나고 고전해서 아직 나는 멀었구나 하는 것을 다시금 깨달았다.
참조
https://www.tcpschool.com/cpp/cpp_datatype_floatingPointNumber#google_vignette
'Error' 카테고리의 다른 글
간단 회고 - 데이터의 종류를 다시 한 번 생각하며 핸들링하자! (0) | 2024.08.12 |
---|---|
[error]Error: Hydration failed because the initial UI does not match what was rendered on the server. (0) | 2023.07.17 |
[error] 배포시 미디어쿼리 적용 안되는 에러 (0) | 2023.07.17 |
[error] Type error: 'result' is possibly 'null' (0) | 2023.07.15 |
[error] vercel배포중 Module not found: Can't resolve 'perf_hooks' ReactJS (0) | 2023.07.14 |