본문 바로가기
행복바이러스 행복바이러스

Bubble.io 수식 처리 및 산술 연산 순서 오류 해결법|JavaScript 연동까지

읽는 시간 약 18분

Bubble.io에서 견적서, 쇼핑몰 결제, 포인트, 수수료처럼 숫자를 다루는 기능을 만들다 보면 계산 결과가 예상과 다르게 표시되는 경우가 있다. 단순한 덧셈과 곱셈인데도 합계가 달라지거나, 소수점이 길게 이어지고, 입력 직후에는 이전 값이 잠깐 보이기도 한다.

이 문제는 Bubble의 계산 기능이 부족해서라기보다 연산 순서, 데이터 형식, 반올림 시점, 빈 값 처리, 워크플로 실행 시점이 한 표현식 안에 섞일 때 주로 발생한다. 기본 수식만으로 해결할 수 있는 문제와 JavaScript를 사용해야 하는 문제를 구분하지 않고 코드를 추가하면 유지보수와 보안이 오히려 나빠질 수 있다.

이 글에서는 Bubble 수식에서 자주 발생하는 산술 연산 오류의 원인을 실제 예제로 확인하고, 기본 기능으로 수식을 안정화하는 방법부터 JavaScript를 연동하는 기준까지 단계별로 정리한다.

Bubble의 동적 수식은 어떻게 계산될까?

Bubble의 Dynamic expression은 입력값이나 데이터의 변화에 맞춰 결과를 갱신하는 실시간 표현식이다. 숫자 입력창의 값, 현재 사용자의 데이터, 검색 결과, Custom state 등을 연결하여 계산식을 만들 수 있다.

예를 들어 상품 가격이 12,000원이고 수량이 3개라면 다음과 같은 구조로 금액을 계산할 수 있다.

Input Price's value * Input Quantity's value

표현식 자체는 간단하지만 실제 앱에서는 할인율, 세금, 배송비, 쿠폰과 같은 값이 추가된다. 이때 연산자를 이어 붙이는 순서만 보고 일반 프로그래밍 언어의 괄호 계산과 완전히 같다고 단정하면 안 된다. Bubble 편집기는 앞의 결과에 연산자를 차례로 연결하는 형태이므로, 복잡한 계산은 중간 결과를 분리해 검증하는 편이 안전하다.

또한 계산 위치도 데이터 출처에 따라 달라질 수 있다. 페이지에 이미 로드된 요소 값이나 Custom state를 이용한 계산은 대체로 브라우저에서 처리되지만, Do a search for 같은 데이터베이스 검색이 포함되면 서버 작업이 개입한다. 서로 다른 출처를 한 표현식에 섞으면 결과가 표시되는 시점이나 워크로드에도 영향을 줄 수 있다.

산술 연산 순서 때문에 결과가 달라지는 대표 사례

1. 할인과 세금을 한 번에 계산한 경우

상품 금액이 100,000원이고 할인율이 10%, 부가세가 10%라고 가정해 보자. 개발자가 의도한 계산은 다음과 같을 수 있다.

(상품 금액 - 할인 금액) + 할인 후 금액의 세금

이를 숫자로 풀면 다음과 같다.

할인 금액 = 100,000 × 0.1 = 10,000
할인 후 금액 = 100,000 - 10,000 = 90,000
세금 = 90,000 × 0.1 = 9,000
최종 금액 = 99,000

그런데 할인 전 금액에 세금을 먼저 붙이거나, 할인율과 세율을 단순히 더해 한 번에 적용하면 비즈니스 규칙이 달라진다. 따라서 수식을 짧게 만드는 것보다 계산 단계를 명시하는 것이 중요하다.

Bubble에서는 다음처럼 중간값을 Custom state로 나누는 방법이 실용적이다.

state_subtotal = 가격 × 수량
state_discount = state_subtotal × 할인율 ÷ 100
state_taxable = state_subtotal - state_discount
state_tax = state_taxable × 세율 ÷ 100
state_total = state_taxable + state_tax + 배송비

