세금·급여 계산기 6종 한 번에 구현
목차
한 번에 계산기 6개를 밀어넣었다. 국민연금, 자동차세, 종합소득세, 주휴수당, 시급, 연차수당 — 각각 도메인이 다 달라서 단순 복붙이 아니라 로직마다 따로 파고들어야 했던 작업이다.
왜 6개를 한 번에?
원래 계획은 하나씩 PR을 올리는 방향이었다. 그런데 6개 계산기의 입력 구조와 출력 포맷이 서로 맞물리는 부분이 있었고, 먼저 배포한 것 기준으로 다음 걸 짜다 보면 중간에 인터페이스가 틀어질 가능성이 있었다. 그래서 src/lib/ 아래에 파일 6개를 동시에 잡고, 전체 윤곽을 먼저 맞춘 뒤 각각 채워 나가는 방식을 택했다.
팀 입장에서도 "이번 스프린트에 이 묶음이 나온다"는 게 명확한 편이 PR 리뷰 부담을 분산시키기보다는 오히려 한 시점에 한꺼번에 검토할 수 있어서 컨텍스트 전환이 줄어든다. 물론 리뷰어 입장에서 파일 6개짜리 PR이 반갑진 않지만, 도메인별로 파일이 분리되어 있으니 각자 담당 영역만 집중해서 볼 수 있도록 리뷰 가이드를 같이 달았다.
파일별 역할과 주의점
| 파일 | 계산 도메인 | 주요 복잡도 |
|---|---|---|
national-pension.ts |
국민연금 | 소득 상·하한 기준 적용 |
car-tax.ts |
자동차세 | 배기량 구간별 세율 테이블 |
comprehensive-income-tax.ts |
종합소득세 | 누진세율 구간 + 공제 항목 |
weekly-holiday.ts |
주휴수당 | 소정근로시간 조건 분기 |
hourly.ts |
시급 환산 | 월급/연봉 → 시급 역산 |
annual-leave.ts |
연차수당 | 통상임금 기반 산정 |
6개 중에서 손이 제일 많이 간 건 comprehensive-income-tax.ts였다. 누진세율 구간 자체는 테이블로 관리하면 되는데, 공제 항목이 중첩되는 경우의 처리 순서를 잘못 짜면 엣지 케이스에서 음수 세액이 튀어나온다. 실제로 내부 테스트에서 한 번 터졌고, 공제 후 과세표준이 0 이하로 내려가면 0으로 클램핑하는 처리를 명시적으로 넣었다.
// comprehensive-income-tax.ts 중 핵심 패턴
function calcTax(taxableIncome: number, deductions: number): number {
const base = Math.max(0, taxableIncome - deductions); // 음수 방지
return TAX_BRACKETS.reduce((acc, bracket) => {
const applicable = Math.min(base, bracket.limit) - bracket.from;
return applicable > 0 ? acc + applicable * bracket.rate : acc;
}, 0);
}
weekly-holiday.ts도 생각보다 분기가 많았다. 주 15시간 미만이면 주휴수당 자체가 발생하지 않고, 소정근로시간이 불규칙한 경우엔 평균값을 쓸지 고정값을 쓸지 정책 결정이 필요했다. 이 부분은 계산 로직보다 "어떤 값을 입력으로 받을 것인가"를 먼저 정의하는 데 시간이 더 걸렸다.
계산기 라이브러리를 짤 때 반복적으로 마주치는 패턴
이런 계산기 류 로직을 여러 개 한 번에 짜다 보면 공통적으로 반복되는 구조가 있다.
- 입력 검증을 함수 진입부에서 먼저 — 음수 소득, 0 이하 차량 배기량 같은 값이 중간 로직에 섞이면 디버깅이 고통스러움
- 세율/구간 테이블은 상수 파일로 분리 — 법령 개정 시 한 곳만 건드리면 되도록
- 순수 함수(pure function) 유지 — 외부 상태 없이 입력 → 출력만. 테스트 작성이 압도적으로 쉬워짐
- 반올림 정책 명시 — 원 단위 절사인지, 십 원 단위 반올림인지 함수 내부에서 암묵적으로 처리하지 말고 주석이든 타입이든 명확하게
car-tax.ts의 경우 배기량 구간 테이블을 별도 상수로 뺐는데, 지방세율 개정이 있을 때 파일 하나만 보면 된다는 게 나중에 꽤 편할 거라고 생각한다.
// car-tax.ts — 구간 테이블 상수 분리 패턴
const CC_TAX_RATES: { maxCC: number; ratePerCC: number }[] = [
{ maxCC: 1000, ratePerCC: 80 },
{ maxCC: 1600, ratePerCC: 140 },
{ maxCC: 2000, ratePerCC: 200 },
{ maxCC: Infinity, ratePerCC: 220 },
];
annual-leave.ts는 통상임금 산정 방식이 사안마다 달라질 수 있어서, 통상임금 계산 로직은 별도 유틸로 빼놓고 연차수당 함수에서 주입받는 구조로 잡았다. 나중에 통상임금 정의가 달라지더라도 해당 유틸만 수정하면 연차수당 로직은 건드리지 않아도 된다.
6개 파일이 src/lib/ 아래에 나란히 늘어선 걸 보는 게 생각보다 기분 좋았다. 도메인이 다 달라서 피곤하기도 했지만, 각 계산 로직이 독립적으로 격리되어 있으니 하나가 틀려도 나머지에 영향이 없다는 게 구조적으로 맞게 된 것 같아 다행이다.
다음은 이 6개에 대한 단위 테스트 커버리지 확보가 우선이다.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.