바이브 코딩 일지 #2: Next.js 모바일 반응형 UI + AWS 배포 실전기

가계부 앱 두 번째 세션. 모바일에서 버튼이 안 눌리는 버그부터, 10+20=1020 미스터리, 정기 거래 기능, Vercel 포기하고 AWS 직접 배포까지 하루 동안의 기록이다.

1. 모바일 버튼이 안 눌린다 — Safe Area

앱을 모바일에서 열어보니 하단 네비게이션 버튼이 아이폰 홈 인디케이터(화면 아래 가로 막대)에 딱 겹쳐서 터치가 안 됐다.

CSS env(safe-area-inset-bottom) 변수가 기기의 하단 안전 영역 크기를 알려주는데, Tailwind에서는 직접 지원 안 해서 커스텀 플러그인을 만들었다:

// tailwind.config.js
plugin(({ addUtilities }) => {
  addUtilities({
    ".pb-safe": {
      paddingBottom: `max(1.5rem, env(safe-area-inset-bottom))`,
    },
  });
});

HTML viewport에 viewport-fit=cover 추가하고, 하단바에 pb-safe 클래스 붙이면 된다.

모바일 퍼스트: 거래 목록이 모바일에서 빽빽했다. Tailwind hidden md:block으로 작은 화면에선 덜 중요한 정보를 숨기고, 768px 이상에서만 보이게 했다. 버튼도 모바일에선 [+] 아이콘만, 데스크톱에선 텍스트까지.

Flex vs Grid: 카드 3개가 모바일에서 2+1로 깨지는 문제. flex-wrap은 내용 길이에 따라 줄바꿈이 달라진다. grid grid-cols-3으로 바꾸면 내용과 상관없이 정확히 3등분. Flex는 유연한 정렬(버튼 그룹), Grid는 칸수 고정(카드 레이아웃).

SSOT: 사이드바와 모바일 하단바가 각각 메뉴를 하드코딩하고 있어서, 메뉴 하나 추가하려면 두 파일을 고쳐야 했다. navigation.ts 하나로 통합해서 Single Source of Truth로 만들었다.

2. 10 + 20 = 1020? — Prisma Decimal 함정

대시보드에서 계좌 잔액 합산이 30이 아니라 1020이 되는 황당한 버그.

원인: Prisma가 금융 데이터 정밀도를 위해 Decimal 타입을 쓰는데, 이게 JSON 직렬화되면서 문자열이 된다. "10" + "20" = "1020".

// 수정 전 — 문자열 합성
const total = accounts.reduce((sum, acc) => sum + acc.balance, 0);
// "0" + "10000" + "20000" = "01000020000"

// 수정 후
const total = accounts.reduce((sum, acc) => sum + Number(acc.balance), 0);
// 30000

순자산 계산도 고쳤다. 부채가 이미 음수(-10000)로 저장되어 있는데 총자산 - 총부채라고 쓰면 100000 - (-10000) = 110000으로 늘어나는 버그. 총자산 + 총부채로 바꿔서 100000 + (-10000) = 90000 정상.

핵심 원칙: 숫자 계산은 백엔드에서, 프론트엔드는 화면에 뿌리기만. 이렇게 해야 iOS/Android 앱을 나중에 만들어도 같은 API에서 같은 결과를 받을 수 있다.

3. 정기 거래 기능

월세, 통신비 같은 걸 매달 수동 입력하기 귀찮아서 N개월치 자동 생성 기능을 만들었다.

핵심 로직: 오늘 이전 거래는 잔고에 반영, 오늘 이후 거래는 목록에만 기록하고 잔고는 안 건드림. 미래 거래까지 잔고에 반영하면 실제 은행 앱이랑 숫자가 달라져서 혼란스럽다.

날짜 계산은 date-fns 라이브러리를 썼다. new Date()로 직접 하면 30일/31일/2월/윤년 처리를 다 직접 해야 한다.

4. Vercel 대신 AWS 직접 배포

Vercel은 git push 한 번이면 배포 끝이라 편한데, 서버리스라서 한계가 있다:
– 24시간 켜져있는 서버가 없음
– 로컬 PostgreSQL 직접 연결이 어려움
– 세션 유지 제약

Lightsail로 갈아탔다. 배포는 rsync로:

rsync -az --exclude='.next/cache' -e "ssh -i my-key.pem" \
  .next/ user@서버IP:/home/user/app/.next/

scp로 300초 걸리던 게 rsync로 1~2초. 변경된 파일만 전송하니까.

서버 관리는 PM2로. npm start는 터미널 닫으면 서버도 죽는데, PM2는 백그라운드에서 24시간 돌려주고, 죽으면 자동 재시작, pm2 reload로 무중단 교체도 된다.

이번 세션에서 배운 핵심: Safe Area, 모바일 퍼스트, Prisma Decimal 문자열 함정, 백엔드/프론트 역할 분리, rsync+PM2 배포. 하나하나 부딪히면서 해결하니까 이해가 확실해진다.