이렇게 구성하면 어느 단계에서 값이 틀렸는지 바로 확인할 수 있고, 할인 정책이 변경되어도 해당 단계만 수정하면 된다.

2. 퍼센트 값을 그대로 곱한 경우

사용자가 할인율 입력창에 15를 입력했을 때 이를 가격에 그대로 곱하면 15%가 아니라 15배가 된다.

잘못된 개념: 가격 × 15
올바른 개념: 가격 × (15 ÷ 100)

입력값을 0.15로 받을 것인지 15로 받을 것인지 먼저 정하고, 데이터베이스 필드명에도 단위를 드러내는 것이 좋다.

discount_rate_decimal = 0.15
discount_percent = 15

같은 앱에서 두 형식을 혼용하면 계산 오류를 찾기 어렵다. 화면에는 15%로 보여주더라도 내부에서는 0.15로 통일하는 방식이 관리하기 편하다.

3. 빈 입력값이 계산에 포함된 경우

페이지가 처음 열렸을 때 수량 입력창이 비어 있으면 계산 결과도 비어 있거나 후속 조건이 실행되지 않을 수 있다. 빈 값과 숫자 0은 의미가 다르기 때문이다.

다음 두 가지 방식 가운데 앱 정책에 맞는 방법을 선택한다.

  • 입력창의 기본값을 0 또는 1로 지정한다.
  • 입력값이 비어 있지 않을 때만 계산 워크플로를 실행한다.

주문 수량처럼 0이 허용되지 않는 항목을 무조건 0으로 바꾸는 것은 적절하지 않다. 이런 경우에는 계산 전에 수량이 비어 있지 않음과 수량이 1 이상이라는 조건을 함께 검사해야 한다.

4. 텍스트와 숫자가 섞인 경우

API 응답, URL 파라미터 또는 텍스트 입력에서 가져온 값은 화면상 숫자로 보여도 실제 형식이 text일 수 있다. 쉼표가 들어간 12,500, 통화 기호가 붙은 ₩12,500, 공백이 포함된 값은 산술 계산에 바로 쓰기 어렵다.

가장 좋은 해결책은 데이터를 받는 단계에서 Number 형식으로 정의하는 것이다. 외부 데이터 때문에 텍스트를 숫자로 변환해야 한다면 다음 순서로 정규화한다.

  1. 통화 기호와 불필요한 공백을 제거한다.
  2. 천 단위 구분 쉼표를 제거한다.
  3. 숫자 형식으로 변환한다.
  4. 변환에 실패한 경우의 기본 처리 또는 오류 메시지를 정한다.

텍스트를 임의로 0으로 바꾸면 데이터 오류가 숨겨질 수 있다. 결제와 정산 기능에서는 변환 실패 시 저장을 중단하고 사용자에게 값을 다시 확인하도록 안내하는 것이 안전하다.

소수점 오차는 왜 생길까?

JavaScript를 포함한 많은 시스템은 소수를 이진 부동소수점 방식으로 처리한다. 그래서 다음처럼 사람이 기대한 값과 미세하게 다른 결과가 나올 수 있다.

0.1 + 0.2 // 0.30000000000000004

화면에서 소수점 자릿수만 줄여 표시하면 보기에는 정상이어도, 데이터베이스에 긴 소수가 저장되어 이후 합계나 비교 조건에 영향을 줄 수 있다. 따라서 표시 형식 지정과 실제 저장값 반올림을 구분해야 한다.

예를 들어 원화 금액은 다음 원칙을 사용할 수 있다.

원화 최종 결제액: 소수점 0자리에서 반올림
할인율: 계산 중 충분한 자릿수 유지
세금: 사업 규칙에 맞춰 절사 또는 반올림

