핵심 요약
0.1 같은 10진 소수는 이진 부동소수점으로 정확히 표현되지 않을 수 있으므로 금액은 정수 최소 단위나 검증된 decimal 방식으로 다뤄야 합니다.
검수 범위
표현 오차·비교 허용범위·반올림·금액 계산을 서로 다른 문제로 나눠 검수했습니다.
부동소수점이 앱에서 문제를 만드는 이유
앱에서 쓰는 float/double(부동소수점)은 “연속적인 실수”를 그대로 저장하는 방식이 아니라, 제한된 비트로 근사값을 표현합니다. 그래서 사람 입장에서는 딱 떨어져야 하는 값(금액, 비율, 진행률, 좌표 변화)이 내부적으로는 아주 미세하게 어긋난 상태로 남을 수 있고, 그 미세한 오차가 비교 연산, 누적 계산, 직렬화(서버 통신), 표시 반올림에서 실제 버그로 터집니다.
아래에서 앱 개발에서 자주 겪는 상황을 “실제 사례” 중심으로 정리해드리고, 바로 적용할 수 있는 코드 예시까지 함께 알려드리겠습니다.
결제 금액이 1원씩 어긋나는 문제
상황
장바구니에서 “정률 할인 → 쿠폰 → 세금/수수료”처럼 단계가 늘어나면, 화면에 표시한 금액과 서버/PG 정산 금액이 1원씩 차이 나는 민원이 생깁니다. 원인은 대부분 double로 금액을 계산하면서 중간 반올림 타이밍이 흔들리기 때문입니다.
잘못된 예시 (Dart/Flutter)
double subtotal = 9900.0; double discountRate = 0.15; // 15% double discounted = subtotal * (1.0 - discountRate); // 8415.0 "같아 보이지만" double fee = discounted * 0.035; // 수수료 double total = discounted + fee; // 화면 표시를 위해 반올림 int displayWon = total.round(); // 특정 케이스에서 1원 차이 발생 가능
실무에서 안전한 방식: “최소 단위 정수”로 계산
/// 원 단위 정수로만 계산한다.
int applyPercentRound(int amountWon, int ratePermyriad) {
// ratePermyriad: 만분율(0~10000), 예: 15% = 1500
// 반올림(half-up)하려면 +5000 후 /10000
return (amountWon * ratePermyriad + 5000) ~/ 10000;
}
void main() {
final int subtotalWon = 9900;
// 15% 할인 => 할인금액
final int discountWon = applyPercentRound(subtotalWon, 1500);
final int discountedWon = subtotalWon - discountWon;
// 수수료 3.5% => 만분율 350
final int feeWon = applyPercentRound(discountedWon, 350);
final int totalWon = discountedWon + feeWon;
print(totalWon); // 금액 로직이 흔들리지 않음
}
실무 포인트는 “계산 기준”은 정수로 고정하고, 마지막 출력만 포맷팅하는 것입니다. 이 방식이면 앱/서버/결제 모듈이 언어가 달라도 결과가 재현 가능해집니다.
버튼 활성화나 조건 분기가 깨지는 문제
상황
“총액이 0이면 결제 버튼 비활성화” 같은 로직을 == 비교로 구현하면, 내부 값이 0.0000000001 같은 형태로 남아 간헐 버그가 납니다. 특히 쿠폰/포인트/프로모션이 여러 번 적용되는 화면에서 잘 터집니다.
잘못된 예시
if (total == 0.0) {
disablePayButton();
}
실무 패턴 1: 금액은 정수로 비교
if (totalWon == 0) {
disablePayButton();
}
실무 패턴 2: 불가피하게 실수를 쓸 때는 “근사 비교” 사용
bool nearlyEqual(double a, double b, {double absTol = 1e-9, double relTol = 1e-9}) {
final diff = (a - b).abs();
if (diff <= absTol) return true;
return diff <= relTol * (a.abs() + b.abs()).clamp(0.0, double.infinity);
}
if (nearlyEqual(total, 0.0)) {
disablePayButton();
}
금액처럼 “최소 단위가 명확한 값”은 근사 비교를 쓰는 것 자체가 위험 신호인 경우가 많고, 이때는 설계를 정수 기반으로 되돌리는 편이 더 안전합니다.
애니메이션 진행률 1.0에 정확히 도달하지 않음
상황
진행률(progress)이 0→1로 끝나야 하는데, 프레임 누적과 부동소수점 오차가 겹치면 마지막에 0.999999로 남아서 애니메이션 종료 조건이 깨집니다. 결과적으로 페이드가 끝나지 않거나, 스냅 위치가 살짝 어긋나고, 스크롤/슬라이더가 미세하게 흔들립니다.
실무 코드: clamp + 종료 스냅
double normalizedProgress({
required Duration elapsed,
required Duration total,
}) {
if (total.inMicroseconds <= 0) return 1.0;
final p = elapsed.inMicroseconds / total.inMicroseconds;
return p.clamp(0.0, 1.0);
}
double snapToEndIfClose(double p, {double eps = 1e-6}) {
if (1.0 - p <= eps) return 1.0;
if (p <= eps) return 0.0;
return p;
}
실무에서는 “수학적으로는 도달해야 한다”가 아니라 “종료 조건을 코드로 보장한다”가 정답입니다.
지도/운동 기록에서 거리·속도가 누적되며 흔들리는 문제
상황
GPS 좌표를 초 단위로 받아 거리·속도를 계속 누적하면, 작은 오차가 축적되어 “오늘 달린 거리”가 일정 구간에서 과하게 튀거나, 반대로 덜 잡히는 현상이 생깁니다.
실무 대응
-
좌표/거리 계산은
double로 하되, 저장/전송/표시 단에서 자릿수를 강제합니다. -
누적값은 일정 간격으로 기준을 재정렬(리셋/재계산)하거나, 서버 기준 점검과 동기화합니다.
예시: 표시/저장 전에 자릿수 고정
double roundTo(double value, int decimals) {
final factor = math.pow(10, decimals).toDouble();
return (value * factor).round() / factor;
}
// 예: 위경도는 소수 6자리, km는 3자리 등 도메인 규칙 고정
final latStored = roundTo(lat, 6);
final lonStored = roundTo(lon, 6);
final distanceKmStored = roundTo(distanceKm, 3);
핵심은 “계산은 근사”라는 현실을 인정하고, 제품 요구사항에 맞는 의미 단위로 값을 고정하는 것입니다.
서버 통신(JSON)에서 값이 달라지는 문제
상황
앱과 서버가 서로 다른 언어/런타임이면 실수 파싱·포맷팅 과정에서 미세한 차이가 생깁니다. 이 차이는 정렬, 랭킹, 중복 제거, 캐시 키, 서명 검증 같은 곳에서 장애로 이어집니다.
실무 권장: 의미 단위로 보내기
-
돈: 최소 단위 정수로 보내기(원/센트)
-
비율: 만분율/퍼밀 같은 정수 스케일로 보내기
-
좌표: 자릿수 고정 문자열로 보내기
예시: 금액(원)과 할인율(만분율)을 정수로 전송
final payload = {
"subtotalWon": subtotalWon, // int
"discountRate": 1500, // 만분율 int (15% = 1500)
"feeRate": 350, // 만분율 int (3.5% = 350)
};
예시: 소수 자릿수 고정 문자열로 전송(좌표 등)
String fixed(double v, int decimals) => v.toStringAsFixed(decimals);
final payload = {
"lat": fixed(lat, 6),
"lon": fixed(lon, 6),
};
NaN/Infinity가 전파되며 화면이 깨지는 문제
상황
분모가 0이 되는 순간, 혹은 데이터 누락/비정상 입력으로 NaN/Infinity가 생기면, 그 값이 차트 축 계산·레이아웃·애니메이션에 들어가 “갑자기 화면 전체가 깨지는” 형태로 확산됩니다.
실무 방어: UI/저장/전송 전에 isFinite 체크
double safeNumber(double v, {double fallback = 0.0}) {
if (v.isNaN || v.isInfinite) return fallback;
return v;
}
// 사용 예
final safeAvg = safeNumber(sum / count, fallback: 0.0);
이 방어는 “문제가 생겼을 때만”이 아니라, 데이터가 외부 입력(API, 센서, 사용자 입력)인 모든 경계에서 적용하는 편이 안전합니다.
결론
앱 개발에서 실수는 단순한 숫자 타입이 아니라 비교, 누적, 표시, 직렬화, 예외값 전파까지 연결되는 품질 이슈의 출발점이 됩니다. 특히 금액처럼 최소 단위가 명확한 도메인은 부동소수점을 계산 기준으로 두는 순간 1원 단위 불일치, 검증 실패, 정산 차이 같은 문제가 반복될 가능성이 커지므로, 원·센트 등 최소 단위 정수로 계산 기준을 고정하는 설계가 가장 안전합니다. 반대로 좌표, 센서, 애니메이션처럼 실수를 쓸 수밖에 없는 영역은 계산과 표시·저장·전송을 분리하고 자릿수 고정, 근사 비교, 종료 스냅, 유한값 검사 같은 방어 규칙을 운영 표준으로 삼아야 재현성과 안정성을 확보할 수 있습니다. 결국 핵심은 “실수를 쓰지 말자”가 아니라, 값의 의미가 바뀌는 경계를 명확히 두고 그 경계에서 오차와 예외값이 확산되지 않도록 통제하는 것입니다.
검증 기준과 참고 자료
이 글은 2026-09-08에 아래 공식 문서와 공개 기술 문서를 기준으로 내용과 용어를 다시 검수했습니다. 제품 버전과 기기 제조사에 따라 화면 이름은 달라질 수 있으므로 실제 화면과 공식 문서를 함께 확인하세요.