byminseok.com

Building a Personal Finance Tracker Part 1. From Principles to Design

Translated from Korean

Building a Personal Finance Tracker App Part 1 — From Principles to Design

> I wanted to build an independent tracker, not just a household ledger. This is the journey of establishing financial principles and translating them into an app.

This series documents the process of creating Pomme, a personal finance tracking app. The name, inspired by Cézanne's apples, embodies the desire to see the essence. It's a single-user SPA built with React + Vite + Vercel, planned and deployed in just two days with Claude Code.

Part 1 covers the groundwork before building the app: establishing financial principles, defining the app concept, researching APIs, and designing the architecture.


Principles Come Before the App

When I decided to build a financial management app, the first thing I did wasn't open the code. I established financial principles first.

The reason is simple. If you don't know what to track, you can't design the app. Household ledger apps are everywhere; there's no reason to build another one. What I wanted was a tool that answered the question: "How close am I to financial independence right now?"

The framework of these principles is as follows.

Philosophy

> Money is the infrastructure of autonomy. > The goal is to create the freedom to choose.

Two Criteria for Judging Independence

  • Track A — Investment assets exceed a certain threshold. The engine where money makes money is in place.
  • Track B — A stable structure where I earn a certain monthly amount or more under my own name.

Meeting both tracks simultaneously signifies you can be independent. While income/expense tracking is necessary, the core was seeing how full both gauges are at a glance.


Not a Household Budget App, but an Independence Tracker

Once the principle was established, the app's identity became clear. Here's how it differs from existing household budget apps:

General Household Budget App Pomme
Focus on income/expense recording Focus on progress toward independence
Goal is saving Goal is asset growth + securing income structure
Detailed transaction history Transaction history delegated to existing apps
Universal Single-user, tailored to my principles

The core screen is the dashboard. Opening the app reveals two gauges.

┌─────────────────────────────────┐
│  🍎 Pomme                       │
│                                 │
│  Track A ━━━━━━━━━░░░░  23%    │
│  투자자산 / 목표                 │
│                                 │
│  Track B ━━░░░░░░░░░░░  15%    │
│  월수입 / 목표                   │
│                                 │
│  D-day  ▸ 목표일까지 XXX일      │
└─────────────────────────────────┘

It's no exaggeration to say this is the app's entirety. The rest are screens for managing data to fill these gauges.


Feature Design — 5 Tabs

The dashboard alone is insufficient. Users must input and verify the data composing the gauges.

Dashboard — Track A/B gauges, D-day, monthly income/expense summary

Portfolio — Stock management (ticker, quantity, purchase price), real-time price checks, liquid assets

Cash Flow — Monthly income/expense trends, net worth change chart. Visualizes existing household ledger data

Roadmap — Roadmap to independence. Milestone timeline and action plan checklist

Settings — API connection settings, data export/import, theme


Where Does the Data Come From — API Research

I never intended to create a ledger where I manually input transaction details. I've been using Whooing, a double-entry ledger service, for years. I can pull income/expense data from Whooing and fetch stock prices via external APIs.

Whooing API

Whooing is a Korean double-entry household ledger service that provides a REST API. It uses OAuth 1.0-based authentication, and the main endpoints are as follows.

Endpoint Purpose
accounts.json Item structure (income/expense/asset account tree)
in_out.json Monthly income/expense changes
mountain.json Monthly net asset trends
bs.json Asset/liability balances
sections.json Section (ledger unit) list

Since it's double-entry bookkeeping, section_id is required for all requests, and dates use the YYYYMMDD integer format.

What if there's no Hooing API? Alternatives include manually importing CSV files or using bank Open Banking APIs. The key is to first decide "where to source the income/expense data."

Stock Quote API — Finnhub

I needed real-time US stock quotes. After comparing several APIs, I chose Finnhub for simple reasons:

  • Free tier allows 60 requests per minute
  • Supports real-time US stock prices (/quote) + 7-day candle charts (/stock/candle)
  • Immediate access with a single API key

Yahoo Finance API is unofficial and unreliable, while Alpha Vantage's 5 requests per minute limit is too restrictive.

Exchange Rate API — ExchangeRate-API

To convert US stock prices to KRW, an exchange rate is needed. I chose ExchangeRate-API (open.er-api.com).

  • Completely free, no API key required
  • Just one line: GET /v6/latest/USD
  • Extract rates.KRW from the response

Architecture Design

With three APIs decided, the overall structure takes shape.

브라우저 (React SPA)
  ├── components/  ← 5페이지 UI
  ├── hooks/       ← 상태 관리 + 데이터 fetching
  └── lib/         ← API 클라이언트 + 계산 로직

Vercel Functions (서버리스)
  ├── /api/whooing        ← 후잉 프록시 (API 키 숨김)
  ├── /api/exchange-rate  ← 환율 프록시 (6시간 캐시)
  └── /api/data           ← KV 읽기/쓰기 (디바이스 동기화)

외부 API
  ├── whooing.com         ← 가계부 데이터
  ├── finnhub.io          ← 미국 주식 시세
  └── open.er-api.com     ← USD/KRW 환율

Why a Server is Needed

