계산기 앱 만들기: 핵심 원리와 패턴
계산기 앱 만들기: 핵심 원리와 패턴
계산기 앱, 쉬워 보이지만 쉽지 않다
"계산기 앱은 프로그래밍 초보자의 연습 프로젝트 아닌가요?" 맞습니다. 기본 사칙연산 계산기는 입문 프로젝트로 훌륭합니다. 하지만 실제 사용자가 쓸 수 있는 수준의 계산기를 만들려면 생각보다 많은 고려 사항이 있습니다. 수식 파싱, 부동소수점 정밀도, 다양한 입력 패턴 처리, 그리고 직관적인 UX까지. 이 글에서는 계산기 앱을 제대로 만들기 위한 핵심 원리와 실전 패턴들을 다룹니다.
수식 파싱과 연산자 우선순위
계산기 앱의 핵심은 사용자가 입력한 수식을 올바르게 해석하고 계산하는 것입니다. 크게 두 가지 접근 방식이 있습니다.
즉시 계산 방식: 사용자가 연산자를 누를 때마다 중간 결과를 계산합니다. 3 + 5 × 2를 입력하면 3 + 5 = 8, 8 × 2 = 16으로 계산됩니다. 기본 계산기 앱에서 흔히 사용하는 방식이지만, 수학적으로 올바르지 않습니다(정답은 13).
수식 평가 방식: 전체 수식을 입력받은 후 연산자 우선순위를 적용하여 계산합니다. 이 방식이 수학적으로 정확합니다.
수식 평가를 구현하는 가장 일반적인 방법은 Shunting-yard 알고리즘입니다. 이 알고리즘은 중위 표기법(infix notation)을 후위 표기법(postfix notation, 역폴란드 표기법)으로 변환한 후 계산합니다.
// 토큰화 (문자열을 숫자와 연산자로 분리)
function tokenize(expression) {
const tokens = [];
let current = '';
for (const char of expression) {
if ('0123456789.'.includes(char)) {
current += char;
} else if ('+-*/'.includes(char)) {
if (current) tokens.push(parseFloat(current));
tokens.push(char);
current = '';
}
}
if (current) tokens.push(parseFloat(current));
return tokens;
}
// 연산자 우선순위
const precedence = { '+': 1, '-': 1, '*': 2, '/': 2 };
eval() 함수를 사용하면 간단하게 수식을 계산할 수 있지만, 보안 위험이 크므로 사용자 입력을 eval()로 직접 실행하는 것은 절대 피해야 합니다. 항상 직접 파서를 구현하거나 안전한 수식 라이브러리를 사용하세요.
부동소수점 정밀도 문제
컴퓨터에서 소수점 연산은 정확하지 않습니다. 이것은 JavaScript뿐만 아니라 거의 모든 프로그래밍 언어에서 발생하는 IEEE 754 부동소수점 표현의 근본적인 문제입니다.
// 유명한 부동소수점 문제
0.1 + 0.2 // 0.30000000000000004
0.3 - 0.1 // 0.19999999999999998
계산기 앱에서 0.1 + 0.2 = 0.30000000000000004가 표시되면 사용자는 버그라고 생각할 것입니다. 이 문제를 해결하는 방법은 여러 가지가 있습니다.
고정 소수점 반올림: 결과를 표시할 때 적절한 자릿수로 반올림합니다.
function formatResult(value) {
// 불필요한 후행 0을 제거하면서 합리적인 정밀도 유지
return parseFloat(value.toFixed(12)).toString();
}
formatResult(0.1 + 0.2); // "0.3"
정수 연산 변환: 소수를 정수로 변환하여 계산한 후 다시 소수로 변환합니다.
function addDecimals(a, b) {
const factor = Math.pow(10, Math.max(
(a.toString().split('.')[1] || '').length,
(b.toString().split('.')[1] || '').length
));
return (Math.round(a * factor) + Math.round(b * factor)) / factor;
}
addDecimals(0.1, 0.2); // 0.3
전용 라이브러리 사용: decimal.js나 big.js 같은 임의 정밀도 라이브러리를 사용하면 가장 정확한 계산이 가능합니다. 특히 금융 관련 계산기에서는 반드시 이런 라이브러리를 사용해야 합니다.
입력 유효성 검사
사용자는 예상치 못한 입력을 합니다. 이를 적절히 처리해야 앱이 오류 없이 동작합니다.
처리해야 할 대표적인 케이스들입니다.
- 연속 연산자:
5 ++ 3→ 마지막 연산자만 유지하거나 에러 표시 - 0으로 나누기:
5 / 0→ "0으로 나눌 수 없습니다" 에러 메시지 - 빈 입력: 연산자만 있는 수식 → 무시하거나 기본값 적용
- 다중 소수점:
3.14.15→ 두 번째 소수점 무시 - 음수 처리:
-5 + 3→ 음수 부호와 빼기 연산자 구분 - 괄호 불일치:
(3 + 5→ 자동으로 닫히지 않은 괄호를 추가하거나 에러 표시
function validateExpression(expression) {
// 괄호 균형 체크
let depth = 0;
for (const char of expression) {
if (char === '(') depth++;
if (char === ')') depth--;
if (depth < 0) return { valid: false, error: '괄호가 올바르지 않습니다' };
}
if (depth !== 0) return { valid: false, error: '닫히지 않은 괄호가 있습니다' };
// 0으로 나누기 체크
if (/\/\s*0(?!\.)/.test(expression)) {
return { valid: false, error: '0으로 나눌 수 없습니다' };
}
return { valid: true };
}
UX 고려사항
계산기는 모든 사용자가 사용하는 보편적인 도구이므로 UX가 특히 중요합니다.
버튼 크기와 간격: 모바일에서 터치하기 쉬운 크기(최소 44x44px)를 보장하세요. 버튼 간 간격은 의도하지 않은 터치를 방지할 만큼 충분해야 합니다.
시각적 피드백: 버튼을 누를 때 색상 변화나 미세한 애니메이션으로 피드백을 제공하세요. 계산이 완료되면 결과 값이 강조되어야 합니다.
키보드 지원: 웹 계산기라면 물리 키보드 입력도 지원해야 합니다. 숫자 키, 연산자 키, Enter(계산), Escape(초기화), Backspace(한 글자 삭제)를 매핑하세요.
히스토리: 이전 계산 결과를 확인할 수 있으면 사용자 경험이 크게 향상됩니다. 최근 계산 목록을 표시하고, 이전 결과를 탭하여 재사용할 수 있도록 하세요.
복사 기능: 계산 결과를 길게 누르거나 아이콘을 탭하면 클립보드에 복사되도록 하세요. 작은 기능이지만 실용성이 높습니다.
반응형 디자인
계산기는 다양한 화면 크기에서 동작해야 합니다. 특히 웹 계산기라면 모바일과 데스크톱 모두에서 최적의 레이아웃을 제공해야 합니다.
.calculator {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
max-width: 400px;
margin: 0 auto;
padding: 16px;
}
.display {
grid-column: 1 / -1;
text-align: right;
font-size: clamp(1.5rem, 5vw, 2.5rem);
padding: 16px;
overflow: hidden;
text-overflow: ellipsis;
}
.btn {
aspect-ratio: 1;
font-size: clamp(1rem, 3vw, 1.5rem);
border: none;
border-radius: 8px;
cursor: pointer;
}
clamp() 함수를 사용하면 화면 크기에 따라 폰트 크기가 자연스럽게 조절됩니다. aspect-ratio로 버튼의 가로세로 비율을 유지하면 어느 화면에서나 일관된 모양을 유지할 수 있습니다.
상태 관리
계산기 앱의 상태는 생각보다 복잡합니다. 현재 입력 중인 값, 선택된 연산자, 누적 결과, 계산 히스토리, 에러 상태 등을 관리해야 합니다.
const initialState = {
display: '0', // 화면에 표시되는 값
expression: '', // 전체 수식
operator: null, // 현재 선택된 연산자
previousValue: null, // 이전 피연산자
isNewInput: true, // 새 숫자 입력 시작 여부
history: [], // 계산 히스토리
error: null, // 에러 메시지
};
function calculatorReducer(state, action) {
switch (action.type) {
case 'INPUT_DIGIT':
return {
...state,
display: state.isNewInput
? action.digit
: state.display + action.digit,
isNewInput: false,
error: null,
};
case 'INPUT_OPERATOR':
// 연산자 처리 로직
break;
case 'CALCULATE':
// 계산 실행 로직
break;
case 'CLEAR':
return initialState;
default:
return state;
}
}
Reducer 패턴을 사용하면 상태 변화를 예측 가능하게 관리할 수 있고, 테스트 작성도 쉬워집니다.
계산 로직 테스트
계산기는 정확성이 생명이므로 테스트가 매우 중요합니다. 기본 연산부터 엣지 케이스까지 다양한 테스트를 작성해야 합니다.
describe('Calculator', () => {
test('기본 사칙연산', () => {
expect(calculate('2 + 3')).toBe(5);
expect(calculate('10 - 4')).toBe(6);
expect(calculate('3 * 7')).toBe(21);
expect(calculate('15 / 3')).toBe(5);
});
test('연산자 우선순위', () => {
expect(calculate('2 + 3 * 4')).toBe(14);
expect(calculate('(2 + 3) * 4')).toBe(20);
});
test('부동소수점 정밀도', () => {
expect(calculate('0.1 + 0.2')).toBe(0.3);
expect(calculate('1.005 * 100')).toBe(100.5);
});
test('에러 처리', () => {
expect(() => calculate('5 / 0')).toThrow('0으로 나눌 수 없습니다');
expect(() => calculate('abc')).toThrow('유효하지 않은 수식');
});
});
다양한 계산기 유형
기본 사칙연산 외에도 특화된 계산기는 다양하게 존재합니다.
- 공학 계산기: 삼각함수, 로그, 거듭제곱, 팩토리얼 등 수학 함수 지원
- 금융 계산기: 대출 이자, 복리 계산, 투자 수익률, 연금 계산 등
- 단위 변환기: 길이, 무게, 온도, 통화 등의 단위 변환
- 날짜 계산기: 두 날짜 간 차이, 특정 일수 후 날짜 계산
- 세금 계산기: 소득세, 부가가치세, 취득세 등 세금 관련 계산
- 건강 계산기: BMI, 기초대사량, 칼로리 계산 등
틈새시장을 공략한다면 특정 분야에 특화된 계산기가 좋은 선택입니다. 예를 들어 "부동산 취득세 계산기"나 "관세 환급금 계산기"같은 도구는 특정 사용자에게 높은 가치를 제공합니다. 핵심 계산 로직을 정확하게 구현하고, 직관적인 인터페이스로 감싸면 충분히 경쟁력 있는 제품이 됩니다.