벨로그 첫 글입니다. 마치 친구한테 평소에 안쓰던 편지를 쓰는 기분인데요. 열심히 한번 써보겠읍니다.
자기소개를 하자면 저는 블록체인 익스플로러 서비스를 운영하는 1년차 (응애) 웹 프론트앤드 개발자입니다. 근래 블록체인 웹 지갑을 개발하였습니다. 개발하면서 생겼던 작은 이슈에 대해 공유해보고자 합니다 :)
일반 원화의 경우 송금할 수 있는 가장 작은 단위는 1원입니다. 그러나 블록체인은 체인에 따라 0.000000001도 보낼 수 있습니다. 체인마다 decimal(허용하는 소수점 자릿수) 값이 정해져있고 해당 decimal 까지 돈의 단위가 오고가죠. 간혹 소숫점 18자리까지 다루어야하는 체인들도 있습니다.
돈을 직접 주고 받는 지갑에서는 이 작은 단위에 오류가 있으면 안되겠죠.
사용자가 1000.056489원을 송금하고자 input에 값을 입력하였고, 어떠한 기능을 위해 1000.056489 에 연산을 수행한다고 합시다. 연산전에 생각해 볼 것이 있습니다. 1000.056489이 진짜 1000.056489 일까요?
정답은 No
입니다.
자바스크립트의 Number 타입은 이 작은 숫자들의 정확도를 보장하지 못합니다. 왜일까요?
컴퓨터는 2진수로 데이터를 저장하죠. 10진수로 값이 입력되면 이를 2진수로 변환합니다. 변환과정에서, 10진수의 소숫점 부분이 2의 배수가 아니라면 2진수로 딱 떨어지게 수를 변환할 수 없습니다. 순환소수마냥 소수부가 계속되는거죠.
ECMAScript 사양에 따르면, 자바스크립트의 숫자 타입은 배정밀도 64비트 부동소수점
형식을 따릅니다.
부동소수점 형식이 무엇일까요?
부동 소숫점
컴퓨터에서 실수를 표현하는 방법은 크게 4가지가 있습니다. 32bit, 64bit에 따라 단정밀도
와 배정밀도
로 분류됩니다. 그리고 소숫점이 고정되어 있냐로 고정 소숫점
과 부동 소숫점
이 나뉘죠.
고정 소숫점
0 000000000000000 0000000000000000
부호 정수 소수점 이하
고정 소숫점 방식은 소수부와 정수부를 나누어서 메모리에 저장되는 방식입니다. 자릿수를 제한하기 때문에 표현할 수 있는 범위가 작고, 메모리가 낭비 될 수 있습니다.
부동 소숫점
부동 소숫점 방식은 고정되어 있지 않고 좌우로 움직 일 수 있는 방식입니다. 크게 지수부와 가수부로 나뉩니다. 배정밀도의 경우의 지수부는 11bit, 가수부는 52bit입니다. 지수부는 Exponential로 표기되는 소숫점을 나타내며 가수부는 가수 혹은 유효 숫자를 나타냅니다.
즉, 소숫점 17자리 수까지만 유효성을 보장해주고 그 이후의 작은 숫자들의 정밀도는 보장하지 않습니다. Number 타입의 Input을 만든다면 소숫점 17자리 이상의 숫자들은 유효성이 보장되지 않으니 문제가 됩니다.
숫자의 유효성을 보장하고 싶다면 어떻게 해야할까요?
이럴때는 String
타입으로 숫자를 저장해야합니다. String은 배열처럼 저장이 됩니다. 아무리 긴 문자열이라도 입력한 대로 저장하죠.
아하! 그럼 사용자의 입력을 받는 Input을 String 타입으로 지정해서 쓰면 되겠네요!
String 타입으로 Input을 사용하기 이전에 고려해야하는 부분이 있습니다. Number 타입으로 input을 지정해서 사용하면 숫자 이외의 문자열은 입력이 되지 않습니다. String 타입으로 Number 타입을 지정해서 쓰려면 숫자만 입력되게 해야하고, 값이 변경 될때 마다 유효한 숫자인지 검사를 하기 위해 확인하는 코드를 추가해야겠죠.
// 숫자 입력만 받음
<input type="number"/>
// 문자형 입력을 받음. 숫자 입력만을 받기 위해서는 on:keydown 혹은 on:change 등 키보드 입력이 들어올때마다 유효한 숫자인지 검사해야함.
<input type="text"/>
Input의 타입을 Number로 지정하여 입력받는 변수의 숫자 형식 유지를 하고, 숫자의 정밀도를 유지하기 위해 String 형식으로 변수를 저장하고 싶다면, Number 타입으로 Input은 유지를 한 채로, event.target.value
으로 입력 값으로 저장하면 됩니다. ( event.target.value
은 string 으로 인식됩니다.)
<script>
function update(event: Event) {
const target = event?.target as HTMLInputElement;
value = target?.value || '';
}
</script>
{#if type === 'text'}
<input
type="text"
bind:value
{placeholder} />
{:else if type === 'number'}
<input
type="number"
{value}
on:change={update} on:keydown={update}
on:keypress={update} on:keyup={update}
{placeholder} />
{/if}
그럼 위와 같은 코드가 완성됩니다.
위 코드에서는 문제가 하나 있습니다. 뭘까요?
바로 update 함수가 쓸데없이 너무 많이 불리게 됩니다. 이때 생각 할 수 있는게 throttle
과 debounce
입니다.
throttle 과 debounce
이벤트가 지속적으로 발생하면 필요하지 않게 과도한 이벤트 처리가 행해집니다. 예를 들어 키보드의 입력이 지속되거나 스크롤 이벤트가 생기면 적게는 수십번 많게는 수백번의 이벤트가 발생할 수 있죠. 때문에 유의미한 시점에서만 이벤트를 처리하기 위해 throttle
과 debounce
를 사용하는게 최적화에 좋아보입니다.
throttle
은 이벤트가 발생하고 일정 주기마다 이벤트가 발생.debounce
는 연속되는 이벤트 중에서 가장 마지막 이벤트만을 유의미한 이벤트로 인식.
Input과 같은 키보드 이벤트의 경우에는 가장 마지막 이벤트만을 유의미한 이벤트로 사용하는 debounce
가 적합합니다.
debounce
는 코드로도 구현 할 수 있지만 Lodash 함수를 사용하면 쉽게 구현 할 수 있습니다.
const onChange = _. debounce (update, debounceTime) ;
function update(event: Event) {
const target = event?.target as HTMLInputElement;
value = target?.value || '';
}
좋습니다! 이제 안전하게 String type의 입력 값을 저장하게 되었습니다.
String으로 연산하기: Big.js
String 변수에 연산을 하려면 어떻게 해야 할까요?
이때 사용하면 좋은게 Decimal.js
혹은 Big.js
와 같은 라이브러리입니다. 해당 라이브러리는 숫자연산을 String 타입으로 수행합니다. 따라서 숫자의 유효성을 보장해 줍니다.
저는 프로젝트에서 Big.js
를 사용하였습니다. Decimal.js
가 Big.js
보다 최대 정밀도 범위가 더 넓고 다양한 수학 함수를 제공하지만, 많은 기능을 제공하는 만큼 라이브러리의 크기가 더 큽니다. 저는 소숫점 18자리까지의 정밀도를 보장해야하고 간단한 사칙연산만을 필요로 했기 때문에 Big.js
를 사용하기로 하였습니다. 구현하고자하는 기능에 따라 적합한 라이브러리를 선택해 사용하면 되겠습니다.
라이브러리를 설치한 뒤 문서에 나온 대로 사용하면 됩니다.
Javascript의 Number 타입의 부동소숫점 정확도의 문제와 String 타입을 사용해야 하는 이유. 그리고 String 타입을 Number 처럼 입력 받아 연산하는 방법까지 정리해보았습니다.