금액 계산의 정확도가 특히 중요하다면 가장 작은 화폐 단위를 정수로 저장하는 방법도 고려할 수 있다. 달러는 센트 단위, 원화는 원 단위 정수로 계산하면 불필요한 소수 오차를 줄일 수 있다.

Bubble 기본 기능으로 먼저 해결하는 방법

JavaScript를 추가하기 전에 다음 순서로 점검하면 대부분의 계산 문제를 해결할 수 있다.

계산식을 작은 단위로 분리하기

긴 표현식 하나보다 소계, 할인액, 과세표준, 세금, 최종 금액으로 나눈다. 화면 계산만 필요하면 Custom state를 활용하고, 서버에서 재사용해야 하는 값이라면 데이터 필드나 Backend workflow의 단계별 결과를 검토한다.

데이터 형식을 통일하기

가격, 수량, 비율 필드는 Number로 만들고 숫자를 Text 필드에 저장하지 않는다. API Connector에서도 응답 필드의 감지 형식을 확인한다.

반올림 시점을 한 곳으로 정하기

각 항목을 먼저 반올림한 뒤 더한 값과 전체 합계를 마지막에 한 번 반올림한 값은 다를 수 있다. 세금계산서, 포인트, 수수료 규칙에 맞춰 반올림 시점을 문서화하고 프런트엔드와 백엔드에서 동일하게 적용한다.

화면 계산과 최종 저장 계산을 분리하기

사용자에게 즉시 보여주는 예상 금액은 클라이언트에서 계산할 수 있다. 그러나 결제액, 잔액, 포인트 차감처럼 조작되면 안 되는 값은 서버 측 워크플로에서 다시 계산해야 한다. 브라우저의 Custom state나 JavaScript 결과만 믿고 최종 금액을 저장해서는 안 된다.

JavaScript 연동이 필요한 경우

다음과 같은 상황에서는 JavaScript가 유용할 수 있다.

  • 여러 단계의 수학 함수를 반복 적용해야 할 때
  • 복잡한 배열 계산이나 사용자 정의 통계 로직이 필요할 때
  • 외부 JavaScript 라이브러리의 기능을 사용해야 할 때
  • 같은 계산 규칙을 하나의 함수로 묶어 재사용해야 할 때
  • 숫자 정규화와 예외 처리가 Bubble 표현식만으로 지나치게 복잡해질 때

Bubble 공식 문서에서는 HTML 요소 또는 Custom plugin을 통해 JavaScript를 사용할 수 있다고 안내한다. 간단한 테스트나 소규모 화면 계산에는 JavaScript 실행 기능을 제공하는 플러그인을 이용할 수 있지만, 핵심 비즈니스 로직이라면 출처와 업데이트 상태가 불명확한 플러그인에 전부 의존하지 않는 편이 좋다.

JavaScript로 계산 함수를 만드는 예제

아래 예제는 가격, 수량, 할인율, 세율, 배송비를 전달받아 계산 결과를 객체로 반환한다.

function calculateOrder(price, quantity, discountPercent, taxPercent, shipping) {
  const values = [price, quantity, discountPercent, taxPercent, shipping]
    .map(Number);

  if (values.some((value) => !Number.isFinite(value))) {
    throw new Error('계산 항목에 올바르지 않은 숫자가 포함되어 있습니다.');
  }

  const [p, q, discountRate, taxRate, deliveryFee] = values;

  if (p < 0 || q < 1 || discountRate < 0 || discountRate > 100) {
    throw new Error('가격, 수량 또는 할인율의 범위를 확인해 주세요.');
  }

  const subtotal = p * q;
  const discount = subtotal * (discountRate / 100);
  const taxableAmount = subtotal - discount;
  const tax = taxableAmount * (taxRate / 100);
  const total = taxableAmount + tax + deliveryFee;

  return {
    subtotal: Math.round(subtotal),
    discount: Math.round(discount),
    tax: Math.round(tax),
    total: Math.round(total)
  };
}

