환율계산과 노선비교, USD로 고지하는 항공사를 100원 단위로 환산하며 실시간 환율 API 대신 고정 환율을 택한 이유와 그 구현을 정리한 개발기입니다.
통화가 섞인 표를 한 줄로 세우려면
유류할증료(YQ) 조회 앱을 만들면서 가장 먼저 부딪힌 벽은 기술이 아니라 통화였습니다. 앱이 다루는 8개 항공사 중 네 곳(제주항공·진에어·에어부산·이스타항공)은 유류할증료를 USD로 고지하고, 나머지 네 곳(대한항공·아시아나항공·티웨이항공·에어서울)은 원화로 고지하죠. 항공사별 상세 화면에서는 이게 큰 문제가 아니었습니다. 제주항공 화면에는 달러를, 대한항공 화면에는 원화를 원문 그대로 보여주면 되니까요.
문제는 '노선비교'였습니다. 사용자가 도쿄를 고르면, 그 노선에 취항하는 모든 항공사의 유류할증료를 싼 순서대로 줄세워 보여줘야 합니다. 그런데 어떤 항공사는 38달러, 어떤 항공사는 62,400원이라면, 둘 중 무엇이 더 싼지 한눈에 비교할 방법이 없죠. 서로 다른 통화를 같은 자 위에 올려놓지 않으면 '줄세우기'라는 기능 자체가 성립하지 않습니다. 실제로 노선비교의 정렬 코드는 통화를 가리지 않고 하나의 원화 숫자(onewayKrw)만 기준으로 삼습니다.
return results.sort((x, y) => {
if (x.onewayKrw == null && y.onewayKrw == null) return 0;
if (x.onewayKrw == null) return 1;
if (y.onewayKrw == null) return -1;
return x.onewayKrw - y.onewayKrw;
});
정렬 키가 원화 하나뿐이니, USD 항공사도 비교 전에 반드시 원화 값을 갖고 있어야 합니다. 그래서 getRouteFares는 USD 항공사의 원문 금액(raw)을 usdToKrw(raw)로 바꿔 onewayKrw에 담고, 원문 달러는 onewayUsd에 따로 보관해 두죠. 여기서 자연스럽게 다음 질문이 나왔습니다. 그 환율을 어디서 가져올 것인가. 실시간 환율 API를 붙일 것인가, 아니면 고정 환율을 박아 둘 것인가.
저는 고정 환율을 택했습니다. 실시간 환율이 '더 정확해 보인다'는 건 대체로 착시라고 판단했기 때문이죠. 유류할증료 자체가 발권 시점에 항공사가 최종 고지하는 값이라 애초에 참고 지표에 가깝고, 사용자가 실제로 청구받는 원화는 우리가 조회 순간 부른 환율이 아니라 결제일에 카드사·항공사가 적용하는 환율로 정해집니다. 즉 우리가 아무리 실시간 환율을 끌어와도 '실제 청구액'과는 여전히 다른 숫자죠. 여기에 실시간 API는 호출 실패·캐싱·타임아웃·표시 지연 같은 복잡성까지 얹습니다. 정확성은 착시인데 실패 지점만 늘어나는 셈이라, "정직하게 하나의 기준 환율을 박고, 그 기준을 명시하자"는 쪽이 훨씬 낫다고 봤습니다.
usdToKrw, 100원 단위로 반올림한 이유
그래서 환율은 개발 시점에 open.er-api.com에서 한 번 받아 온 값을 상수로 고정했습니다. 코드에는 환율과 그 기준일을 나란히 남겨 뒀죠.
// USD → 원화 환산 (개발 시점 환율, open.er-api.com 기준)
export const USD_KRW_RATE = 1508.29;
export const RATE_DATE_LABEL = "2026.07.10";
// USD 금액을 원화(100원 단위 반올림)로 환산한 숫자
export function usdToKrw(usd: number): number {
return Math.round((usd * USD_KRW_RATE) / 100) * 100;
}
export function formatAmount(value: number, unit: CurrencyUnit): string {
return unit === "KRW"
? `${value.toLocaleString("ko-KR")}원`
: `USD ${value.toLocaleString("en-US")}`;
}
여기서 눈여겨볼 부분은 Math.round(... / 100) * 100, 100원 단위 반올림입니다. 처음엔 그냥 Math.round(usd * USD_KRW_RATE)로 1원 단위까지 계산하려 했었죠. 하지만 곱셈 결과를 실제로 찍어 보니 생각이 바뀌었습니다. 30달러는 30 × 1508.29 = 45,248.7이라 1원 단위로 반올림하면 45,249원이 나옵니다. 이 45,249원이라는 숫자는 정밀해 보이지만, 그 정밀함이 가짜라는 게 문제였습니다. 애초에 기준 환율 자체가 특정 날짜에 고정된 참고값인데, 끝자리 9원까지 보여 주면 마치 그만큼 정확한 청구액인 양 착각을 부르니까요.
무엇보다 노선비교 화면은 원화 항공사와 USD 항공사가 한 줄에 섞입니다. 원화로 고지하는 항공사들은 표 자체가 62,400원, 59,200원처럼 딱 떨어지는 금액인데, 그 사이에 45,249원 같은 환산값이 끼면 유독 지저분하게 튀죠. 100원 단위로 반올림하면 45,200원이 되어, 옆줄의 원화 금액들과 시각적으로도 자연스럽게 어울립니다. 정밀함을 일부러 100원 단위까지 '내려서', 이 숫자는 정확한 청구액이 아니라 비교용 근사값이라는 성격을 화면 스스로 드러내게 한 셈이죠.
실제 도쿄 노선비교가 이 설계를 그대로 보여 줍니다. 에어부산은 원문 30달러가 약 45,200원으로, 진에어 37달러는 약 55,800원, 제주항공과 이스타항공의 38달러는 약 57,300원으로 환산되어, 원화로 고지하는 티웨이항공 59,200원·대한항공 62,400원과 한 줄에 나란히 줄을 섭니다.

