AI와 함께 가상자산 세금 계산기를 만들다 — 코인택스 기획부터 구현까지

소개

시작은 하나의 불안에서

2026년부터 가상자산 양도소득세가 시행됩니다. 국내 가상자산 투자자 700만 명이 처음으로 세금 신고를 해야 하는 상황입니다. 그런데 막상 준비하려 보니 문제가 보였습니다.

업비트에서 산 BTC를 빗썸으로 옮겨 팔고, ETH는 메타마스크 지갑에서 직접 매도했다면? 세법은 코인별 총평균법으로 계산하라고 하는데, 거래소 3곳에 흩어진 내역을 어떻게 합산할까요? 게다가 투자 내역은 민감한 금융 정보인데, 어딘가에 업로드해야 한다면?

이 문제가 코인택스(CoinTax) 의 출발점이었습니다.

진행 과정

1단계: 기획 — "70% 확신이면 실행한다"

시장을 먼저 읽었다

막연한 아이디어를 사업으로 만들기 전에, 시장부터 분석했습니다. 결과는 명확했습니다.

구분

현황

과세 시작

2027년 (2026년 귀속분)

대상

국내 투자자 약 700만 명

기존 해결책

엑셀 수기 계산 / 고비용 세무 의뢰 / 해외 서비스 (한국 세법 미반영)

기존 도구는 모두 한 가지 문제가 있었습니다. 복수 거래소·지갑 통합을 지원하지 않는다는 것. 그리고 해외 서비스는 2026년 의제취득가액 특례를 알 리 없었습니다.

4대 헌장으로 원칙을 고정했다

기획서를 쓰면서 가장 중요하게 다룬 건 '무엇을 만드느냐'가 아니라 '무엇을 절대 타협하지 않느냐' 였습니다.

① 신뢰 인프라 — 단순 계산기가 아닌, 투자자가 의지할 수 있는 세무 인프라
② 보안의 절대성 — Zero-Trace 원칙은 협상 불가 조항
③ 70% 승률 — 충분한 분석 후 과감하게 실행
④ 미래 지능화 — AI 기반 절세 예측으로 진화

특히 Zero-Trace 원칙이 핵심이었습니다. 사용자의 거래 내역을 서버에 저장하지 않고, 브라우저 메모리 내에서만 연산 후 세션 종료 시 완전 삭제. 이 결정 하나가 이후 모든 아키텍처의 기준점이 되었습니다.


2단계: 설계 — 세법을 코드로 번역하다

가장 어려운 부분은 세법 이해였다

기술보다 도메인 지식이 먼저였습니다. 세법의 핵심 규칙을 정확히 이해해야 올바른 코드를 쓸 수 있었습니다.

총평균법 계산 원칙:

코인별 양도차익 = 양도가액 - (총평균 취득단가 × 매도수량)
전체 세액 = MAX(코인별 차익 합산 - 250만원, 0) × 20%

여기서 치명적인 함정이 있었습니다. 총매출 - 총비용으로 계산하면 안 됩니다. 코인별로 먼저 차익을 산출한 후 합산해야 합니다. 이 원칙을 코드에 명시적으로 남겼습니다.

// 계산 순서 원칙:
//   ① 코인별: 양도가액 - 취득원가 = 코인 양도차익
//   ② 전체: coinBreakdown[].gain 합산 = 총 양도차익
//   (totalRevenue - totalCost 방식 절대 사용 금지)

의제취득가액 — 납세자 유리 값 자동 선택

2026년 말 시가가 실제 취득원가보다 높으면, 세법은 시가를 취득원가로 선택할 수 있게 해줍니다. 납세자에게 유리한 방향으로. 이 로직을 순수 함수 하나로 만들었습니다.

export function applyDeemedPrice(
  actualCost: number,
  marketPriceAt2026End: number
): { appliedCost: number; deemedApplied: boolean } {
  if (marketPriceAt2026End > actualCost) {
    return { appliedCost: marketPriceAt2026End, deemedApplied: true }
  }
  return { appliedCost: actualCost, deemedApplied: false }
}

12줄짜리 함수지만, 이 함수가 사용자의 세금을 수십만 원 줄여줄 수 있습니다.

아키텍처: 레이어를 명확히 구분했다

domain/      세금 상수, 에러 코드 (변경 가장 적은 레이어)
types/       전체 타입 정의
features/    비즈니스 로직 (세액 계산, 의제취득가, 소스 통합)
lib/         인프라 (파일 파서, 시세 API, PDF, Zero-Trace)
components/  UI 컴포넌트
store/       Zustand 전역 상태

