집에 둔 서버를 운영 환경으로 만들기까지

미니PC 서버에 배포, 관측, 백업 기준을 세우고 App Store 공개까지 간 기록.

한강자리 서버는 FastAPI 백엔드, worker, PostgreSQL/PostGIS, Redis로 나뉘어 있었다. 이제 이 구성은 내 노트북 밖에서 계속 돌아야 했다.

그때부터 기준이 바뀌었다. “로컬에서 서버가 뜬다”만으로는 부족했다. 내가 자고 있을 때도 데이터 수집이 돌고, 배포는 매번 같은 순서로 진행되고, 문제가 생기면 어디부터 확인할지 바로 알 수 있어야 했다.

개인 프로젝트에서는 여기서 쉽게 멈춘다. 내 앱이고, 내가 만든 서버이니 문제가 생기면 그때 보면 된다고 생각하기 쉽다. 하지만 App Store에 올라간 앱이 호출하는 API라면 기준이 달라진다. 사용자는 서버가 집에 있는지 클라우드에 있는지 알 필요가 없고, 오래된 값이나 빈 화면을 보면 앱을 믿기 어려워진다.

집에 둔 서버도 운영 환경이었다

한강자리 백엔드는 집에 있던 미니PC에 두기로 했다.

클라우드로 올리면 더 익숙하고 편했을 것이다. 그래도 이번 프로젝트에서는 앱만 만들고 끝내고 싶지 않았다. 서버를 띄우고, 배포하고, 모니터링하고, 장애를 확인하고, 백업과 복구까지 갖춘 작은 제품을 직접 끝까지 운영해보고 싶었다.

처음에는 Docker Compose에 가까운 단순한 구조로 시작했다. 로컬 개발 환경에서 Postgres, Redis, API, worker를 띄우고, 미니PC에도 비슷하게 배포하는 방식이었다.

그런데 “밖에서 계속 쓰는 서버”라고 생각하는 순간 질문이 늘어났다.

  • 마이그레이션은 언제 돌릴 것인가
  • API와 worker는 어떻게 나눠 배포할 것인가
  • Postgres와 Redis는 어떻게 관리할 것인가
  • 배포 뒤에는 무엇을 확인할 것인가
  • 외부에서 API가 죽었을 때 터널 문제인지, K3s 문제인지, 앱 문제인지 어떻게 구분할 것인가
  • 로그와 메트릭은 어디서 볼 것인가
  • 잘못된 배포를 어떻게 되돌릴 것인가

이 질문들이 쌓이면서 Cloudflare Tunnel, K3s, ArgoCD, local CI, 배포 상태 저장소가 붙었다.

외부 요청은 엣지와 터널을 지나 미니PC의 K3s ingress로 들어온다. 원본 서버를 직접 열어두지 않고, 터널이 연결을 유지하는 방식으로 사용자 요청 경로와 운영 경로를 분리했다.

CI가 lint/test/build를 통과한 이미지 태그를 배포 상태 저장소에 적어두면, ArgoCD가 미니PC 쪽 클러스터를 그 태그로 맞춘다. Prometheus와 Grafana로 메트릭을 보고, 로그 파이프라인과 백업/복구 절차도 같이 정리했다.

작은 앱치고는 과하게 보일 수 있다. 하지만 이번 프로젝트의 목표 중 하나가 “작은 서비스를 실제로 운영 가능한 제품으로 끝까지 만들어보기”였기 때문에 이 과정 자체가 중요했다. 미니PC를 쓴다고 운영 책임이 작아지지는 않았다. 오히려 내가 직접 챙겨야 할 부분이 더 잘 보였다.

앱은 App Store에 올린 바이너리 하나로 끝나지 않았다. API, 데이터 수집, 캐시, 알림, 배포 경로, 운영 설정 관리, 헬스 체크, 로그, 메트릭, 백업까지 모두 제품의 일부였다.

서버가 켜져 있다는 것만으로는 부족했다

미니PC를 쓰면 비용은 낮아지지만 책임은 더 선명해진다. 클라우드 콘솔에서 버튼으로 해결되던 것들을 직접 정해야 한다. 디스크가 부족해지면 어디서 알 수 있는지, 재부팅 후 서비스가 올라오는지, DB 백업은 어디에 남는지, 잘못된 배포를 되돌릴 수 있는지 같은 질문이 계속 따라온다.

그래서 프로세스가 떠 있는지만 운영 기준으로 삼지 않았다. 내가 자리를 비운 동안에도 다음을 확인할 수 있어야 했다.

  • API가 살아 있는가.
  • worker가 정해진 시간마다 도는가.
  • 마지막 수집 성공 시각이 너무 오래되지 않았는가.
  • DB와 Redis가 정상인가.
  • 배포한 이미지가 실제로 반영됐는가.
  • 문제가 생겼을 때 어느 계층부터 볼지 정해져 있는가.

그 뒤로 로그와 메트릭은 있으면 좋은 장식이 아니었다. 이것들이 없으면 장애를 고치는 문제가 아니라, 어디가 고장났는지 맞히는 일부터 시작해야 한다. 개인 프로젝트라도 사용자가 쓰기 시작하면 “내 컴퓨터에서는 되는데”로 끝낼 수 없다.