이 함수의 핵심은 계산 자체보다 검증 과정에 있다. Number()로 입력값을 숫자로 변환하고, Number.isFinite()로 NaN이나 무한대가 들어오는 것을 막는다. 가격과 수량, 할인율의 허용 범위도 계산 전에 검사한다.

Bubble에 결과를 돌려줄 때는 사용하는 플러그인 또는 Custom plugin의 반환 방식에 맞춰 각 값을 Number 타입으로 전달한다. JSON 문자열 전체를 넘긴다면 Bubble에서 다시 파싱하는 구조가 필요하므로, 총액 하나만 필요할 때는 숫자 하나를 반환하는 방식이 더 단순하다.

Toolbox 방식으로 연동할 때의 흐름

Toolbox와 같은 플러그인의 Run javascript와 Javascript to Bubble 요소를 활용한다면 일반적인 연결 흐름은 다음과 같다.

  1. 페이지에 JavaScript 결과를 받을 요소를 추가한다.
  2. 결과 타입을 Number로 맞춘다.
  3. 가격이나 수량이 변경될 때 JavaScript 실행 워크플로를 호출한다.
  4. 계산 결과를 수신한 뒤 Custom state 또는 화면 요소에 반영한다.
  5. 최종 저장 전에는 Backend workflow에서 신뢰 가능한 원본 데이터로 다시 계산한다.

요소 이름은 고정된 규칙으로 관리하는 것이 좋다. 예를 들어 js_total_result, state_order_total처럼 역할이 드러나는 이름을 사용하면 페이지가 복잡해져도 데이터 흐름을 추적하기 쉽다.

클라이언트 JavaScript를 결제 계산에 그대로 쓰면 위험한 이유

브라우저에서 실행되는 JavaScript는 사용자가 개발자 도구로 확인하거나 조작할 수 있다. 따라서 다음 값의 최종 결정권을 클라이언트에 두면 안 된다.

  • 실제 결제 금액
  • 쿠폰 사용 가능 여부
  • 회원 등급별 할인율
  • 포인트 적립 및 차감액
  • 판매자 정산 금액
  • 접근 권한과 유료 기능 활성화 여부

안전한 구조는 브라우저에서 예상 금액을 빠르게 표시하되, 주문 확정 시 서버가 데이터베이스의 가격과 할인 정책을 다시 불러와 최종 금액을 계산하는 방식이다. 클라이언트가 보낸 총액은 참고값으로만 취급하고 서버 계산 결과와 다르면 결제를 중단해야 한다.

계산 오류를 찾는 디버깅 순서

결과가 맞지 않을 때 처음부터 수식 전체를 바꾸기보다 아래 순서로 범위를 좁힌다.

  1. 가격, 수량, 할인율 등 원본 값을 각각 Text 요소에 임시로 표시한다.
  2. 각 값의 데이터 타입이 Number인지 확인한다.
  3. 빈 값, 음수, 0, 매우 큰 수를 넣어 본다.
  4. 소계부터 최종 금액까지 중간 결과를 하나씩 표시한다.
  5. 반올림 전 값과 반올림 후 값을 비교한다.
  6. 입력 변경 직후 워크플로가 중복 실행되는지 확인한다.
  7. 데이터베이스 검색이 수식 내부에서 반복되는지 살펴본다.
  8. 서버에 저장하기 직전 값과 저장된 값을 비교한다.

Bubble의 Debug mode에서 Step-by-step을 사용하면 워크플로의 각 단계와 당시 값을 확인하는 데 도움이 된다. 단, 실제 사용자 정보나 결제 데이터가 화면 또는 로그에 과도하게 노출되지 않도록 테스트 데이터로 점검하는 것이 좋다.

성능을 떨어뜨리지 않는 계산 구조

산술 계산 자체는 대체로 가볍지만, 수식 안에 데이터베이스 검색을 여러 번 넣으면 문제가 달라진다. 같은 상품의 가격이나 할인율을 반복 검색하는 대신 한 번 가져온 데이터를 Parent group’s thing, Current cell’s thing 또는 Custom state로 재사용하는 것이 효율적이다.