lib/zero-trace에서 Zustand를 import하지 않는다는 규칙처럼, 레이어 간 의존 방향을 단방향으로 고정했습니다. AI와 함께 작업할 때 이런 명문화된 규칙이 특히 중요합니다. 다음 세션에서도 같은 원칙이 유지되기 때문입니다.


3단계: 구현 — Claude Code와의 페어 프로그래밍

"코딩"보다 "설계 대화"가 더 많았다

Claude Code로 개발하면서 가장 놀란 점은 코드 생성 속도가 아니었습니다. 복잡한 도메인 지식을 함께 정리하는 과정이 훨씬 빠르다는 것이었습니다.

세법 엣지케이스를 자연어로 설명하면 타입 정의부터 비즈니스 로직까지 정확히 변환되었습니다:

  • "업비트에서 매수하고 빗썸으로 이동해서 빗썸에서 매도했을 때 취득원가는?"
    TRANSFER 타입 도입, 집계 시 자동 제외

  • "매도가 없는 코인은 리포트에서 제외해야 한다"
    soldQuantity > 0 필터링 로직

  • "거래 ID 중복 감지는 어떻게?"
    detectDuplicates.ts 분리 구현

실제로 마주친 버그: React 19 무한 루프

로컬 테스트에서 첫 번째 에러가 발생했습니다.

The result of getServerSnapshot should be cached to avoid an infinite loop

원인은 Zustand 셀렉터가 매 렌더마다 새 객체를 반환하는 것. React 19의 useSyncExternalStore가 이를 참조값 변경으로 감지해 무한 루프가 발생했습니다.

// 문제: 매 렌더마다 새 객체 {}
return useCoinTaxStore((s) => ({
  portfolio: s.portfolio,
  addCoin: s.addCoin,
}))

// 수정: useShallow로 얕은 비교
return useCoinTaxStore(
  useShallow((s) => ({
    portfolio: s.portfolio,
    addCoin: s.addCoin,
  }))
)

React 19 + Zustand 5 조합의 주의사항입니다. 객체를 반환하는 셀렉터에는 항상 useShallow가 필요합니다.


4단계: 디자인 — "Vault Dark"

현재 UI의 문제를 직시했다

초기 구현은 기능에만 집중했습니다. 결과는 bg-gray-50 + border-gray-200의 무채색 평범한 UI. 세금 계산 서비스의 신뢰감도, 암호화폐 서비스의 브랜드감도 없었습니다.

디자인 방향 결정 시 두 가지를 고려했습니다:

  • 암호화폐 사용자 → 다크 인터페이스를 신뢰한다

  • 세무 서비스 사용자 → 정밀함과 숫자 가독성을 중시한다

이 교집합에서 나온 컨셉이 "Vault Dark" — Bloomberg 터미널의 정밀함과 크립토 거래소의 현대적 감성.

디자인 시스템 3가지 핵심 결정

① 앰버 골드 액센트 (#F0A500)

신뢰(금융)와 크립토의 교집합 색상. 납부 세액 카드에만 골드 그라디언트를 적용해 시각적 위계를 만들었습니다.

② IBM Plex Mono — 숫자 전용 폰트

₩12,500,000 같은 금융 데이터에 모노스페이스 폰트를 적용했습니다. 자릿수가 정렬되고, "이 숫자는 신뢰할 수 있다"는 인상을 줍니다.

③ 배경 그리드 텍스처

순수 CSS로 72px 격자를 배경에 깔았습니다. 정밀도와 전문성을 암시하면서도, 투명도 25%로 방해가 되지 않습니다.

backgroundImage:
  'linear-gradient(var(--ct-border) 1px, transparent 1px),' +
  'linear-gradient(90deg, var(--ct-border) 1px, transparent 1px)',
backgroundSize: '72px 72px',
opacity: 0.25,


결과와 배운 점

  1. 클로드코드로 오류를 체크하면 계속해서 나오고, 이를 다시 체크하면 무한 반복된다. 결국 어느 단계에서는 다음 단계로 나가는 것이 전체적인 작업의 효율과 오류체크에 바람직하다.

  2. 실제로 화면으로 구현해 보아야 결과물에 대한 감을 잡을 수 있다. 특히 나와 같은 비개발자는 바이브코딩으로 해도 잘 진행되는것처럼 보이지만 막상 구현을 해보면 생각과는 다른 결과물이 나올 수도 있다. 수시로 구현하면서 수정하는 것이 오히려 효율적으로 생각된다.

뉴스레터 무료 구독

👉 이 게시글도 읽어보세요