Guide/환율 계산기 아키텍처

환율 계산기 앱은 환율을 어떻게 최신 상태로 유지할까?

환율 계산기는 전체 환율 관측값을 저장하고 정해진 주기로 갱신하며, 요청 실패나 시장 휴장 중에도 원래 관측 시각을 유지해 데이터 상태를 정확히 전달한다.

ERexchangerate.dev·Aug 23, 2026·8분 분량

신뢰할 수 있는 계산기는 화면을 그릴 때마다 숫자 하나를 다시 요청하지 않는다. 환율을 source, market_session, data_updated_at과 함께 저장하고 잠시 재사용한 뒤, 갱신에 성공했을 때만 교체한다. 응답 시각은 API가 답한 때이고 관측 시각은 환율 자체가 바뀐 때다.

Key points
rates, sources, market_session, data_updated_at을 포함한 전체 관측값을 캐시한다.
데이터 나이는 data_updated_at으로 판단한다. timestamp는 응답 생성 시각이다.
활발한 환율은 거래 주간 중 장중 갱신되고 일일 기준환율은 자체 발표 일정에 따른다.
주말이나 휴장 중에는 마지막 정상 관측값과 원래 시각을 유지한다.
갱신 실패 시 마지막 정상값을 시각과 함께 표시하고, 정상값이 없으면 오류를 반환한다.

응답 전체를 하나의 관측값으로 다루기

메타데이터가 없는 환율은 의미가 불분명하다. 장중 현물, 일일 기준값, 다른 통화를 거쳐 계산한 교차환율일 수 있다. rates.EUR만 꺼내지 말고 객체 전체를 저장한다. API 레퍼런스는 응답 필드를 정의하고, 방법론은 출처와 시장 상태 라벨이 정해지는 방식을 설명한다.

json · abbreviated latest responsecopy
{
  "base": "USD",
  "source": "live",
  "market_session": "open",
  "timestamp": "2026-08-23T09:42:06Z",
  "data_updated_at": "2026-08-23T09:41:00Z",
  "rates": { "EUR": 0.86124, "GBP": 0.74281 },
  "sources": { "EUR": "live", "GBP": "live" },
  "derived_symbols": []
}

source는 여러 통화 응답에서 가장 덜 최신인 데이터 등급을 요약한다. 통화별 상태는 sources를 읽고, derived_symbols로 중간 통화를 거쳐 계산된 환율을 확인한다.

두 시각은 의미가 다르다
timestamp는 API가 응답을 만든 시각이고 data_updated_at은 원본 관측값이 기록된 시각이다. 최신 여부와 화면 표시는 후자를 기준으로 한다.

타이머보다 먼저 갱신 정책 정하기

모든 제품에 맞는 간격은 없다. 쇼핑몰의 대략적 표시값은 몇 분간 공유 캐시할 수 있고, 운영 대시보드는 보이는 동안 짧게 갱신할 수 있으며, 일일 보고서는 필요한 고시 이후 한 번 가져올 수 있다. 렌더링마다 요청해도 같은 관측값이 돌아올 수 있다.

용도앱 정책표시할 내용
쇼핑몰 예상가공유 캐시, 몇 분마다 갱신통화, 참고용 표시, 관측 시각
운영 대시보드짧은 캐시, 보이는 동안 갱신환율, 출처, 시장 상태, 시각
일일 보고서필요한 고시 이후 한 번환율과 저장된 관측 시각
결제 금액참고 환율을 사용하지 않음결제 또는 거래 제공자 결과

market_sessionopen, weekend, interbank_closed 중 하나다. 활발한 통화는 거래 주간에 움직이고 일일 기준 시계열은 발표 일정에 따라 갱신된다.

서버에 작은 캐시 만들기

통화 코드를 정렬해 안정적인 키를 만들고 성공한 응답만 저장한다. 동시에 들어온 갱신은 하나로 합치며 인증 요청과 API 키는 서버에 둔다.

typescript · latest-rates.tscopy
type RateSource = "live" | "ecb_daily" | "fred_daily";

type LatestRates = {
  base: string;
  source: RateSource;
  sources: Record<string, RateSource>;
  market_session: "open" | "weekend" | "interbank_closed";
  timestamp: string;
  data_updated_at: string;
  rates: Record<string, number>;
  derived_symbols: string[];
};

type CacheEntry = {
  value: LatestRates;
  fetchedAt: number;
};

const cache = new Map<string, CacheEntry>();
const pending = new Map<string, Promise<LatestRates>>();

function cacheKey(base: string, symbols: string[]): string {
  return `${base}:${[...symbols].sort().join(",")}`;
}