통화별 렌더 분기와 정직한 각주
숫자를 원화로 통일했다고 해서, 화면까지 원화만 보여 주면 그건 오히려 불친절합니다. 제주항공이 실제로 고지하는 값은 어디까지나 38달러이지 57,300원이 아니니까요. 그래서 렌더링은 통화에 따라 분기합니다. 원화 항공사는 금액만 담백하게 찍지만, USD 항공사는 세 가지를 덧붙이죠. 환산값 앞에 '약 '을 붙여 근사값임을 알리고, 원문 달러(USD 30)를 함께 병기하며, 화면 아래에 환율 기준을 밝히는 각주를 답니다.
노선비교 화면의 분기는 이렇게 생겼습니다. formatAmount는 언제나 원화로 찍되, USD 항공사일 때만 '약 ' 접두를 붙이고 원문 달러를 부제로 병기합니다.
<span className="cmp-fare">
{f.airline.unit === "USD" ? "약 " : ""}
{formatAmount(krw, "KRW")}
</span>
{f.onewayUsd != null && (
<span className="cmp-usd">
USD {f.onewayUsd.toLocaleString("en-US")}
</span>
)}
가장 신경 쓴 건 각주의 문장이었습니다. 고정 환율을 택한 이상, 그 선택을 숨기지 않고 정직하게 드러내는 문장이 이 설계의 마지막 조각이라고 봤거든요. 그래서 노선비교에는 "USD로 고지하는 항공사는 2026.07.10 환율(1 USD = 1,508.29원) 기준 환산값이에요"라고 기준일과 환율을 그대로 밝혔습니다. 항공사 상세 화면에서는 한 걸음 더 나아가, "원화 금액은 2026.07.10 환율로 환산한 값이라, 실제 청구액은 결제일 환율에 따라 달라져요"라고 한계까지 못 박았죠.
이 각주는 사족이 아니라, 실시간 환율을 포기한 대가를 사용자에게 되돌려 주는 장치입니다. 실시간 API를 붙였다면 "지금 이 순간의 정확한 환율"이라는 인상을 주지만 결제일 환율과 어긋나는 위험은 그대로 남습니다. 반대로 고정 환율은 애초에 "이 값은 특정 날짜 기준의 참고값"이라고 화면이 스스로 고백하니, 기대와 현실의 간극이 작죠. 개인적으로는, 조회용 앱에서 숫자의 정밀도보다 중요한 건 그 숫자가 무엇을 뜻하는지 오해 없이 전달하는 일이라고 생각합니다. usdToKrw의 100원 단위 반올림과 이 정직한 각주는 결국 같은 목적을 향하는 한 쌍이었던 셈이죠.