블로그로 돌아가기

Vue.js와 실시간 환율 API로 환율 변환기 만들기 (2026 가이드)

V
Vlado Grigirov
April 28, 2026
Currency API Vue.js Tutorial Exchange Rates JavaScript Finexly Composition API

실시간 API에서 환율을 가져오는 Vue 환율 변환기가 필요하다면, 이 튜토리얼이 처음부터 끝까지 안내합니다. Composition API, TypeScript, 무료 환율 API를 사용해 프로덕션에 바로 투입할 수 있는 Vue 3 컴포넌트를 만듭니다 — 적절한 디바운싱, 캐싱, 에러 처리, Nuxt용 SSR-안전 패턴 포함.

마지막에는 재사용 가능한 <CurrencyConverter /> 컴포넌트, 어떤 Vue 3 프로젝트에도 넣을 수 있는 useCurrencyRates 컴포저블, 그리고 실제 사용자에게 통화 변환을 배포할 때 중요한 트레이드오프에 대한 명확한 이해가 남습니다.

무엇을 만드는가

다음 기능을 갖춘 Vue 3 환율 변환기:

  • Finexly API를 통한 170개 이상 통화의 실시간 환율
  • 사용자가 입력하는 동안 반응형으로 갱신되는 변환
  • 통화 교체 버튼(USD → EUR이 한 번 클릭으로 EUR → USD가 됨)
  • 네트워크를 낭비하지 않는 디바운스된 API 호출
  • 지연 시간을 낮게 유지하고 무료 할당량을 지키는 인메모리 캐시
  • 에러 처리, 로딩 상태, 깔끔한 TypeScript 타입
  • Nuxt 3에서 hydration 불일치 없이 동작하는 SSR-친화적

최종 컴포넌트는 약 80줄. 컴포저블은 60줄 더. 그게 전부입니다.

사전 요구 사항

다음에 익숙해야 합니다:

  • Vue 3 문법 (<script setup> 형태)
  • 기본 TypeScript
  • fetch로 REST API 호출

Finexly API 키도 필요합니다. 대시보드에서 받으세요 — 약 30초 걸리고 무료 플랜은 신용카드 없이 월 1,000회 요청을 줍니다. 서비스를 처음 쓴다면 Finexly API 문서에 5분짜리 퀵스타트가 있습니다.

1단계: Vite로 Vue 3 프로젝트 세팅

처음 시작한다면:

npm create vite@latest finexly-converter -- --template vue-ts
cd finexly-converter
npm install
npm run dev

Vite는 Vue 3, TypeScript, hot module reload를 즉시 제공합니다. src/App.vue를 열어 보일러플레이트를 비우세요 — 곧 변환기로 교체합니다.

기존 Nuxt 3 프로젝트에 통합한다면 이 단계를 건너뛸 수 있습니다. 아래 컴포저블은 vue의 표준 refcomputed를 사용하므로 Nuxt에서도 동일하게 동작합니다.

2단계: API 키를 안전하게 보관

Finexly API 키를 컴포넌트에 직접 붙이지 마세요. .env.local에 둡니다:

VITE_FINEXLY_API_KEY=your_key_here

Vite는 VITE_ 접두사가 붙은 변수를 클라이언트에 노출합니다. Nuxt에서는 NUXT_PUBLIC_FINEXLY_API_KEY를 사용하고 useRuntimeConfig().public.finexlyApiKey에서 읽습니다.

클라이언트 번들에 키가 노출되는 게 걱정되면 백엔드 라우트나 서버리스 함수로 요청을 프록시하세요. 그 패턴은 7단계에서 보여줍니다.

3단계: useCurrencyRates 컴포저블 만들기

컴포저블은 변환기의 핵심입니다. fetch, 캐시, 로딩/에러 상태를 책임지고 — 컴포넌트는 표시 전용으로 유지합니다.

src/composables/useCurrencyRates.ts 생성:

import { ref, type Ref } from 'vue'

const API_KEY = import.meta.env.VITE_FINEXLY_API_KEY
const BASE_URL = 'https://api.finexly.com/v1'

// 모듈 레벨 캐시: 이 컴포저블을 호출하는 모든 컴포넌트 사이에서 공유.
// 각 항목은 5분 — 즉각적으로 느껴질 만큼 길고, 변동성 큰 FX 움직임에도
// 정확함을 유지할 만큼 짧다.
const cache = new Map<string, { rates: Record<string, number>; expires: number }>()
const TTL_MS = 5 * 60 * 1000