자동화는 속도보다 실수를 줄였다

CI/CD를 붙인 이유도 멋있어 보이기 위해서가 아니었다. 매번 수동으로 이미지를 만들고 서버에 들어가서 재시작하면, 언젠가 한 단계를 빼먹는다. 특히 앱 공개 전에는 작은 수정이 자주 생긴다. 문구를 바꾸고, API 응답을 다듬고, worker 간격을 조정하고, 다시 배포한다.

그때 배포 절차를 내 기억에 맡기면 수정할수록 불안해진다. 반대로 lint, test, build, 이미지 빌드, 배포 상태 저장소 갱신, ArgoCD 반영 순서가 정해져 있으면 작은 수정도 같은 길로 나간다. 자동화는 속도를 올리는 도구이기도 하지만, 내 실수를 줄이는 안전장치이기도 했다.

운영 환경을 만들며 가장 많이 한 일은 새로운 기술을 붙이는 것이 아니라 반복되는 실수를 줄이는 것이었다. 한 번 성공한 배포를 다음에도 거의 같은 방식으로 성공시키고, 문제가 생기면 같은 순서로 확인할 수 있게 만드는 일. 작은 서비스라도 이 정도 장치가 생기니 마음이 훨씬 편해졌다.

그 정도가 갖춰지고 나서야 출시 준비로 넘어갈 수 있었다. 서버가 운 좋게 떠 있는 게 아니라, 다시 배포하고 확인할 수 있어야 앱을 밖에 내놓을 수 있었다.

App Store에 올리며 약속을 다시 줄였다

돌아보면 출시는 기능을 계속 붙여 간 과정이 아니었다. 처음 붙잡은 “여의도 주차” 문제를 한강 나들이 전체로 넓히되, App Store에 보여줄 수 있고 실제로 감당할 수 있는 말로 다시 줄이는 과정이었다.

  • 05.12 - PRD 정리: 시작점은 여의도 주차장이었다. 위젯으로 “지금 갈 수 있나”를 빨리 확인하는 문제부터 붙잡았다.
  • 05.17 - 0.1.0: 주차 현황을 확인할 수 있는 첫 앱 형태가 생겼다.
  • 05.19 - 일반 모드: 행사, 시설, 공지, 길찾기를 더해 “나들이 전후에 볼 것”으로 범위를 넓혔다.
  • 05.20-23 - 운영 정리: API와 worker를 나누고, 배포와 관측을 같은 절차로 반복할 수 있게 했다.
  • 06.09-12 - 출시 준비: 번역, 스크린샷, 브랜드 사이트, App Store 메타데이터를 실제 기능에 맞췄다.

앱 기능이 어느 정도 완성된 뒤에도 바로 출시할 수 있는 건 아니었다.

App Store Connect가 요구하는 일은 생각보다 많았다. 앱 이름, 부제, 설명, 키워드, 개인정보 처리방침, 지원 페이지, 연령등급, App Privacy, 리뷰 노트가 필요했다.

스크린샷, TestFlight 빌드, 실제 기기 검증, 다국어 메타데이터도 빠질 수 없었다.

이 과정에서 한강자리의 표현도 많이 바뀌었다.

초기에는 “주차”가 훨씬 앞에 있었다. 하지만 공개 메타데이터에서는 “한강공원 나들이 정보를 한눈에”로 정리했다. 앱 설명도 주차만 앞세우지 않고, 11개 한강공원, 주차 여유, 혼잡 흐름, 공개 행사, 시설, 공지, 위젯, 알림을 함께 담았다.

브랜드 사이트도 만들었다. 처음에는 지원 페이지와 개인정보 처리방침 정도면 충분할 거라고 생각했지만, 앱을 모르는 사람이 들어왔을 때 “이 앱이 뭘 하는지”를 빨리 이해할 수 있는 페이지가 필요했다.

그래서 hangangjari.app에 다국어 브랜드 사이트를 만들고, 앱 화면과 위젯, 예측, 알림, 길찾기, 공식 데이터 출처가 보이게 했다.

App Store 홈 스크린샷

출시 전 마지막 며칠은 코드보다 검증과 정리가 더 많았다. 다국어 스크린샷을 만들고, App Store Connect에 올리고, 빌드 번호를 올려가며 TestFlight 후보를 만들었다.

최종 후보를 정한 뒤에는 릴리즈 태그와 공개 메타데이터를 맞췄다. App Store에 보이는 이름, 설명, 개인정보 고지가 실제 기능과 어긋나지 않는지도 다시 확인했다.

이때 분명해졌다. 출시 준비는 개발의 부록이 아니었다. 이름, 설명, 법적 고지, 개인정보 안내, 스크린샷, 지원 페이지, 브랜드 사이트는 사용자가 앱을 켜기 전에 먼저 만나는 제품이었다.

마지막에는 기능보다 설명을 줄였다

