연봉 5% 올랐는데 왜 체감은 안 될까? [2편] 개발기
지난 [1편]에서는 연봉 협상 시뮬레이터가 뭘 보여주는지 다뤘습니다. 이번 편에서는 역산 기능과 이직 비교 로직을 어떻게 구현했는지, 그리고 기존 도구를 재사용하며 얻은 이점을 기록합니다.
1. 세금 계산 로직, 통째로 재사용하기
이 시뮬레이터의 핵심 계산 함수(computeNetPay)는 이전에 만든 [연봉 실수령액 계산기]의 로직을 그대로 가져왔습니다. 근로소득공제, 인적공제, 누진세율, 근로소득세액공제까지 이어지는 흐름을 다시 짤 필요가 없었습니다.
function computeNetPay(annualSalary, dependents, children, mealAllowance, withholdingRatio) {
// 4대보험 계산
// ...
// 근로소득공제 → 과세표준 → 누진세율 → 세액공제
// ...
return { monthlyGross, netMonthly, netAnnual: netMonthly * 12 };
}
이미 검증된 계산 로직이 있으니, 이번엔 “이 함수를 여러 번 호출해서 무엇을 비교할 것인가”에만 집중할 수 있었습니다. 실제로 이 프로젝트에서 새로 짠 코드의 대부분은 세금 계산이 아니라, 그 결과를 여러 시나리오에 걸쳐 비교하는 로직이었습니다.
2. 목표 금액에서 인상률을 거꾸로 찾기 — 이분탐색
“월 실수령액을 30만원 늘리려면 몇 % 인상이 필요한가”는 세금 계산 함수의 역함수를 구하는 문제입니다. 그런데 이 함수는 구간마다 공제율과 세율이 바뀌는 계단식 함수라, 역함수를 수식으로 딱 떨어지게 구하기 어렵습니다.
대신 **이분탐색(binary search)**을 썼습니다. 인상률 0%~100% 사이에서, 중간값을 넣어보고 목표에 못 미치면 그 구간의 위쪽 절반을, 넘으면 아래쪽 절반을 다시 탐색하는 방식입니다.
function reverseCalc() {
const target = Number(document.getElementById('targetIncrease').value);
const baseResult = computeNetPay(baseAnnual, ...);
let lo = 0, hi = 100;
for (let i = 0; i < 30; i++) {
const mid = (lo + hi) / 2;
const newAnnual = Math.round(baseAnnual * (1 + mid / 100));
const r = computeNetPay(newAnnual, ...);
if ((r.netMonthly - baseResult.netMonthly) < target) lo = mid;
else hi = mid;
}
// hi가 목표를 만족하는 인상률의 근사값
}
30번 반복하면 오차가 사실상 무의미한 수준(2^-30)까지 좁혀집니다. 세금 계산이 복잡해서 수식으로 못 풀 때, 이렇게 “계산은 그대로 두고 입력값만 반복해서 좁혀나가는” 방식이 실용적인 대안이 될 수 있습니다. 이 방법이 통하는 전제는 “연봉이 오르면 실수령액도 오른다”는 단조증가 성질인데, 세금 구조상 이 전제는 항상 성립합니다.
3. 이직 vs 인상 비교 — 두 개의 경로를 나란히 시뮬레이션
이직과 매년 인상을 비교하는 기능은, 두 개의 별도 시나리오를 매년 반복 계산해서 누적하는 구조입니다.
let staySalary = baseSalary;
let jumpSalary = baseSalary;
for (let y = 1; y <= years; y++) {
staySalary = y === 1 ? staySalary : staySalary * (1 + annualRaise / 100);
jumpSalary = y === 1 ? jumpSalary * (1 + jumpRate / 100) : jumpSalary * (1 + annualRaise / 100);
stayTotal += computeNetPay(staySalary, ...).netAnnual;
jumpTotal += computeNetPay(jumpSalary, ...).netAnnual;
}
이직 경로는 1년차에만 큰 폭(jumpRate)으로 오르고, 그 이후로는 유지 경로와 똑같은 인상률로 오른다고 가정했습니다. 실제로는 이직 후 회사마다 인상 정책이 다르겠지만, 비교의 기준을 통일해야 “이직 자체의 효과”만 따로 볼 수 있어서 이렇게 단순화했습니다. 이 가정은 계산기 안내에도 명시해뒀습니다.
4. 금액 없는 공유 카드 — 연봉 계산기 때 만든 캔버스 함수 재사용
지난 연봉 실수령액 계산기에서 “조선시대 등급 카드”를 캔버스로 그렸던 코드를, 이번엔 “인상률-체감률 격차 카드”로 용도만 바꿔서 재사용했습니다.
ctx.font = 'bold 52px "Malgun Gothic", sans-serif';
ctx.fillStyle = '#ffd166';
ctx.fillText(`격차 ${gap}%p`, W / 2, 440);
금액이 아니라 “격차(%p)”라는, 그 자체로는 개인 정보가 아닌 숫자만 카드에 담기도록 설계한 부분은 동일합니다. 도구는 다르지만 “공유해도 부담 없는 정보만 이미지에 담는다”는 원칙은 그대로 가져왔습니다.
5. 마무리하며
이번 프로젝트는 새로운 걸 만들었다기보다, 이미 만든 부품들을 새로운 조합으로 다시 쓴 작업에 가까웠습니다. 세금 계산 함수, 캔버스 공유 카드, 아코디언과 다크모드까지, 지난 도구들에서 검증된 코드를 그대로 가져다 쓰니 이번엔 “새 시나리오를 어떻게 비교할 것인가”라는 문제에만 집중할 수 있었습니다. 도구를 하나씩 만들 때마다 다음 도구를 더 빨리 만들 수 있는 재료가 쌓인다는 걸 체감한 프로젝트였습니다.
무료로 직접 사용해보고 싶으시다면 [1편]에 링크해둔 시뮬레이터를 확인해보세요.