interface RatesResponse {
  base: string
  date: string
  rates: Record<string, number>
}

export function useCurrencyRates() {
  const rates: Ref<Record<string, number>> = ref({})
  const loading = ref(false)
  const error = ref<string | null>(null)

  async function fetchRates(base: string) {
    const cached = cache.get(base)
    if (cached && cached.expires > Date.now()) {
      rates.value = cached.rates
      return
    }

    loading.value = true
    error.value = null
    try {
      const url = `${BASE_URL}/latest?base=${base}&apikey=${API_KEY}`
      const res = await fetch(url)
      if (!res.ok) throw new Error(`Finexly returned ${res.status}`)
      const data: RatesResponse = await res.json()

      rates.value = data.rates
      cache.set(base, { rates: data.rates, expires: Date.now() + TTL_MS })
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Failed to load rates'
    } finally {
      loading.value = false
    }
  }

  function convert(amount: number, from: string, to: string): number {
    if (from === to) return amount
    const fromRate = rates.value[from]
    const toRate = rates.value[to]
    if (!fromRate || !toRate) return 0
    return (amount / fromRate) * toRate
  }

  return { rates, loading, error, fetchRates, convert }
}

짚어둘 만한 몇 가지:

  • cache는 의도적으로 모듈 레벨에 둡니다. 컴포저블을 호출하는 두 컴포넌트가 같은 캐시를 공유하므로 라우트 전환 시 같은 기준 통화를 다시 받지 않습니다.
  • ratesRecord<string, number> 타입이라 나중에 <select>에 그대로 넣을 수 있습니다.
  • convert 함수는 크로스 환율 계산을 하므로 "from" 통화가 바뀔 때마다 다시 받을 필요가 없습니다. 자세한 건 5단계.

4단계: 변환기 컴포넌트

src/components/CurrencyConverter.vue 생성:

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useCurrencyRates } from '@/composables/useCurrencyRates'

const { rates, loading, error, fetchRates, convert } = useCurrencyRates()

const amount = ref(100)
const from = ref('USD')
const to = ref('EUR')

const converted = computed(() =>
  convert(amount.value, from.value, to.value).toFixed(2)
)

const currencies = computed(() => Object.keys(rates.value).sort())

function swap() {
  ;[from.value, to.value] = [to.value, from.value]
}

let timeout: ReturnType<typeof setTimeout> | null = null
watch(from, (newBase) => {
  if (timeout) clearTimeout(timeout)
  timeout = setTimeout(() => fetchRates(newBase), 300)
})

onMounted(() => fetchRates(from.value))
</script>

<template>
  <div class="converter">
    <h2>Currency Converter</h2>

    <div class="row">
      <input v-model.number="amount" type="number" min="0" />
      <select v-model="from">
        <option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
      </select>
    </div>

    <button @click="swap" aria-label="Swap currencies">⇅</button>

    <div class="row">
      <output>{{ loading ? '...' : converted }}</output>
      <select v-model="to">
        <option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
      </select>
    </div>

    <p v-if="error" class="error">{{ error }}</p>
  </div>
</template>

<CurrencyConverter />App.vue에 넣으면 동작하는 변환기가 완성됩니다. 첫 fetch가 돌아오면 통화 목록이 채워집니다. 금액 입력 필드에 입력하면 네트워크 요청 없이 결과가 반응형으로 갱신됩니다. 통화 교체 버튼은 드롭다운을 재배열합니다. "from" 통화 변경은 디바운스된 재요청을 트리거합니다.

5단계: 크로스 환율 변환 (요청을 낭비하지 않기)

한 가지 기준에서만 변환한다면 이 섹션은 건너뛸 수 있습니다. 그러나 대부분의 변환기는 사용자가 양쪽을 선택할 수 있게 하며 — 순진한 구현은 "from" 변경마다 다시 받습니다.

크로스 환율 계산이 이를 해결합니다. USD를 기준으로 EUR과 JPY의 대 USD 환율이 있다면, EUR → JPY 환율은 그저 (1 / EUR_rate) * JPY_rate입니다. 3단계의 convert 함수가 정확히 그 일을 합니다:

return (amount / fromRate) * toRate

세션당 한 번만 받으면 됩니다. 무료 할당량에 큰 이득. from의 watcher는 방어 수단이 됩니다 — 사용자가 캐시된 환율 표에 없는 이국적인 통화를 고르면, 그 통화를 기준으로 다시 받음으로써 변환이 계속 동작합니다.

