[바이브코딩 도전기] Synology Web Station에서 PHP 가계부를 올리기까지 — 폴더 구조와 기본 설계

NAS에 뭔가를 직접 만들어 올린다는 게 처음엔 막연하게 느껴졌다. 가계부 앱을 하나 쓰다가 서비스가 종료되거나 유료 전환되는 경험을 몇 번 하고 나니, 그냥 내가 직접 돌리는 게 낫겠다 싶었다. 마침 Synology NAS가 있었고, Web Station이라는 기능이 있다는 걸 알게 됐다.

이 글은 그 구조를 처음 잡을 때 어떤 식으로 생각했는지를 정리한 것이다. 코드 강의보다는 “이런 식으로 생겼구나” 정도로 읽으면 된다.


Web Station이 뭔지 먼저

Synology DSM에는 Web Station이라는 패키지가 있다. 쉽게 말하면 NAS 안에 웹 서버를 켜주는 도구다. Apache나 Nginx를 고를 수 있고, PHP 버전도 패키지 센터에서 설치해서 연결할 수 있다.

외부에서 접근하려면 공유기 포트포워딩이나 Synology의 QuickConnect, 또는 직접 도메인 연결 같은 설정이 필요하다. 하지만 집 안 로컬 네트워크에서만 쓸 거라면 그냥 NAS IP 주소로 접근하면 된다. 나는 일단 로컬 전용으로 시작했다.

Web Station을 설치하고 나면 기본적으로 /volume1/web 같은 경로가 웹 루트로 잡힌다. 이 폴더에 파일을 넣으면 브라우저에서 접근할 수 있게 된다. PHP 파일도 마찬가지다. 이게 전부다. 거창한 서버 세팅 없이 이 정도면 시작할 수 있다.


폴더를 어떻게 나눴나

처음엔 파일을 막 넣었다가, 조금 쓰다 보니 금방 엉켜버렸다. 그래서 다시 정리했다. 구조는 단순하게 잡았다.

web/
├── index.php
├── api/
│   ├── payroll.php
│   ├── cards.php
│   └── ...
├── assets/
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   ├── dashboard.js
│   │   ├── payroll.js
│   │   └── ...
│   └── icons/
└── manifest.webmanifest

index.php가 화면의 뼈대를 잡는다. 메뉴 이동이나 탭 전환은 JavaScript로 처리하고, 실제 데이터는 api/ 폴더 안의 PHP 파일들이 JSON으로 응답하는 식이다. 프론트엔드에서 fetch()로 요청하면 PHP가 SQLite 데이터베이스를 읽고 결과를 돌려준다.

프레임워크는 따로 안 썼다. React나 Vue를 붙이면 빌드 환경도 필요하고 복잡해지니까, 그냥 바닐라 JS로 짰다. 기능이 늘면서 js 파일이 늘어나긴 했지만, 각 화면마다 파일 하나씩 대응하는 방식이라 관리는 어렵지 않았다.


데이터베이스는 SQLite

MySQL을 설치할 수도 있었는데, 굳이 그럴 필요를 못 느꼈다. 혼자 쓰는 가계부에 동시 접속이 많을 리도 없고, SQLite는 파일 하나가 데이터베이스 전체라서 백업도 그 파일 하나만 복사하면 된다. PHP에서 PDO로 연결하면 바로 쓸 수 있다.

테이블은 몇 개 안 된다. 카드 거래 내역, 월급 기록, 카드사 정보 정도다. 처음에 스키마를 너무 복잡하게 잡으려다가, 일단 단순하게 시작하고 필요할 때 컬럼을 추가하는 방식으로 바꿨다. 실제로 개발하다 보면 처음 설계가 다 맞는 경우가 없더라.

예를 들어 월급명세서 기능을 만들 때, 처음엔 정기급여 항목만 넣었다. 그런데 쓰다 보니 수시급여처럼 매달 이름이 달라지는 항목도 직접 입력하고 싶어졌다. 그래서 salary_records 테이블에 label 컬럼을 하나 추가하고, API와 화면을 같이 수정했다. 이런 식으로 조금씩 붙여나가는 게 혼자 만드는 프로젝트에선 오히려 편하다.


개발하면서 예상 못 한 문제들

구조 자체는 단순한데, 막상 기능을 붙이다 보면 생각지 못한 데서 시간을 잡아먹는다.

툴팁이 다른 요소에 가려지는 문제