출시 막바지에는 기능을 더 넣는 것보다 설명을 줄이고 서로 맞추는 일이 많았다. App Store 설명은 길게 쓰면 오히려 흐려졌고, 스크린샷 문구는 짧아야 했다. 개인정보 처리방침과 지원 페이지는 과장 없이 정확해야 했다. 공식 앱이 아니라는 점도 숨기면 안 됐다.

개발자 입장에서는 이미 다 아는 내용이라 가볍게 넘기고 싶어진다. 하지만 사용자는 앱을 설치하기 전에 이름, 아이콘, 스크린샷, 설명, 개인정보 고지를 먼저 본다. 이 첫인상에서 신뢰를 주지 못하면 기능이 좋아도 시작이 어렵다.

그래서 출시 준비는 별도의 제품 개발처럼 느껴졌다. 코드가 돌아가는지 확인하는 일과, 모르는 사람이 이 앱을 믿고 설치할 수 있게 설명하는 일은 다른 종류의 일이다. 한강자리는 그 두 번째 일을 끝까지 해보는 연습이기도 했다.

운영과 출시가 따로 떨어진 단계는 아니었다. 서버가 안정적으로 돌아가야 App Store 설명도 실제 동작에 맞게 쓸 수 있고, 개인정보와 지원 페이지도 과장 없이 정리할 수 있다.

끝까지 공개하고 싶었던 앱

한강자리는 무료 앱이다. 로그인 없이 쓸 수 있고, 즐겨찾기는 기기에 저장된다. 위치 정보는 가까운 공원과 주차장을 정렬할 때만 기기 안에서 쓴다. 제품 품질 확인을 위한 익명 사용 이벤트는 있지만, 처음부터 불필요한 계정과 개인정보를 요구하지 않는 쪽으로 만들었다.

돈을 벌 앱은 아니다. 그래도 끝까지 만들고 싶었던 이유는 분명했다.

내가 불편했던 것을 고치고 싶었다. 그리고 그게 나에게만 필요한 것이 아니라면, 다른 사람에게도 나눌 수 있으면 좋겠다고 생각했다.

한강은 서울 사람에게는 너무 익숙한 장소다. 그래서 오히려 정보가 흩어져 있어도 다들 어떻게든 찾아간다. 하지만 차를 가져가거나, 처음 가는 공원을 고르거나, 외국인 관광객이 한강공원을 찾거나, 아이와 주말에 나가려는 상황이 되면 작은 정보 하나가 출발 여부를 바꾼다.

어느 공원에 갈지.
차를 가져가도 될지.
지금 사람이 많은지.
오늘 행사가 있는지.
화장실이나 편의시설은 어디 있는지.
길찾기는 어떤 앱으로 열면 되는지.

한강자리는 이런 질문에 단정적인 답을 대신 내려주는 앱이라기보다, 나가기 전 망설임과 도착 후 헤맴을 줄일 근거를 한곳에 모아주는 앱이 되고 싶다.

이번 프로젝트를 하면서 배운 것도 결국 거기에 가깝다.

  • 작은 불편을 끝까지 따라가면 생각보다 넓은 제품이 된다.
  • 공공데이터 앱은 데이터를 보여주는 데서 끝나지 않고, 데이터의 한계까지 설명해야 한다.
  • 클라이언트 구조는 화면 배치보다 앱, 위젯, 알림이 같은 정보를 이어받게 만드는 일이었다.
  • 백엔드는 API를 만드는 데서 끝나지 않고, 데이터 신뢰도와 수집 실패를 다루는 일까지 포함한다.
  • 위젯과 알림처럼 작은 화면일수록 무엇을 버릴지가 중요하다.
  • 배포, 관측성, 백업, 릴리즈 노트는 개인 프로젝트에서도 제품의 일부다.

내가 자주 되뇌는 말이 있다.

불편한 게 있으면 고치고, 쓸 만하면 나누자.

한강자리는 그 생각으로 만든 앱이다.

아직 부족한 점이 많다. 데이터는 더 정확해야 하고, 표현은 더 쉬워져야 하고, Android도 필요하다. 하지만 적어도 한강에 갈 때마다 매번 웹페이지를 열던 내 습관 하나는 앱과 위젯으로 바뀌었다.

그걸 다른 사람들도 편하게 쓸 수 있다면, 이 프로젝트는 충분히 의미가 있다.

결론은 소박하다. 그래도 한강자리에는 이 정도가 맞다. 큰 플랫폼을 세운 이야기가 아니라, 내가 자주 겪던 불편을 앱 출시까지 밀고 간 이야기다.

작은 서버를 운영하고, App Store에 제출하고, 설명을 정리하고, 다시 확인하는 일까지 모두 포함해서 하나의 출시였다. 출시일은 끝이 아니라, 이 작은 도구가 다른 사람의 휴대폰에서 계속 믿을 만하게 돌아가기 시작한 날이었다.

서버를 크게 만들었다는 이야기는 아니다. 작게 돌리는 서버라도 사용자가 붙는 순간에는 배포, 관측, 복구, 설명까지 제품의 일부가 된다. 공개한다는 건 기능을 보여주는 일인 동시에, 그 기능을 계속 책임질 수 있는지 확인하는 일이었다.

링크

이미지 확대