특히 Repeating Group의 각 셀에서 별도 검색과 복잡한 계산을 실행하면 데이터 수가 늘수록 워크로드가 커질 수 있다. 서버가 처리해야 하는 검색과 브라우저가 처리할 단순 계산을 구분하고, 목록에 필요한 필드만 가져오는 구조가 좋다.

JavaScript도 무조건 빠른 해결책은 아니다. 입력할 때마다 긴 코드를 실행하거나 Bubble과 JavaScript 사이에서 값을 여러 번 주고받으면 화면 갱신이 불안정해질 수 있다. 실시간 계산에는 짧은 지연을 두는 디바운스 방식이나 계산 버튼을 사용하는 것도 방법이다.

배포 전 테스트 체크리스트

  • 가격과 수량 필드가 Number 타입인가?
  • 할인율을 15와 0.15 중 어떤 형식으로 저장할지 통일했는가?
  • 빈 값, 0, 음수에 대한 처리 규칙이 있는가?
  • 반올림, 올림, 버림의 시점이 명확한가?
  • 중간 계산값을 단계별로 확인할 수 있는가?
  • 표시용 금액과 실제 저장 금액의 계산 규칙이 같은가?
  • 클라이언트 계산 결과를 서버에서 다시 검증하는가?
  • JavaScript 변환 실패와 예외 상황을 처리하는가?
  • 동일한 데이터베이스 검색을 수식 안에서 반복하지 않는가?
  • 모바일과 데스크톱에서 입력 변경 시 결과가 동일한가?

자주 묻는 질문

Bubble에서 괄호를 사용해 연산 순서를 지정하면 되나요?

복잡한 수식을 한 줄에서 해결하려 하기보다 중간값을 Custom state나 워크플로 단계로 분리하는 편이 명확하다. 괄호와 동일한 계산 의도를 만들더라도 단계별 결과가 보여야 오류를 빠르게 찾고 정책 변경에도 대응하기 쉽다.

화면에 표시된 값만 반올림하면 충분한가요?

아니다. 표시 형식만 바꾸면 내부 값은 긴 소수로 남을 수 있다. 비교, 저장, 합산에 사용할 값이라면 비즈니스 규칙에 맞는 시점에 실제 계산값도 반올림해야 한다.

모든 계산을 JavaScript로 옮기면 더 빠른가요?

반드시 그렇지는 않다. 단순한 합계와 비율 계산은 Bubble 기본 표현식이 관리하기 쉽다. JavaScript는 복잡한 로직이나 외부 라이브러리가 필요한 경우에 제한적으로 사용하고, 보안이 필요한 최종 계산은 서버에서 검증해야 한다.

Custom state에 계산값을 저장해도 되나요?

화면 표시와 임시 계산에는 적합하다. 하지만 Custom state는 브라우저의 임시 상태이므로 주문 금액이나 권한 같은 신뢰 기준으로 사용해서는 안 된다. 중요한 값은 서버에서 재계산한 뒤 데이터베이스에 저장한다.

마무리

Bubble.io의 산술 연산 오류는 대부분 복잡한 수식 자체보다 데이터 형식과 계산 순서, 반올림 시점, 실행 위치를 명확히 구분하지 않아 발생한다. 먼저 입력값을 Number로 통일하고, 긴 수식을 작은 단계로 나누며, 각 중간 결과를 확인해야 한다.

기본 표현식으로 관리하기 어려운 계산만 JavaScript 함수로 분리하면 코드 의존성을 줄이면서 확장성을 확보할 수 있다. 다만 브라우저에서 계산된 값은 조작 가능하므로 결제, 포인트, 권한처럼 중요한 결과는 반드시 서버에서 다시 검증해야 한다. 이 원칙만 지켜도 계산 정확도와 앱의 보안, 유지보수성을 함께 높일 수 있다.

youngji
함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.