6단계: 로딩 스켈레톤과 에러 상태

스피너도 괜찮지만, 환율 로딩 중 레이아웃이 흔들리는 건 거슬립니다. 첫 fetch 동안 자리표시자 옵션을 렌더링:

<select v-model="from" :disabled="loading && currencies.length === 0">
  <option v-if="currencies.length === 0">Loading...</option>
  <option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
</select>

에러 경로에서는 막다른 메시지 대신 재시도 버튼을 보여줍니다:

<div v-if="error" class="error-box">
  <p>{{ error }}</p>
  <button @click="fetchRates(from)">Retry</button>
</div>

예의를 갖추려면 429(레이트 제한)와 5xx를 분리해서 처리하세요. Finexly의 월 1,000회 무료 요청과 5분 캐시면 정말로 큰 스파이크가 와야 한도에 닿습니다 — 그래도 깔끔한 재시도 경로가 UI를 든든하게 합니다.

7단계: 서버 프록시로 API 키 숨기기

import.meta.env.VITE_*에 있는 모든 것은 클라이언트 번들에 들어갑니다. 대부분의 읽기 전용 통화 위젯에서는 허용 가능한 수준입니다 — 최악은 누군가 키로 환율을 긁어가는 정도. 다층 방어가 필요하면 서버 측에서 요청을 프록시하세요.

Nuxt 3 서버 라우트 예:

// server/api/rates.get.ts
export default defineEventHandler(async (event) => {
  const { base } = getQuery(event)
  const config = useRuntimeConfig()
  return await $fetch(
    `https://api.finexly.com/v1/latest?base=${base}&apikey=${config.finexlyApiKey}`
  )
})

그런 다음 컴포저블이 Finexly origin 대신 /api/rates?base=${base}를 가리키게 하세요. 키는 절대 서버를 떠나지 않습니다. 같은 패턴이 모든 Vue 메타프레임워크에서 동작합니다 — Nuxt, Quasar, 그냥 Express 백엔드까지.

8단계: 프로덕션 체크리스트

배포 전:

  • 공격적으로 캐시. FX 환율은 매초 변하지 않습니다. 클라이언트 5분 TTL과 서버 1분 TTL이면 거의 모든 표시 용도에 충분합니다. 프로덕션에서 검증된 패턴은 캐싱 및 에러 처리 가이드를 참고하세요.
  • 올바르게 반올림. 통화 표시는 통화의 최소 단위에 맞춰 반올림합니다 (USD 2자리, JPY 0자리, KWD 3자리). .toFixed(2) 대신 Intl.NumberFormat(locale, { style: 'currency', currency: to.value })를 사용하세요.
  • Intl로 포맷팅. new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(123.45)는 "€123.45"를 주고 사용자 로케일을 존중합니다.
  • SSR 안전 fetch. Nuxt에서는 onMounted 안의 생 fetch보다 useFetch를 선호하세요. 서버 렌더 중에 환율이 사용 가능해 hydration 불일치를 피할 수 있습니다.
  • 할당량 모니터링. 월 요청 예산의 80%를 소진하면 경고하는 작은 로거를 추가하세요. 더 많은 트래픽에는 요금제가 무료 티어 끝에서 시작합니다.

Vue 2 (Options API) 변형

Vue 2를 쓰고 있다면 같은 로직을 거의 줄 단위로 옮길 수 있습니다:

export default {
  data() {
    return { rates: {}, loading: false, error: null, amount: 100, from: 'USD', to: 'EUR' }
  },
  computed: {
    converted() {
      const fr = this.rates[this.from]
      const tr = this.rates[this.to]
      return fr && tr ? ((this.amount / fr) * tr).toFixed(2) : '0.00'
    },
    currencies() {
      return Object.keys(this.rates).sort()
    },
  },
  mounted() { this.fetchRates(this.from) },
  watch: {
    from(newBase) { this.fetchRates(newBase) },
  },
  methods: {
    async fetchRates(base) {
      this.loading = true
      try {
        const res = await fetch(
          `https://api.finexly.com/v1/latest?base=${base}&apikey=${process.env.VUE_APP_FINEXLY_KEY}`
        )
        const data = await res.json()
        this.rates = data.rates
      } catch (e) {
        this.error = e.message
      } finally {
        this.loading = false
      }
    },
  },
}

Composition API는 컴포넌트 간 로직 공유에 더 깔끔하지만, 단일 변환기 위젯이라면 Options API도 잘 동작합니다. 팀이 더 함수적인 접근을 선호한다면 JavaScript 통합 가이드가 프레임워크 비종속 패턴을 다룹니다.