async function fetchLatest(base: string, symbols: string[]): Promise<LatestRates> {
  const url = new URL(`https://api.exchangerate.dev/v1/latest/${base}`);
  url.searchParams.set("symbols", [...symbols].sort().join(","));

  const response = await fetch(url, {
    headers: process.env.EXCHANGERATE_API_KEY
      ? { Authorization: `Bearer ${process.env.EXCHANGERATE_API_KEY}` }
      : {},
    signal: AbortSignal.timeout(8000),
  });

  if (!response.ok) {
    throw new Error(`FX refresh failed: ${response.status}`);
  }

  return response.json() as Promise<LatestRates>;
}

export async function getLatestRates(
  base: string,
  symbols: string[],
  maxAgeMs = 60_000,
): Promise<LatestRates> {
  const key = cacheKey(base, symbols);
  const existing = cache.get(key);

  if (existing && Date.now() - existing.fetchedAt < maxAgeMs) {
    return existing.value;
  }

  const inFlight = pending.get(key);
  if (inFlight) return inFlight;

  const refresh = fetchLatest(base, symbols)
    .then((value) => {
      cache.set(key, { value, fetchedAt: Date.now() });
      return value;
    })
    .finally(() => pending.delete(key));

  pending.set(key, refresh);
  return refresh;
}

인스턴스가 여러 개라면 공유 저장소나 프레임워크 캐시를 사용한다. 값과 관측 메타데이터를 함께 저장하고 성공 시에만 교체한다는 원칙은 같다.

갱신 실패는 새 환율이 아니다

타임아웃, 429, 상류 오류는 시장 관측값이 아니다. 마지막 정상값을 허용하는 기능이라면 원래 data_updated_at과 대체 상태를 표시한다. 정상값이 없으면 오류를 반환한다. 429에서는 Retry-After를 따르고 일시 오류에는 무작위 지연을 포함한 지수 백오프를 쓴다. 401은 설정을 고쳐야 한다.

typescript · explicit stale fallbackcopy
type ConverterResult =
  | { status: "fresh"; data: LatestRates }
  | { status: "last_known_good"; data: LatestRates; reason: string };

async function getConverterRates(
  base: string,
  symbols: string[],
): Promise<ConverterResult> {
  const key = cacheKey(base, symbols);

  try {
    return { status: "fresh", data: await getLatestRates(base, symbols) };
  } catch (error) {
    const previous = cache.get(key);
    if (!previous) throw error;

    return {
      status: "last_known_good",
      data: previous.value,
      reason: error instanceof Error ? error.message : "Refresh failed",
    };
  }
}

429 응답에서는 Retry-After가 있으면 따르고 모든 인스턴스가 동시에 재시도하지 않게 한다. 일시 장애에는 무작위 지연을 포함한 지수 백오프가 적절하다. 인증 오류는 재시도가 아니라 설정 수정이 필요하다.

시장 휴장은 정상 상태다

market_session: weekend 응답이 거래 주간 마지막 관측값을 담는 것은 정상이다. 새 HTTP 응답 시각으로 관측 시각을 덮어쓰거나 주말 환율을 만들어 내지 않는다.

시장 휴장 중에는 앱 캐시 시간을 늘릴 수 있지만 거래 주간이 재개되면 다시 갱신해야 한다. 모든 통화가 한 일정에 따른다고 가정하지 말고 반환된 시장 상태와 제품 요구사항으로 결정한다.

환산 후 한 번만 반올림하기

기준 통화가 USD라면 25 USD를 EUR로 바꾸는 계산은 25 × rates.EUR이다. 환율 정밀도를 유지하고 최종 금액만 대상 통화 규칙에 따라 반올림한다.

참고용이며 체결가가 아니다
계산기 환율은 표시, 예상, 분석에 적합하다. 실제 청구액은 스프레드, 수수료, 반올림, 실행 시각을 포함한 결제 또는 거래 제공자의 값을 사용한다.

출시 전 확인 항목

  • 화면에 필요한 통화만 요청한다.
  • 기준 통화와 정렬한 심볼로 캐시 키를 만든다.
  • 성공한 전체 응답만 저장하고 HTTP 오류는 저장하지 않는다.
  • 동시 갱신 요청을 중복 실행하지 않는다.
  • 데이터 나이가 중요하면 data_updated_at을 표시한다.
  • sources, market_session, derived_symbols를 보존한다.
  • API 키는 서버에 둔다.
  • 마지막 정상값에는 원래 시각과 명확한 상태를 표시한다.
ER
exchangerate.dev
FX 데이터로 개발하는 사람을 위한 아키텍처와 연동 가이드.

계속 읽기

Referencesource와 market_session 읽기읽기 TutorialNext.js와 TypeScript의 환율읽기 Guide참고 환율과 체결 환율읽기
더 많은 비교Fixer vs exchangerate.devOpen Exchange Rates vs exchangerate.devCurrencylayer vs exchangerate.dev
학습Reading source and market_session in your pipelineIndicative vs executable FX rates: what a rates API actually gives youECB 기준환율 설명
실시간 환율EUR/USDGBP/USDUSD/JPY