평소 쓰던 가계부 앱들이 다 어딘가 마음에 안 들었다. 광고가 붙어있거나, 원하는 카테고리가 없거나, 무엇보다 내 지출 데이터가 남의 서버에 쌓이는 게 찜찜했다. 그래서 그냥 직접 만들기로 했다.
1. 일단 만들어보기
처음엔 단순하게 시작했다. 브라우저에서 바로 쓸 수 있는 HTML 한 장짜리 가계부. 날짜, 수입/지출 구분, 카테고리, 금액, 메모를 입력하면 표로 쌓이고, 월별로 탭을 넘겨보고, 이번 달 지출을 카테고리별로 막대그래프처럼 보여주는 정도였다.
디자인은 실제 가계부 노트를 흉내 냈다. 지출은 빨간 잉크, 수입은 파란 잉크로 구분하고, 이번 달 잔액은 도장처럼 동그랗게 찍히는 형태로 만들었다. 옛날 장부책 느낌을 내고 싶었다.
이 단계에서는 브라우저 안에서만 데이터가 저장되는 방식이었다. 써보기엔 충분했지만, 곧 한계가 보였다.
2. “내 서버에 저장하고 싶다”
브라우저 저장 방식은 기기를 바꾸면 데이터가 사라진다는 게 문제였다. 마침 집에서 운영 중인 홈서버(우분투 + Docker 환경)가 있었기 때문에, 이번 기회에 제대로 된 백엔드를 만들어서 그쪽에 데이터를 저장하기로 했다.
요구사항을 정리하면:
- 구글 계정으로 로그인
- 내 계정으로만 접근 가능 (남이 못 들어오게)
- 데이터는 내 서버의 DB에 저장
- 기존에 쓰던 Docker + nginx 리버스 프록시 구조에 그대로 얹기
그래서 구성을 이렇게 잡았다.
- 백엔드: Node.js + Express
- 로그인: Passport.js의 구글 OAuth 전략
- DB: SQLite (개인용이라 가볍게)
- 배포: Dockerfile + docker-compose로 패키징해서 기존 서버에 컨테이너 하나 추가하는 방식
- 접근 제어: 로그인한 구글 계정 이메일이 허용 목록에 있을 때만 API 접근 허용
프론트엔드는 기존 화면을 그대로 가져오되, 로컬 저장 대신 서버 API를 호출하도록 바꾸고, 로그인 전에는 “구글 계정으로 로그인” 버튼만 보이는 게이트 화면을 앞에 붙였다.
3. 배포하면서 겪은 삽질들
이론상 다 되는 그림이었는데, 실제로 서버에 올리면서는 예상대로 여러 번 막혔다.
3-1. 포트는 열었는데 접속이 안 됨
docker-compose.yml에서 포트를 외부에 열도록 고쳤는데도 계속 로컬(127.0.0.1)에만 열려있었다. 원인은 단순했다 — 컨테이너를 완전히 내렸다 올리지 않고 그냥 재실행만 해서, 바뀐 포트 설정이 반영되지 않았던 것. docker compose down 후 다시 up을 해주니 해결됐다.
방화벽(ufw)도 의심했지만 확인해보니 애초에 꺼져 있었다. 삽질 원인은 늘 생각보다 단순한 곳에 있다는 걸 다시 느꼈다.
3-2. redirect_uri_mismatch
nginx로 도메인을 연결하고 https까지 붙인 뒤 구글 로그인을 테스트하니 400 오류: redirect_uri_mismatch가 떴다.
원인은 구글 콘솔에 등록한 리디렉션 URI 경로와, 실제 서버 코드가 쓰는 콜백 경로가 달랐던 것. 예전에 다른 서비스(포럼)에 구글 로그인을 붙일 때 썼던 경로 이름을 그대로 재사용해서 등록해버린 게 문제였다.
여기서 새삼 깨달은 건, 구글 OAuth 콜백 경로는 정해진 규칙이 아니라 각 백엔드 라이브러리가 알아서 정하는 임의의 값이라는 점이다. 어떤 라이브러리는 /auth/google_oauth2/callback을 관례로 쓰고, 어떤 라이브러리는 /auth/google/callback을 쓴다. 구글 콘솔에 등록하는 값은 실제 서버 코드의 경로와 토씨 하나까지 정확히 일치해야 한다.
3-3. client secret이 invalid하다는 오류
경로 문제를 고치고 나니 이번엔 TokenError: The provided client secret is invalid가 떴다.
원인을 찾아보니, 구글에서 다운로드한 인증 정보 JSON 파일 내용을 복사해서 .env에 붙여넣는 과정에서 시크릿 값 뒤에 JSON의 다른 부분(","redirect_uris)까지 같이 딸려 들어가 있었다. 값만 정확히 잘라서 다시 넣으니 해결됐다.
이런 실수는 JSON 파일에서 값을 복사할 때 따옴표나 쉼표까지 같이 긁는 경우 흔히 생긴다. 이후로는 값을 복사할 때 앞뒤로 이상한 문자가 안 붙었는지 한 번씩 확인하는 습관이 생겼다.
4. 완성된 모습
세 가지 문제를 순서대로 해결하고 나니 구글 로그인부터 데이터 저장까지 정상적으로 동작했다.
- 접속하면 구글 로그인 게이트가 먼저 뜨고
- 로그인하면 내 계정 이메일 기준으로 기록이 분리되어 저장되고
- 월별 탭을 넘기며 지난 기록을 볼 수 있고
- 이번 달 지출을 카테고리별로 한눈에 볼 수 있게 됐다
5. 돌아보며
처음엔 그냥 “가계부 앱 하나 만들어볼까” 정도로 가볍게 시작했는데, 결과적으로는 OAuth 인증, 리버스 프록시, Docker 배포까지 한 사이클을 다시 돌아본 계기가 됐다. 특히 에러 메시지 하나하나가 사실은 꽤 명확한 힌트를 담고 있다는 걸 다시 느꼈다 — redirect_uri_mismatch는 정말 URI가 안 맞아서 나는 에러였고, client secret is invalid도 정말 값이 잘못 들어가서 나는 에러였다. 조급해하지 않고 에러 메시지를 있는 그대로 읽는 게 결국 제일 빠른 길이었다.
다음 목표는 예산 한도를 넘으면 알림을 주는 기능과, 연간 통계 화면을 추가하는 것이다.