흔한 함정 (그리고 피하는 법)

Nuxt의 hydration 불일치. onMounted에서 fetch를 호출하면 클라이언트에서는 동작하지만 SSR 일관성을 깹니다. 대신 useFetchuseAsyncData를 사용하세요.

긴 세션 후 오래된 환율. 위의 5분 TTL은 하루 종일 열린 탭이 1시간 전 환율을 보여준다는 뜻. visibilitychange로 새로고침:

document.addEventListener('visibilitychange', () => {
  if (!document.hidden) fetchRates(from.value)
})

부동 소수점 드리프트. JavaScript 숫자는 double입니다. 0.1 + 0.2 !== 0.3. 금액에는 정수 센트로 곱한 뒤(amount * 100) 계산하고 다시 나눠주세요. 또는 결제 관련 작업에는 dinero.js 같은 라이브러리를 사용하세요.

개발 중 CORS 에러. 일부 통화 API는 브라우저 직접 호출을 금지합니다. Finexly는 클라이언트 사용을 위해 브라우저 origin을 허용하며, 그 외에는 7단계의 프록시가 해결합니다.

왜 Vue 프로젝트에 Finexly인가

프런트엔드 앱용 환율 API 선택 시 중요한 것 몇 가지: 응답 시간, 정확도, 무료 티어의 관대함, 깔끔한 JSON. Finexly는 네 가지 모두를 노립니다 — sub-50ms p95 지연, 60초마다 갱신되는 mid-market 환율, 170개 이상의 통화, 가공 없이 Vue ref에 바로 들어가는 JSON 형태.

대안과의 비교가 궁금하다면 통화 API 비교가 트레이드오프를 자세히 다룹니다. 또는 나란히 비교 페이지를 사용하세요.

자주 묻는 질문

이 튜토리얼을 Vue 2에서도 쓸 수 있나요?

네. 컴포저블 패턴은 Vue 3 전용이지만, 기저 로직 — 환율을 받아 data에 저장하고 변환을 계산하는 — 은 Options API에서도 동일하게 동작합니다. 위의 Vue 2 예시가 그대로 대체됩니다.

Finexly API는 Vue 프로젝트에 무료인가요?

무료 플랜은 월 1,000회 요청을 주며, 사이드 프로젝트, 포트폴리오, 작은 SaaS에는 충분합니다. 5분 캐시와 함께라면 일일 활성 사용자 약 200명을 지원합니다. 더 큰 트래픽은 요금제를 보세요.

API 키가 클라이언트 번들에 노출되지 않게 하려면?

7단계처럼 서버 라우트를 통해 요청을 프록시하세요. VITE_*NUXT_PUBLIC_* 접두사 모두 변수를 클라이언트에 보이게 만듭니다. 민감한 것은 서버 함수 뒤에 있어야 합니다.

환율은 얼마나 정확한가요?

Finexly는 여러 Tier-1 유동성 공급자의 mid-market 환율을 집계해 60초마다 갱신합니다. 표시와 대부분의 가격 책정 응용에 충분히 정확합니다. 거래 실행에는 스트리밍 피드가 필요합니다 — REST 대 WebSocket 가이드를 보세요.

과거 금액을 변환할 수 있나요?

네 — Finexly의 /historical 엔드포인트는 날짜 파라미터를 받아 임의의 영업일 환율을 반환합니다. 패턴은 위의 /latest 엔드포인트와 동일하며, URL만 바꾸면 됩니다. 과거 환율 API 가이드에서 자세히 다룹니다.

정리

이제 실제 운영 관심사 — 캐싱, 디바운싱, 에러 상태, SSR, API 키 보안 — 를 처리하는 Vue 3 환율 변환기를 갖췄습니다. 컴포저블 패턴 덕에 같은 useCurrencyRates를 내비게이션 바 위젯, 결제 페이지, 대시보드 차트에 fetch 로직을 다시 작성하지 않고 떨어뜨릴 수 있습니다.

자신의 프로젝트에서 시도할 준비가 됐나요? Finexly 무료 API 키 받기 — 신용카드 불필요. 월 1,000회 무료 요청으로 시작하고 트래픽이 무료 티어를 넘어설 때 업그레이드하세요.

Vlado Grigirov

Senior Currency Markets Analyst & Financial Strategist

Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...

View full profile →

이 기사 공유하기