계산 도구를 만들 때 독자가 정말 알고 싶은 것은 숫자가 어떻게 나왔는지였다. 실제 소스의 계산 분리와 검산 기록을 다시 확인하고 익명 입력으로 재사용할 수 있는 작은 예제를 만들었다.

화면이 예쁘다고 계산이 맞는 것은 아니다

계산 화면은 결과 숫자를 크게 보여주기 쉽다. 그러나 입력 조건이 잘못되었거나 공식의 단위가 섞이면 큰 숫자는 오히려 잘못된 확신을 줄 수 있다. 나는 실제 계산 도구의 소스를 다시 읽고 계산 함수가 화면에서 분리되어 있는 부분을 확인했다. 입력 검사와 계산 결과를 따로 반환하므로 화면 없이도 같은 조건을 다시 검산할 수 있었다.

이번에 기존 계산 모듈의 로컬 검사 네 개를 실행했다. 가상 입력의 결과와 역산, 표시용 조정과 핵심 계산의 분리, 잘못된 입력과 주소 상태를 확인했다. 실제 운영비를 새로 수집하거나 사업 수익을 검증한 작업은 아니다. 소스와 테스트가 확인한 산술의 범위를 공개 설명의 출발점으로 삼았다.

기간과 단위를 먼저 같은 기준으로 맞춘다

한 달의 고정 비용에 하루의 변동 비용을 그대로 더하면 합계부터 의미가 어긋난다. 새 예제는 같은 기간의 고정 비용과 변동 비용, 같은 단위의 추가 목표를 받도록 했다. 수량도 그 기간에 해당하는 수량이다. 입력창이 숫자를 받는다는 이유만으로 이 전제까지 자동으로 확인할 수는 없다. 화면에서 기준을 설명하고 자료의 기간도 확인해야 한다.

예비율은 비용 합계에 적용하며 매출 차감율은 필요한 총액에서 빠지는 비율로 정의했다. 비용에 예비분과 목표를 더한 순액을 구한 뒤, 차감 이후 그 순액이 남도록 총액을 계산한다. 이 정의를 바꾸면 공식도 바뀐다. 이름이 비슷한 비율을 같은 계산으로 처리하지 않고 어느 금액에 적용하는지를 함께 남긴다.

올림한 값도 다시 원래 조건으로 돌아가 본다

단위당 값을 화면에 표시하려면 소수점을 어떻게 처리할지 정해야 한다. 필요한 총액을 확보하는 설명용 예제에서는 선택한 소수점 격자로 올림한다. 그러고 나서 올림한 값에 수량을 곱했을 때 계산한 총액보다 작아지지 않는지 확인한다. 표시 숫자를 보기 좋게 반올림하고 검산을 끝내면 작은 차이를 놓칠 수 있다.

새 모듈은 차감할 금액과 차감 후 순액도 돌려준다. 총액에서 차감액을 뺐을 때 앞에서 정한 필요 순액으로 돌아오는지 독립적으로 비교할 수 있다. 부동소수점 계산에는 작은 오차가 생길 수 있으므로 실제 금전 정산에서는 통화별 정밀도와 정산 규칙을 더 엄격히 정해야 한다. 이 예제의 올림 규칙을 모든 거래에 적용하는 정답으로 제시하지 않는다.

코드와 구현 살펴보기

화면 없이 확인하는 단위당 값 계산

export function requiredUnitValue({fixed,variable,target,units,bufferPercent=0,feePercent=0,decimals=0}){
 const amount=(value,name)=>{if(value===''||value===null||value===undefined||typeof value==='boolean')throw new TypeError(name+' requires a number');const n=Number(value);if(!Number.isFinite(n)||n<0||n>1e12)throw new RangeError(name+' outside the example range');return n;};
 const f=amount(fixed,'fixed'),v=amount(variable,'variable'),t=amount(target,'target'),q=amount(units,'units'),b=amount(bufferPercent,'bufferPercent'),p=amount(feePercent,'feePercent');
 if(!Number.isInteger(q)||q<1||q>1e6)throw new RangeError('units must be an integer from1 to1000000');
 if(b>100||p>=100)throw new RangeError('buffer must be at most100 and fee below100');
 if(!Number.isInteger(decimals)||decimals<0||decimals>4)throw new RangeError('decimals must be an integer from0 to4');
 const cost=f+v,buffer=cost*b/100,netNeed=cost+buffer+t,grossNeed=netNeed/(1-p/100),unitExact=grossNeed/q,scale=10**decimals,unitCeil=Math.ceil(unitExact*scale)/scale;
 if(![grossNeed,unitExact,unitCeil].every(Number.isFinite))throw new RangeError('Calculated value is outside the example range');
 return {cost,buffer,netNeed,grossNeed,fee:grossNeed*p/100,units:q,unitExact,unitCeil,decimals,scope:'Illustrative arithmetic only. All amounts share one period and unit; no tax, demand forecast or business suitability model.'};
}

계산할 수 없는 입력을 성공 값으로 바꾸지 않는다

수량이 영이면 단위당 값을 나눌 수 없고 차감율이 백 퍼센트면 남는 몫이 없다. 빈 입력을 무조건 영으로 바꾸면 사용자가 아직 입력하지 않은 상태와 실제 비용이 영인 상태를 구별하기 어렵다. 새 예제는 빈값과 음수, 숫자가 아닌 값, 분수 수량을 명시적으로 거절한다. 실제 도구에서 허용할 수량의 성격은 별도로 정할 수 있다.

핵심 함수는 입력이 잘못되면 결과를 만들지 않고 오류를 돌려주는 흐름을 유지한다. 화면에서는 이전의 정상 결과를 지우고 현재 조건의 문제를 보여준다. 입력을 수정했는데 지난 숫자가 계속 남아 있으면 사용자는 그 숫자가 새 조건의 답이라고 읽을 수 있다. 계산과 표시를 분리하는 일은 이런 상태의 의미를 분명히 하는 데도 도움이 된다.

같은 코드와 설명용 조건을 같이 공개한다

새 계산 함수에는 역산과 올림 격자, 입력 경계 등 여덟 검사를 만들었다. 아래 예제는 그 함수를 그대로 사용하며 입력을 외부로 보내지 않는다. 숫자는 작은 가상 단위로 두었고 실제 사업의 비용이나 적정 가격처럼 설명하지 않았다. 세금과 수요, 회수 시점과 다른 비용은 이 산술 모델에 포함되어 있지 않다.

계산식과 전제가 공개되어 있으면 독자는 무엇을 바꿔야 하는지 스스로 판단할 수 있다. 이번 검사는 로컬 함수와 공개 HTML 응답의 범위이며 새 브라우저 조작은 검수하지 않았다. 적용할 때는 실제 단위와 자료, 표시 규칙, 화면 상태를 이어서 확인한다. 나는 결과 숫자 하나보다 그 숫자가 만들어지는 기준을 남기려 한다.