튜토리얼 툴팁을 만들었는데, 하이라이트 처리한 요소 뒤로 툴팁이 숨어버리는 일이 생겼다. z-index를 높여도 해결이 안 됐다. 알고 보니 부모 요소에 transform이나 opacity 같은 속성이 있으면 새로운 stacking context가 생겨서 자식의 z-index가 그 안에서만 적용된다는 문제였다.

결국 툴팁 엘리먼트를 body 바로 아래로 옮기고, JavaScript로 위치를 계산해서 붙이는 방식으로 해결했다. CSS 하나 건드리면 될 것 같았는데 꽤 시간이 걸렸다.

iOS 홈 화면 아이콘 문제

스마트폰 홈 화면에 추가해서 앱처럼 쓰고 싶었다. PWA 설정을 하면 되는데, manifest.webmanifest에 아이콘을 SVG로 넣었더니 iOS Safari에서 아이콘이 아예 안 나왔다.

찾아보니 iOS는 apple-touch-icon에 SVG를 지원하지 않는다. PNG가 필요하다. macOS 터미널에서 sips 명령어로 SVG를 PNG로 변환하고, 180×180, 192×192, 512×512 사이즈로 각각 만들어서 assets/icons/에 넣었다. manifest.webmanifestindex.php <head>에도 경로를 추가했더니 해결됐다.

이런 건 문서를 읽을 때는 그냥 지나치는 내용인데, 막상 안 되면 원인 찾는 데 시간이 꽤 걸린다.

날짜 이동 버튼이 가끔 작동 안 하는 문제

대시보드, 월급, 카드 화면에 연도·월을 앞뒤로 이동하는 버튼이 있다. 그런데 화면을 전환했다 돌아오면 버튼이 반응을 안 하는 경우가 생겼다.

원인은 모듈 초기화 타이밍이었다. 화면 전환 시 JS 모듈이 다시 초기화되면서 이벤트 리스너가 중복으로 붙거나, 반대로 아예 안 붙는 상황이 생겼다. 이걸 이벤트 위임 방식으로 바꿨다. 버튼 각각에 리스너를 붙이는 대신, 상위 컨테이너 하나에 붙이고 클릭 이벤트가 올라오면 거기서 처리하는 방식이다. 이후로는 타이밍 문제가 없어졌다.


카드 기능을 만들면서 생긴 고민

카드 내역은 생각보다 복잡했다. 단순히 쓴 금액을 기록하는 게 아니라, 카드사마다 결제일과 사용기간이 다르기 때문이다.

예를 들어 어떤 카드는 전월 1일~말일 사용분을 이번 달 15일에 청구하고, 다른 카드는 전전월 말일~전월 말일 사용분을 이번 달 말일에 청구한다. 이걸 그냥 거래일 기준으로만 보면 “이번 달에 얼마 썼나”를 정확하게 파악하기 어렵다.

그래서 카드사별로 결제일과 사용기간 오프셋을 설정할 수 있게 만들고, 거래 내역을 청구월 기준으로 재분류하는 로직을 넣었다. 이미 저장된 거래가 재분류 기준에 따라 다른 월로 이동할 수 있다는 점은 처음에 좀 헷갈렸는데, 오히려 실제 청구서와 맞추려면 이 방식이 맞다.

내역을 여러 건 선택해서 태그나 메모를 한꺼번에 수정하는 기능도 붙였다. 카드 명세서를 가져오면 비슷한 가맹점 거래가 여러 건 있는 경우가 많아서, 하나씩 수정하면 귀찮기 때문이다. 드래그로 여러 행을 선택하는 기능도 만들었는데, 첫 번째 행이 드래그 시작 시 의도치 않게 토글되는 버그가 있었다. 드래그 시작 시점에 상태를 따로 기록해두고 드래그 종료 후 일괄 처리하는 방식으로 고쳤다.


지금 상태 정리

구조 자체는 복잡하지 않다. NAS 웹 폴더에 PHP 파일 몇 개, JS 파일 몇 개, SQLite 파일 하나. 이게 전부다. 외부 서비스 의존 없이 집 안 네트워크에서만 돌아가는 가계부다.

기능을 붙이는 과정에서 생각보다 자잘한 문제들이 많았다. z-index 하나, PNG 변환 하나, 이벤트 타이밍 하나. 이런 것들이 쌓이다 보면 “그냥 앱 쓰는 게 낫지 않나” 싶을 때도 있다. 근데 내가 원하는 방식으로 동작하는 가계부가 생겼고, 데이터가 내 NAS 안에만 있다는 게 마음에 든다.

다음 편에서는 카드 내역을 어떻게 가져오고 분류하는지를 좀 더 자세히 정리할 예정이다.


시리즈 글 모아보기

댓글 남기기