A React SPA might seem sufficient, but there's a reason a server is required.

API key protection. Exposing Whooing OAuth tokens in the browser lets anyone view your household accounts. The server injects the key, and the client makes requests via a proxy like /api/whooing?endpoint=in_out.json.

Caching. Exchange rates only need updating every 6 hours. Caching them on the server reduces external API calls.

Device Synchronization. Using only localStorage means portfolios entered on a phone won't appear on a desktop. Storing data in Vercel KV (Redis) enables synchronization anywhere.

In contrast, Finnhub doesn't go through a server. Since users enter API keys directly in Settings, direct client calls are acceptable. Finnhub's free key is essentially at the level of public data anyway.

Server Proxy Pattern

[브라우저]                    [Vercel Function]               [후잉 API]
    │                              │                              │
    ├─ GET /api/whooing ──────────▶│                              │
    │  ?endpoint=in_out.json       │── X-API-KEY 헤더 주입 ──────▶│
    │  &params=...                 │                              │
    │                              │◀──────── 응답 ───────────────│
    │◀──────── 응답 ───────────────│                              │

The client only specifies which endpoint to call; the server handles all authentication. This pattern applies not just to Fwing but to any OAuth API.


Data Model Design

The data managed by the app falls into four main categories.

Portfolio — Stock holdings + Cash equivalents

Portfolio
  ├── holdings[]       ← {ticker, shares, avgCost, currency}
  ├── cashAssets[]     ← {name, amount, currency, excludeFromTrackA}
  └── excludedAssets[] ← {name, amount, note}  트랙A 제외 자산

Goals — Goal setting

Goals
  ├── trackA: {target, label}   예) 투자자산 1.5억
  └── trackB: {target, label}   예) 월수입 200만

Settings — API connections + App settings

Settings
  ├── whooingSectionId   ← 후잉 섹션 선택
  ├── finnhubApiKey      ← 주식 시세 API 키
  ├── cashAccountIds[]   ← 현금성으로 볼 후잉 계좌
  └── theme              ← light / dark / system

SideIncome — Side income (automatically calculated by Hwing)

Track Calculation Logic

Once the data model is defined, the calculation formula for filling the gauge naturally follows.

Track A = Total investment assets

= SUM(종목별 shares × 현재가 × 환율)
  + SUM(현금성자산 중 excludeFromTrackA=false)

Current prices are fetched in real-time from Finnhub, and exchange rates from the ExchangeRate-API. Static data (holding quantity, purchase price) and dynamic data (market price, exchange rate) combine to form a single number.

Track B = This Month's Side Income

= 이번 달 후잉 총수입 - 본업 월급

Excluding the main job salary item from the household ledger leaves only "money earned under my name."


Data Storage Strategy

A database is excessive for a single-user app. Storage is divided into two layers.

Write-Through Cache Pattern

[쓰기]
유저 조작 → localStorage (즉시) → 1초 debounce → POST /api/data (KV)

[읽기]
앱 시작 → GET /api/data (KV) → localStorage 덮어쓰기
Storage Role
localStorage Cache for immediate reads. Works offline
Vercel KV (Redis) Device synchronization. Persistent storage

If KV is unavailable, gracefully degrade to localStorage only. This enables the "enter on phone, view on desktop" experience without complex sync logic.

KV stores the entire {portfolio, goals, settings, sideIncome, exportedAt} as a single blob. Since it's a single-user app, one key is sufficient.


Technology Stack Selection

Choice Reason
React 19 + TypeScript Component-based UI. Type safety
Vite Fast HMR. Minimal setup
Vercel Functions + KV + Deployment all in one place. Free tier sufficient
Recharts React Native charts. Net worth trend, spending bar chart
Pretendard Korean variable font. Clean number alignment

The reason I didn't use Next.js is simple: I don't need SSR. What's the point of SEO for a one-person app? Static builds with Vite and using only Vercel Functions for serverless is much lighter.


Design Decisions

Just as important as features is whether it's an "app you want to open every day." Since it's a personal project, I went with my own taste.

  • Atelier Tone — Background #F8F7F4 with paper texture (SVG noise). Workshop-like atmosphere instead of the cold feel typical of finance apps
  • Dark Mode[data-theme="dark"] CSS variable override. Warm gold accents
  • Gauge Animation — Counts up from 0 to current value. Gives a "filling up" sensation each time it opens
  • CSS Variables — Manage all colors with variables. No hardcoding allowed

Part 1 Wrap-Up

I got this far without writing a single line of code. To summarize, this is the process I followed:

  1. Establish financial principles → Define what to track
  2. Finalize app concept → Not a household ledger, but an independence tracker
  3. Research external APIs → Hwing, Finnhub, ExchangeRate-API
  4. Design architecture → React SPA + Vercel Functions proxy
  5. Data model + storage strategy → Write-through cache
  6. Tech stack + design direction

If you want to build a personal finance app, I recommend starting by clarifying "What do I actually need?" before diving into code. This prevents losing direction mid-development.

Part 2 covers translating this design into actual code: dashboard UI, Finnhub real-time market integration, Hwing API proxy, and Vercel KV synchronization.