[프로젝트 회고] AI와 함께 덤핏(Dumpit!)을 만들다

Jieun Jung·2026년 10월 23일
post-thumbnail

AI와 함께 덤핏(Dumpit!)을 만들다


들어가며

4월 성장일지에서 개인 프로젝트를 진행 중이라고 소개하면서, 다음에는 프로젝트 회고를 써보겠다고 했다. 그리고 9월 성장일지에서도 덤핏 회고를 예고했다.

그 회고를 이제야 쓴다. 할 일을 정리하는 서비스를 만들어 놓고 정작 회고라는 할 일은 꽤 오래 미뤄두었다…. 이쯤 되면 나부터가 서비스를 더 열심히 써야겠다는 생각이 든다. 🙄

덤핏(Dumpit!)은 머릿속에 흩어진 할 일과 아이디어를 적고, 실행할 수 있는 형태로 정리하는 생산성 서비스다. 자유롭게 적은 글을 AI가 할 일 후보로 정리해 주고, 사용자는 이를 수정해 등록할 수 있다. 루틴, 달력, 뽀모도로를 통해 실행을 돕고, 완료와 집중 기록에는 작은 보상을 붙였다.

대시보드 이미지 2026년 10월 12일 기준 내가 실사용중인 웹 화면이다.


웹은 React, Windows 앱은 Electron, Android 앱은 React Native와 Expo로 구성했다. 세 클라이언트는 Spring Boot 백엔드와 PostgreSQL, Redis를 함께 사용한다. 개발 과정에서는 Claude Code와 Codex를 설계, 구현, 코드 검토와 검증에 활용했다.

아키텍처

만들다 보니 화면과 기능 외에도 생각할 일이 꽤 많았다.

  1. 낯선 기술을 어디까지 이해해야 적용할 수 있는지
  2. 백엔드는 어떤 정책을 책임져야 하는지
  3. 테스트와 실제 사용 사이에는 어떤 차이가 있는지
  4. 배포한 뒤에는 무엇을 챙겨야 하는지.

이번에는 이 네 가지 관점에서 덤핏을 돌아보려고 한다.




Agent 주도 개발: 처음 써보는 기술을 프로젝트에 넣는다는 것


구현할 수 있는 범위가 넓어졌다

AI Agent를 활용하면서 고민하게 된 것 중 하나는 이것이었다

사용해 본 적 없는 기술을 어떻게 프로젝트에 녹여낼 것인가?

AI Agent를 활용해서 개발을 할 때의 큰 장점은 낯선 기술이 필요할 때 구조를 설명해 달라고 하고, 기존 코드와 연결할 방법을 검토하고, 작은 구현부터 시도할 수 있다는 것이다. 특히 이번에 개발한 덤핏처럼 웹과 데스크톱, 모바일을 함께 다루는 프로젝트에서는 이런 도움이 컸다. 내게는 익숙하지 않은 안드로이드 앱, 네이티브 위젯이나 빌드 설정으로 넘어가도, 일단 무엇을 살펴봐야 할지 질문할 수 있었기 때문이다.

다만 구현 범위가 넓어지는 만큼 내가 검토해야 할 범위도 넓어졌다. 실행되는 코드를 받았을 때 그 코드가 기존 기능과 어떤 관계인지 설명할 수 있는지는 또 확인해야 했다. 무엇보다도 우려되었던 건, 내가 처음 접하는 영역에서는 결과가 그럴듯한지 판단할 기준이 매우 부족하다는 점이었다. 에이전트의 설명을 읽으면 이해한 것 같은데, 조금 다른 문제가 생겼을 때 무엇을 확인해야 하는지 모르겠다는 생각도 많이 들었다.

완벽히 이해했어 짤방

AI Agent 시대가 되면서 기술에 대한 깊은 이해가 필요하진 않아졌지만, 명확히 이해하고 개발하는 것에 비해 우려할 부분이 많아졌다.

그래서 나는 아래와 같이 생각하고 행동하기로 했다.


내가 설명할 수 있는 지점을 만들자

모든 기술을 깊이 공부한 뒤에야 손댈 수 있다고 생각하면 요즘 같은 시대에 개발을 시작하기 어렵다. 그렇다고 필요한 기술이나 설정에 대해 AI에게만 맡기고 넘어가면, 나중에 내가 모르는 것들이 잔뜩 쌓여 확인이 더욱 힘들어진다. 그래서 낯선 기술을 적용할 때 내가 확인할 기준을 다음처럼 정리해 보았다.

  • 왜 필요한가. 지금 해결하려는 문제에 이 기술이 어떤 역할을 하는가.
  • 어디와 연결되는가. 데이터와 상태가 어느 경계를 지나고, 기존 구현의 무엇을 바꾸는가.
  • 어떻게 실패하는가. 문제가 생기면 어느 로그와 결과물을 확인해야 하는가.
  • 어떻게 검증하는가. 원하는 동작을 확인할 방법과 기존 방식으로 돌아갈 방법이 있는가.

Desktop 앱과 Android 앱을 만들 때 특히나 이런 기준이 도움이 되었다. 내 경우는 Java 백엔드로 개발을 시작했고, 웹개발이 주력이었기 때문에 앱 개발 경험이 없었다. 그렇기에 AI와 질답을 주고받으며 기술스택을 선정했다.

우선 데스크톱에서는 기존 React 화면을 재사용하면서 트레이와 별도 타이머 창을 붙일 수 있도록 Electron을 사용했다. Android에서는 React 경험을 활용할 수 있는 React Native와 Expo로 화면을 구성하고, 데이터 처리는 기존 Spring Boot API에 연결했다.

이때 기술 이름을 아는 데서 한 걸음 더 들어갈 필요가 있었다. Electron에서는 화면과 운영체제 기능이 어떤 통로로 요청과 상태를 주고받는지, Android에서는 네이티브 Google 로그인이 기존 서버의 인증과 어떻게 이어지는지가 확인할 지점이었다. 문제가 생겼을 때도 화면의 오류인지, 네이티브 기능의 오류인지, 서버 요청의 실패인지 구분할 수 있어야 했다. 특히 로그인·알림·홈 화면 위젯처럼 네이티브 구성이 필요한 기능은 개발 서버에서 화면이 뜨는 것만으로 검증할 수 없고, 실제 설치한 앱에서 확인해야 한다는 점도 배워야 했다.

에이전트에게 설명과 구현을 요청하더라도 공식 문서의 적용 조건과 현재 프로젝트 설정을 대조하고 작은 단위로 확인할 필요가 있다. 이 과정을 거쳐 내가 설명할 수 있는 부분을 조금씩 넓히는 것이 낯선 기술을 사용하면서도 학습을 놓치지 않는 방법이라고 생각했다.


Agent 주도 개발에서는 맥락 전달도 중요하다

프로젝트가 커지면서는 에이전트에게 어떤 맥락을 전달할지도 중요해졌다. 이미 정한 정책, 바꾸면 안 되는 동작, 함께 확인해야 하는 다른 클라이언트가 있었기 때문이다.

덤핏은 초기에 Claude Code를 메인으로 작업했었다. Fable 5를 메인 오케스트레이터로 두고, Sonnet나 opus, codex를 서브로 사용하는 형태였다. 각각의 세션에서 모두 지켜야 할 작업 규칙은 CLAUDE.md와 메인 메모리에 담겼다. 세션별 하위 규칙들은 세션의 메모리에 두었다.

그러다 OpenAI가 Astra를 출시하면서 Codex를 메인 에이전트로 사용하게 되었는데, 이를 위해 지침과 메모리의 전반적인 마이그레이션을 진행했다. 물론 지정을 잘 시킨다면 Codex에서도 기존 Claude Code용 파일들을 읽어올 순 있겠지만 토큰이 자잘하게 낭비된다는 생각이 들었다. 그래서 그동안의 작업 맥락과 지침을 담은 인수인계서를 Claude Code에게 작성하게 했고, 이를 Codex에게 읽게 해서 지침과 맥락을 이관했다. 최종적으로 AGENTS.md와 제품 결정, 구조, 현재 상태를 정리한 문서를 두었다. 작업 전에는 현재 코드와 테스트, Git 이력을 확인하고, 구현 계획을 검토한 뒤 변경하는 흐름을 사용했다.

클로드 코덱스 하이파이브 이후로도 둘이 꽤나 성공적인 협업을 하고 있었다.


당연하게도 이 문서들이 모든 문제를 막아주지는 않는다. 오래된 내용은 오히려 다음 작업을 헷갈리게 할 수 있어서, 실제 코드와 맞는지 확인하는 일도 필요하다. 그래도 매번 대화의 처음으로 돌아가 프로젝트를 다시 설명하는 부담을 줄이고, 내가 무엇을 승인했는지 돌아볼 기준은 만들어 줬다.

AI를 통해 시도할 수 있는 일이 많아진 것은 분명하다.
앞으로 더 신경 쓰고 싶은 것은, 그 과정에서 얻은 이해를 내 쪽에도 남기는 일이다.




백엔드 개발: 기능들이 만나는 지점의 정책


하나의 서버를 여러 클라이언트가 사용할 때

백엔드 개발자를 목표로 하는 입장에서 덤핏 개발은 기능 사이의 관계를 생각해 볼 기회였다. API 하나의 정상 응답을 확인한 뒤에도, 그 요청이 어떤 상태에서 들어오는지와 이후 어떤 요청이 이어지는지를 살펴야 했다.

인증이 한 예다. 덤핏은 웹과 모바일에서 같은 서버 세션 모델을 사용한다. Android의 네이티브 Google 로그인으로 받은 ID 토큰을 서버에서 검증한 뒤, 웹과 같은 형태의 서버 세션으로 연결한다.

이때 사용자를 식별하는 기준도 맞아야 했다. 웹에서 연결한 Google Calendar 토큰을 모바일에서도 찾으려면 인증 주체의 식별값이 일치해야 했고, 실제로 모바일의 식별값을 웹과 같은 Google sub로 맞추는 수정이 있었다. 각 로그인 방식이 성공하는 것과, 같은 계정의 기능들이 이어서 동작하는 것은 함께 확인해야 할 문제였다.


Redis를 붙일까, 말까

Redis를 사용할 것인지도 고민거리였다. 도입하면 관리할 구성 요소가 하나 늘어난다. 필요가 없는데도 '자주 쓰는 NoSQL'이라는 이유로 도입한다면 기술부채가 된다. 도입하지 않는다면 세션과 사용량 같은 상태를 어디에 두고, 서버가 다시 시작될 때 어떻게 다룰지 정해야 한다. '기술을 많이 사용했다' 라는 만족감만으로 기술스택을 추가하는 것은 프로젝트를 대여섯번 즈음 한 시점에선 그만할 때가 되었다.

현재 덤핏에서 Redis는 다음 네 가지 케이스에 사용된다.

  • 로그인 세션
  • Google 연동용 OAuth 토큰
  • AI 일일 사용량
  • 푸시 알림의 중복 방지

내 프로젝트에서 Redis를 도입한 이유를 설명하려면 이런 역할부터 짚어야 한다.

Redis에 어떤 상태를 저장하는지 정리하고 나니, Redis를 사용할 수 없을 때 각 기능이 어떻게 동작해야 하는지도 정해야 했다. 로그인 세션을 읽지 못하는 것, AI 사용량을 확인하지 못하는 것, 이미 보낸 알림인지 확인하지 못하는 것은 사용자에게 서로 다른 영향을 주기 때문이다.

예를 들어, 현재 덤핏의 AI 일일 사용량 제한은 Redis 오류가 발생하면 경고만 남기고 유저의 AI 사용 요청은 허용한다. 사용량을 정확히 제한하지 못하더라도 서비스 이용을 이어가게 하는 선택이다.

덤핏의 AI 사용량 로직

  1. DB에서 사용자를 조회한다.
  2. Redis에서 오늘 사용 포인트를 증가시키고, 일일 한도를 넘었는지 검사한다.
  3. 여기서 Redis 오류가 나면 경고와 Sentry 보고를 남긴다.
  4. DB에 기능 종류, 사용 포인트, 허용 여부, REDIS_UNAVAILABLE 사유를 기록한다.
  5. DB 기록이 정상적으로 끝나면 AI 처리를 진행한다.

Redis 장애 시 DB에 이력을 남기고 AI 기능을 계속 허용하도록 구현했지만, DB에 사용 이력이 남는 것과는 별개로 그 이력으로 한도를 제한하고 있지는 않다. 이럴 경우 장애 발생 시 생겨나는 추가적인 사용량은 고스란히 운영자인 내가 떠맡게 된다. 아직은 사용자 수가 별로 없어 괜찮지만, 추후 사용자가 늘어나면 내가 감당할 수 있는 비용의 경계도 함께 정해야 할 것이다.

반면 푸시 알림은 중복 여부를 확인하지 못하면 발송을 건너뛴다. 이 경우에는 알림을 놓칠 가능성을 감수하더라도 같은 알림을 반복해서 보내는 일을 피하도록 했다. 알림을 못 받은 경우에 느낄 불편함과 같은 알림이 계속 올 때 느낄 불편함을 저울로 쟀을 때, 후자가 더 신경쓰이고 불편하리라 판단했다. 같은 Redis 장애라도 기능마다 감수하기로 한 문제가 달랐다.

이처럼 Redis 도입 여부를 판단하려면 구체적인 질문이 필요했다. 어떤 상태를 맡길 것인지, 그 상태를 읽거나 저장하지 못하면 사용자에게 무슨 일이 생기는지, 그때 요청을 허용할지 멈출지까지 생각해야 했다. AI 에이전트에게 기술 선택을 물을 때도 이런 조건을 먼저 정리해야 답을 내 프로젝트에 적용할 수 있겠다고 느꼈다.


탈퇴한 계정이 다시 살아난 이유

회원 탈퇴와 자동 재로그인이 만났을 때는 더 명확한 문제가 생겼다.

덤핏은 탈퇴 후 30일의 유예 기간을 두고, 그 안에는 복구할 수 있도록 했다. 한편 모바일에는 세션이 만료되면 조용히 다시 로그인해 사용 흐름을 이어주는 기능이 있었다.

그런데 탈퇴 직후 발생한 인증 오류를 일반적인 세션 만료로 처리하면서 자동 재로그인이 실행됐다. 서버는 그 로그인을 유예 기간 안에 돌아온 사용자로 받아들였고, 방금 탈퇴한 계정을 복구해 버렸다.

탈퇴 후 재로그인 시 문제가 되었던 로직

서버에서 탈퇴 처리 완료 후...
→ 앱이 로그아웃 정리 과정에서 푸시 토큰 해제를 요청
→ 서버는 이미 탈퇴한 계정이므로 401 + SESSION_INVALIDATED 응답
→ 앱은 응답 코드의 의미를 구분하지 않고 자동 재로그인
→ 서버는 유예 기간 중 로그인이라는 이유로 계정 복구

덤핏은 회원 탈퇴 후 30일 동안 계정을 복구할 수 있도록 했다. 그런데 운영 로그에서 탈퇴 직후 0.2초도 지나지 않아 계정이 복구되는 현상이 나타났다. 탈퇴 처리가 실패한 것이 아니라, 정상적으로 처리된 탈퇴를 앱이 곧바로 되돌리고 있었다.

시작점은 탈퇴 후의 로그아웃 정리 과정이었다. 앱이 푸시 알림용 기기 토큰을 해제하려고 요청했지만, 서버는 이미 탈퇴한 계정이므로 접근을 거부했다. 서버는 SESSION_INVALIDATED라는 코드로 이를 구분했지만 앱은 401 응답을 일반적인 세션 만료로 받아들여 자동 재로그인을 실행했다. 당시 서버는 유예 기간 안에 로그인하면 복구 의사가 있다고 판단했기 때문에, 이 자동 로그인으로 방금 탈퇴한 계정을 되살렸다.

수정은 앱과 서버 양쪽에서 진행했다. 앱은 계정 비활성화 응답에 자동 재로그인을 하지 않도록 했고, 탈퇴 후에는 이미 서버에서 삭제한 기기 토큰을 다시 해제하지 않게 했다. 서버에도 복구 동의를 별도로 전달하도록 바꿔, 자동 로그인으로 탈퇴를 취소할 수 없게 했다. 사용자가 직접 로그인하더라도 복구 여부를 묻고 동의한 경우에만 계정을 복구하도록 보완했다.

이 문제에서 놓친 것은 '다시 인증할 수 있다'는 사실과 '탈퇴를 취소하고 싶다'는 의사의 차이였다. 로그인 편의를 위한 자동화가 계정 상태까지 바꿀 수 있었던 만큼, 서버에서도 복구 동의를 독립적인 조건으로 확인해야 했다.


HTTP 상태 코드 너머의 의미

이 사례 이후에는 인증 성공과 사용자의 의도를 명확히 구분해서 생각하게 되었다. Google 로그인으로 본인임을 확인했다고 해서, 탈퇴를 취소하는 데도 동의한 것은 아니었다. 서버가 로그인 요청만 보고 복구 의사를 추정했던 것이 문제였다. 유저가 탈퇴했던 계정을 복구할 것인지 아닌지는 유저의 선택에 맡기는 것이 옳았다.

인증 오류를 처리하는 기준도 구체화됐다. 세션이 만료됐다면 다시 인증해서 이용을 이어갈 수 있지만, 탈퇴하거나 정지된 계정이라면 자동으로 접근을 되살려서는 안 된다. 클라이언트는 응답에 담긴 이유에 따라 재시도 여부를 결정하고, 서버는 복구 동의가 없는 요청으로 계정 상태가 바뀌지 않도록 해야 했다.

AI 에이전트에게 기능을 맡길 때도 이런 조건을 함께 전달할 필요가 있었다. '세션이 만료되면 자동으로 다시 로그인한다'는 요구사항에 '탈퇴한 계정이 로그인을 시도했다고 해서, 탈퇴 요청을 자동으로 취소하고 데이터를 복구하지 않는다'는 경계까지 포함하는 것이다. 정상적으로 동작하는 경로와 함께, 자동화가 멈춰야 하는 지점도 정리하고 테스트로 확인해야 한다는 기준이 생겼다.




사용자 테스트와 최적화: 실제로 쓰는 상황에서 확인하기


개발환경뿐만 아니라 유저가 사용할 때도 잘 동작해야 한다

웹과 데스크탑 앱 서비스는 6월 경에 처음 선보였고, 9월 성장일지에 Android 비공개 테스트를 마치고 프로덕션 심사를 신청했다고 적었다. (현재는 안드로이드 앱도 Google Play에서 받을 수 있다!)

웹, 데스크탑 앱, 안드로이드 배포 파일을 만들고 사용 흐름을 확인하는 과정에서는, 기능별 정상 동작 외에도 살펴볼 조건이 많았다. 즉, 사용자 입장에서 어떤 것이 불편한지 파악하는 것도 '잘 동작하게 하는 것'의 일부였다.

내가 실사용했다는 증거로 히트맵을 남긴다 2026년 10월 21일 기준 내 계정의 히트맵이다. 서비스를 처음 올린 뒤로 실제로 꾸준히 사용하면서 개선점을 찾고있다.


예를 들어, 브레인 덤프를 분석하고 등록한다는 로직만으로는 내가 썼던 초안이 보존되는지 여부를 확인할 수 없다. 작성 중 화면을 옮기거나 새로고침했을 때, 분석에 실패했을 때, 후보를 수정하다 취소했을 때도 기존에 작성했던 초안이 남아있는 것이 사용자 입장에서는 옳다. 이 사실을 내가 몇 번 사용해보고 나서야 깨달았다.

AI가 잘 분석하는지 테스트하려고 엄청 길게 작성해놨는데, 그게 일시적인 서버 오류로 통째로 날아갔을 때의 처참함이란….🫠

이 때 느낀 불편함을 해소하고자, 웹에서 작성한 초안을 유저의 localStorage 에 저장하기로 했다. 데스크탑 앱은 electron을 사용하고 있고 웹을 미러링하는 형태였기에 동일하게 localStorage를 사용할 수 있었고, 안드로이드 앱의 경우 asyncStorage를 사용하도록 별도의 처리를 했다.

설정 화면에서도 비슷한 기준이 필요했다. 예를 들어 활동 시간이나 태스크의 내용을 바꾸다가 저장에 실패했다면, 오류를 알리는 동시에 방금 입력한 값도 남겨 두어야 다시 저장할 수 있다. 서버에서 기존 설정을 늦게 받아오더라도 그사이 사용자가 수정한 값을 덮어쓰면 안 된다. 그래서 저장 성공 여부와 화면에 보이는 입력값을 구분하고, 실패했을 때도 작업을 이어갈 수 있도록 처리했다.

화면이 잘 보이고 버튼이 제대로 눌리는지 외에도 확인할 것이 많았다. 좁은 화면이나 큰 글자 설정에서 내용이 잘리지 않는지, 키보드로 원하는 버튼까지 이동할 수 있는지 등을 살폈다. 반복해서 확인할 수 있는 조건은 AI가 직접 테스트할 수 있게 브라우저 조작 권한을 주었다.

스크린리더의 보조가 필요한 유저를 위해 스크린리더가 잘 읽을 수 있도록 처리 과정을 거치기도 했다. Agent를 통해 실제 웹에서의 동작을 검증했고, 앱에서의 검증은 usb 디버깅 모드로 진입해 사용자 테스트를 맡겼다. 최종적으로 내가 한 번 사용해보는 것도 잊지 않았다.

앞으로 사용자 피드백을 받을 때도 "어느 화면이 불편했나요?"에서 한 걸음 더 들어가고 싶다. 문제가 생기기 직전에 무엇을 했는지, 어떤 결과를 기대했는지, 실제로는 어떻게 달랐는지를 함께 기록하려 한다. 문제가 된 부분을 내가 다시 확인할 수 있어야 불편함을 제대로 개선할 수 있기 때문이다.


앱을 만들면서 화면도 다시 만들었다

처음 안드로이드 앱을 만들 때에는 웹에서 만든 것을 어디까지 가져갈 수 있는지부터 다시 생각하게 됐다. 데스크탑 앱은 웹의 화면을 띄워주기 때문에 화면 사이즈와 로그인 유지 정도만 생각하면 됐지만, 안드로이드 앱의 경우는 전혀 달랐다. 웹과 기능은 동일하기 때문에 같은 API와 DB를 이용하지만, 작은 휴대폰 화면이라는 특성, 그리고 React와는 조금 다른 React Native의 특성에 맞춰 컴포넌트를 재배열해야했다. (이미 한 번 만든 화면인데 다시 만들 일이 이렇게 많을 줄은 몰랐다;;)

웹에서 익숙하게 쓰던 스타일이나 라이브러리를 앱에서도 사용할 수 있는지, 마우스로 조작할 때와는 다르게 터치로 했을 때, 휴대폰의 자판을 사용할 때도 흐름이 편한지 확인해야 했다. 같은 기능을 구현하더라도 기기마다 화면이나 조건이 조금씩 달랐다. 호환성을 확인하는 일 또한 개발자의 역할이라는 것을 안드로이드 앱 테스트할 때 여실히 깨달은 것 같다. 웹에서의 완성도가 앱의 완성도를 보장하지는 않았다.

웹과 데스크탑 앱, 안드로이드 앱 화면 비교

앱내의 이미지를 불러올 때에도 차이가 드러났다. 웹에서는 깔끔하게 보이던 이미지가 앱에서는 흐릿하게 보이기도 했고, 반대의 경우도 있었다. 덤핏처럼 픽셀 도트 컨셉의 화면에서는 이런 차이가 명확히 눈에 들어왔다. 심지어 기능은 동일한데 완성도가 부족해 보이기까지 했다.

웹에서는 작은 도트 이미지를 확대할 때 픽셀 경계를 유지하도록 image-rendering: pixelated 라는 설정의 CSS를 적용했다. 하지만 React Native에서는 같은 설정을 그대로 사용할 수 없었다. 휴대폰에서는 화면에 지정한 크기보다 더 많은 물리 픽셀로 이미지를 그리기도 한다. 이 과정에서 주변 픽셀의 색을 섞어 확대하면, 도트 특유의 또렷한 경계가 흐려질 수 있었다.

안드로이드 앱에서의 이미지 문제 재현

따라서 원래 색을 섞지 않는 최근접 방식으로 미리 확대한 표시용 이미지를 만들었다. 애니메이션도 프레임별로 나누어 표시용 이미지를 생성하고, Android가 이를 다시 낮은 해상도로 처리하지 않도록 보완했다.

데스크톱에는 다른 문제가 있었다. 덤핏 데스크톱 앱은 빌드된 웹 화면과 이미지를 설치 파일에 포함한다. 따라서 웹의 이미지를 교체해도 이미 설치된 앱에는 예전 이미지가 남아 있었다. 최신 소스에서 표시 상태를 확인한 뒤, 새 이미지가 실제 설치 파일 안에도 포함됐는지 검사하며 다시 패키징했다.

이 경험으로 이미지의 원본과 표시 방식, 사용자에게 전달된 파일까지 함께 확인하게 됐다. 같은 그림을 사용한다고 해서 다른 환경에서도 내가 보던 것과 같은 모습이 보장되지는 않았다.


느리다는 '느낌'을 실측으로 증명하고 해결하기

Case 1: Web

웹에서는 처음 접속한 랜딩 페이지부터 미묘하게 느리다는 느낌을 받았다. 휴대폰으로 웹에 접속하면 픽셀 폰트가 적용되기 전에 기본 폰트가 먼저 보이기도 했다. 첫 화면에서 무엇을 기다리고 있는지 알아보기 위해 Lighthouse로 로딩 성능을 측정하고, 네트워크 전송량과 빌드 산출물의 크기를 확인했다.

랜딩의 로고는 화면에 표시하는 크기에 비해 큰 PNG였고, 픽셀 폰트도 전체 글리프를 내려받고 있었다. 랜딩에서 당장 사용하지 않는 다른 페이지의 코드와 Sentry SDK까지 초기 번들에 포함되어 있었다. 첫 화면을 보여 주기 위해 필요 이상으로 많은 자원을 받고 있었던 것이다.

로고는 표시 크기를 고려해 줄이고 WebP로 변환했다. 픽셀 폰트는 사용하는 한글과 문자 범위로 서브셋을 만들고, 미리 다운로드를 시작하도록 했다. 다른 페이지는 이동할 때 코드를 불러오도록 분리하고, Sentry도 초기 화면 로딩 이후에 초기화하도록 바꿨다.

개선 전후의 프론트엔드를 각각 프로덕션 빌드한 뒤, 같은 PC에서 vite preview로 실행해 랜딩 페이지를 비교했다. Lighthouse의 모바일 에뮬레이션과 simulated throttling 설정도 동일하게 유지했다. 휴대폰으로 접속한 웹 환경을 가정한 측정으로, 화면은 412×823, 네트워크는 RTT 150ms·대역폭 약 1.6Mbps, CPU는 4배 느린 조건이었다.

지표개선 전개선 후
Lighthouse Performance5669
LCP17.4초5.7초
총 전송량2,947KiB790KiB

첫 로드에서 전송한 용량은 약 73% 줄었고, 해당 조건에서 계산된 LCP도 낮아졌다. 다만 저장된 결과는 전후 각 1회 측정이며, 실제 사용자의 평균 속도를 의미하지는 않는다. 여러 개선을 함께 적용했으므로 각 변경이 시간을 얼마나 줄였는지도 구분하지 못했다.

물론 5.7초를 보고 이제 충분히 빠르다고 박수칠 단계는 아니었다. 그래도 처음의 막연한 느낌을 이미지·폰트·JavaScript의 크기와 로딩 순서라는 구체적인 문제로 바꿔 볼 수 있었다.

Case 2: Android

Android에서는 실행 속도와 별개로 배포 파일의 크기를 최적화 대상으로 삼았다. 기능을 유지하면서 사용하지 않는 코드와 리소스를 줄일 여지가 있는지 확인하는 작업이었다.

R8을 이용한 코드 축소·최적화·난독화와 리소스 축소를 적용했다. 효과를 확인하기 위해 같은 제품 소스와 의존성을 기준으로, 해당 설정을 끈 빌드와 켠 빌드를 각각 만들었다. Java·Android SDK·지원 CPU 아키텍처·서명 조건을 맞추고, Gradle로 생성한 release APK와 AAB의 파일 크기를 비교했다.

파일최적화 OFF최적화 ON감소율
APK115,017,434 bytes101,824,125 bytes11.47%
AAB82,092,527 bytes77,379,713 bytes5.74%

비교 대상은 로컬 검증용 인증서로 서명한 release 빌드였다. 서로 다른 출시 버전 사이의 차이가 아니라, 같은 소스를 기준으로 최적화 설정을 바꿨을 때의 차이를 확인한 것이다.

여기서 확인한 개선은 APK와 AAB의 파일 크기 감소였다. 앱의 시작 시간이나 화면 반응 속도, Google Play에서 사용자가 실제로 내려받는 용량은 측정하지 않았다. 따라서 이번 결과는 '앱이 빨라졌다'보다 '배포 파일의 크기를 줄였다'고 설명하는 것이 정확하다.

최적화 이후 위젯에서 이미지가 사라졌다

파일이 작아졌다고 최적화가 끝난 것은 아니었다. 이후 Android 1.2.2에서 통합 리소스 축소를 추가로 켰을 때, 설치된 앱의 위젯 외형이 달라지고 글자가 보이지 않는 문제가 보고됐다.

소스 묶음에는 위젯 PNG가 들어 있었다. 그러나 정상 버전과 문제 버전의 AAB 내부를 비교하니, 이전에 존재하던 필수 위젯 이미지 이름 69개가 문제 버전에서는 모두 빠져 있었다. 위젯이 getIdentifier로 이름을 통해 이미지를 찾는 코드와 빌드 설정을 대조했고, 통합 리소스 축소 과정에서 해당 이미지들이 제거된 것으로 원인을 좁혔다.

위젯 이미지에 tools:keep="@drawable/w_*" 보존 규칙을 추가하고, 정상 버전·문제 버전·수정 버전의 AAB에 같은 검사를 적용했다. 문제 버전은 필수 리소스 누락으로 실패했고, 수정한 1.2.3에서는 이미지 이름뿐 아니라 실제 그림과 참조 연결까지 확인했다. 실제 기기에서 위젯이 정상적으로 표시되는지는 별도의 확인 과제로 남겼다. (추후 확인하니 정상적으로 표시됐다. 휴~)

AI가 작성한 테스트를 검토할 때도 같은 기준이 필요했다. 테스트 개수에 안심하기 전에, 이번에 바꾸는 설정이 어떤 문제를 일으킬 수 있고 그 문제를 실제로 검사하는지 살펴야 했다. 이번에 발견된 위젯 리소스 누락도 이후 배포 파일을 검사할 때 반복해서 확인할 조건으로 남겼다.




운영: 배포 이후에도 계속되는 개발


RDS 비용 때문에 Supabase로 옮기기

AWS RDS를 사용하다가 비용 부담 때문에 Supabase로 데이터베이스를 옮긴 일도 있었다. 실사용자가 붙고 수익화가 가능해지기까지 내가 매달 감당할 수 있는 비용 이 기술 선택의 현실적인 기준이 됐다. 기능을 얼마나 더 만들 수 있을지만을 고민하다가, 일단 다음 달에도 유지할 수 있는지부터 생각하게 되는 순간이었다. (로그를 포함해도 데이터도 별로 없던 첫달에 백업용 DB 사용 금액까지 해서 몇 만원의 금액이 청구되는 것을 보고 당황했다.)

이전 후에도 Spring Boot가 JDBC로 PostgreSQL에 접근하는 구조를 유지했다. 사용자 인증과 서비스 권한 판단은 기존 백엔드가 담당한다. Supabase Auth나 Data API를 사용하는 구조로 바꾸지는 않았으며, 사용하지 않는 Data API는 비활성화했다. Supabase를 사용한다고 해서 클라이언트가 Supabase Auth나 Data API로 직접 데이터를 다루도록 바꾼 것은 아니다.

위의 설정을 제외한 RDS와 Supabase의 차이를 정리하면 다음과 같다. 비용의 경우 상품 이름만으로 비교하기보다 선택한 구성과 사용량을 함께 봐야 한다.

비교할 지점AWS RDSSupabase
과금인스턴스 가동 시간과 할당한 저장 공간 등이 기본이며, 구성에 따라 백업·전송 비용도 고려해야 한다. RDS 과금 문서요금제와 프로젝트별 컴퓨팅 자원, 사용량·부가 기능을 함께 봐야 한다. 유료 요금제에도 추가 과금 항목이 있다. Supabase 과금 문서
DB 접근 제어VPC 안에 비공개 DB를 두고 보안 그룹으로 접근을 제한하는 구성을 할 수 있다. RDS 네트워크 문서PostgreSQL과 풀러에 접속 가능한 IP 범위를 제한할 수 있다. 이 제한은 HTTPS API에는 적용되지 않으므로 API 접근 제어를 별도로 봐야 한다. Supabase 네트워크 문서

사용 편의에서도 확인할 것이 있었다. Supabase는 대시보드에서 연결 정보를 제공하지만, 직접 연결과 커넥션 풀러의 연결 방식이 다르다. 기본 직접 연결은 IPv6를 사용하므로 IPv4만 가능한 환경에서는 세션 모드 풀러 등의 선택지를 검토해야 한다. 트랜잭션 모드 풀러에는 prepared statement 같은 기능의 제약도 있다. 덤핏처럼 계속 실행되는 백엔드에서는 서버의 네트워크 환경과 JDBC 설정에 맞는 방식을 골라야 한다. 같은 PostgreSQL을 사용하더라도 연결 방식과 구조에 차이가 있다. Supabase 연결 문서

또 요금제를 고를 때는 저장 공간과 연결 수, 백업 보관·복구 범위도 함께 비교할 필요가 있다. 내 경우는 아직 실사용자가 많이 확보되지 않은 상황이라 무료 요금제를 사용하고 있는데, 이후 사용자와 데이터가 늘어나면 다른 곳으로 마이그레이션하거나, 요금제를 이용하고자 한다.

DB를 포함해 여러가지 요소들을 꼼꼼히 따져가며 선택할 수 있어야한다는 것을 배웠다.


DB를 옮기면서 함께 봐야 할 보안의 경계

위에 언급했던 것처럼, Supabase에서는 DB 연결 외에 자동으로 제공되는 Data API도 고려해야 한다. 덤핏처럼 Spring 백엔드를 통해 데이터를 다루는 구조에서는, 이 별도 경로의 공개 범위와 권한이 중요한 확인 대상이다. 백엔드의 인증 검사는 백엔드를 거치는 요청에 적용되기 때문이다.

공식 문서도 Data API를 사용하지 않는 애플리케이션에는 해당 기능을 끄는 방법을 안내한다. 사용한다면 노출하는 테이블의 권한과 RLS(행 단위 접근 정책)를 구성해야 한다. 내가 API를 호출하지 않는다는 것과 외부에서도 접근할 수 없다는 것은 따로 확인할 사항이었다. Supabase Data API 보안 문서

RLS 역시 인증 구조와 함께 이해해야 한다. Supabase Auth의 사용자 정보를 활용하는 정책을 덤핏의 Spring 세션 구조에 그대로 가져온다고 두 인증이 연결되는 것은 아니다. 현재 구조를 기준으로 보면, 요청한 사용자가 해당 데이터에 접근할 수 있는지는 백엔드에서 계속 책임져야 한다. DB 정책을 추가할 때에도 접속 역할과 사용자 정보를 전달하는 방식을 함께 설계할 필요가 있다. Supabase RLS 문서

연결 보안에서는 TLS 암호화와 서버 인증서 검증도 구분해야 한다. 예를 들어 공식 문서의 verify-full은 인증서와 호스트 이름까지 검증한다. 접속 성공 여부에 더해 어떤 수준으로 연결을 보호하는지 확인할 기준이 된다. Supabase SSL 문서

이 부분을 정리하면서 남은 질문은, 새 환경에서 데이터가 어느 경로로 오가고 각 경로를 누가 통제하는지였다. 관리형 서비스를 선택해도 내 서비스의 인증 구조와 접근 범위는 내가 이해해야 한다. 비용을 줄이기 위해 시작한 이전이 백엔드와 인프라의 경계를 다시 살펴보는 계기가 됐다.


배포 과정도 사용자에게 영향을 준다

웹은 Cloudflare Pages, 백엔드는 GitHub Actions를 이용해 자동으로 배포하도록 구성했다. 자동화해 두면 다음 배포부터 편해질 것이라 기대할 수 있지만, 그 과정이 서비스에 어떤 영향을 주는지도 살펴야 했다.

실제로 배포할 때 전체 컨테이너를 내렸다 올리는 방식에서, 백엔드만 다시 빌드하고 교체하는 방식으로 바꾼 이력이 있다. 세션을 관리하는 Redis까지 함께 내려가면서 배포 때마다 로그인 상태에 영향을 주는 것을 줄이기 위한 변경이었다. 배포 명령의 범위가 사용자의 편의성으로 이어지는 사례였다.

한편 워크플로에 성공 표시가 떠도 로그를 더 봐야 하는 경우가 있었다. 서버의 Git 동기화가 기존 변경 때문에 중단됐는데, 뒤의 명령이 성공해 전체 단계는 성공으로 표시된 것이다. 당시 배포에서는 새 JAR 업로드와 컨테이너 시작, 공개 웹의 새 파일과 API 상태를 별도로 확인했다. (서버 작업 사본의 동기화와 중간 실패를 전체 결과에 반영하는 개선은 후속 과제로 남았다.)

현재 배포가 시작되는 시점은 PR을 승인했을 때가 아니라, 실제로 main에 병합했을 때다. main이 갱신되면 GitHub Actions의 CI와 백엔드 배포, Cloudflare Pages의 웹 배포가 각각 실행된다. 이 작업들은 독립적으로 진행되므로 웹과 백엔드 중 어느 쪽이 먼저 반영될지는 정해져 있지 않다. 현재 백엔드 배포는 main의 CI 성공을 기다리도록 연결돼 있지도 않다.

백엔드는 GitHub Actions에서 새 JAR을 만든 뒤 EC2로 전달하고, 서버에서 Docker 이미지를 빌드한다. 이때까지는 기존 백엔드가 계속 요청을 처리한다. 이후 기존 컨테이너를 중지하고 새 컨테이너를 실행하는데, 새 Spring Boot가 요청을 받을 준비를 마칠 때까지는 API를 사용할 수 없는 구간이 생긴다. Redis를 유지해 로그인 세션을 보존하더라도, 백엔드 교체 중의 일시적인 요청 실패까지 없어지는 것은 아니었다. 같은 API를 사용하는 웹과 Android, 데스크톱 앱 모두 이 영향을 받는다.

내가 실사용했다는 증거로 히트맵을 남긴다 내 프로젝트에서 GitHub 레포지토리의 main에 작업을 병합하고 나면 벌어지는 일들을 순서도로 표현했다.


현재 배포 스크립트는 컨테이너 실행 명령에서 끝나며, API가 실제로 정상 응답하는지 기다리는 단계는 없다. 정확한 중단 시간도 아직 측정하지 않았다. 앞으로는 CI 성공을 배포 조건으로 연결하고, 배포 후 상태 확인을 자동화하며, 백엔드 교체 중 발생하는 중단을 줄이는 방법을 검토하려고 한다. 배포 자동화의 완성도를 판단할 때에도 명령이 끝났는지에 더해, 사용자가 서비스를 계속 이용할 수 있는지를 확인해야겠다고 생각했다.


구현, 빌드, 사용자에게 전달되는 시점

여러 클라이언트를 운영하면서는 완료 상태도 나누어 볼 필요가 있었다. 웹 화면을 수정해 배포했다고 해서 설치된 Android 앱이나 Windows 앱의 화면까지 함께 바뀌는 것은 아니었다. 각 클라이언트에 포함되는 화면과 로직은 별도의 빌드와 배포를 거쳐야 했다.

데스크톱 앱은 아래와 같이 버전을 지정한 태그를 원격 저장소에 push하면 GitHub Actions가 실행되도록 구성했다.

git tag vX.Y.Z
git push origin vX.Y.Z

해당 태그의 웹 화면을 빌드해 Electron 앱에 포함하고, Windows 설치 파일을 만들어 GitHub Release에 올리는 흐름이다. 이후 릴리스의 공개 여부와 Latest 지정을 확인해야 웹의 다운로드 링크와 앱의 업데이트 경로를 통해 사용자에게 전달될 수 있다. 기존 사용자의 앱에 변경이 적용되려면 업데이트 설치와 재실행까지 이루어져야 한다.

실제로 상점에 새로운 품목을 반영한 Windows 0.1.11 설치 파일을 로컬에서 만들어 놓고도, 공개 릴리스는 0.1.10에 머물러 있던 일이 있었다. 웹에서는 변경된 모습이 보였지만 설치된 앱에는 나타나지 않았다. 구현과 빌드를 마쳤다고 긑이 아니라, 새 설치 파일을 사용자에게 전달하는 단계가 남아 있었던 것이다. 이후 10월 14일에 0.1.11을 GitHub의 최신 릴리스로 공개했다.

Android도 정식 AAB 생성과 검증, Play 업로드, 검토와 출시, 사용자의 업데이트 적용을 구분해야 했다. 현재는 1.2.4가 Google Play에 제공되고 있다. 다만 AAB 파일을 만들었다는 기록만으로 출시까지 완료됐다고 판단할 수는 없다. 작업 기록에도 빌드 완료, 업로드 완료, 사용자에게 제공 중인 상태를 각각 남길 필요가 있었다.

AI Agent와 작업할 때도 이 구분은 필요했다. 로컬의 수정과 검증을 맡기는 것, 운영 배포와 공지 게시를 진행하는 것은 영향 범위가 다르다. 실제 사용자가 있는 환경에 반영할 시점과 범위는 내가 결정하고 확인해야 했다. 기능을 구현했다는 사실에 더해 어떤 경로를 거쳐 사용자에게 전달됐는지까지 확인하는 것이 운영의 일부였다.


서비스를 유지하기 위한 결정들

운영에는 비용과 사용 정책도 포함됐다. 덤핏은 AI 기능에 일일 사용량과 기능별 차감 기준을 두고 있다. 별도의 후원이나 결제를 유도하고 있지 않은 만큼, AI 사용량을 무제한으로 풀어두면 (악의적 공격을 포함해) 너무 많은 사용량이 발생 시 개인적으로 감당하기 어렵다고 판단했기 때문이다.

업데이트를 설명하는 공지도 같은 맥락이다. 개발 기록에는 구현 세부 사항이 남지만, 사용자에게는 무엇이 달라졌고 자신이 쓰는 앱에 언제 적용되는지가 필요하다. 실제로 상점과 아이콘 변경을 안내할 때도 웹 적용과 Android 적용을 구분해 공지를 작성했다.

이런 일까지 하다 보니 서비스를 운영한다는 말이 조금 더 구체적으로 느껴졌다. 기능을 추가한 뒤에도 확인하고 설명할 일이 이어진다. 다음 기능이 재미있어 보여도, 지금 사용 중인 흐름을 유지하고 남아 있는 문제를 챙기는 일에 시간을 배분해야 했다.


마치며

덤핏을 만들며 AI Agent의 도움으로 시도할 수 있는 범위가 넓어졌다. 동시에 내가 더 공부해야 할 부분도 꽤 선명해졌다. 위에서 이야기한 낯선 기술을 접목하는 법, 웹과 앱에서 달라지는 화면, 테스트가 놓치는 조건, 빌드와 배포 과정 같은 것들이다.

백엔드 개발을 더 잘하고 싶다는 목표도 이 과정에서 구체적인 질문으로 이어졌다. 같은 계정을 여러 클라이언트가 사용할 때 무엇을 맞춰야 하는지, 사용자의 의도를 서버에서 어떻게 구분할지, 변경 후에도 데이터와 기존 동작을 어떻게 유지할지, 서버단에서 어떤 문제가 생길 수 있고 어떤 해결책이 있을지를 좀 더 고민하게 됐다.

아직 모든 기술을 충분히 이해했다고 말하기는 어렵다. AI의 설명을 다시 읽어야 하는 부분도 있고, 직접 실행하고 결과를 비교해야 이해되는 부분도 있다. 앞으로도 에이전트의 도움을 받되 다음 변경을 판단할 수 있을 만큼 내 이해를 남기는 작업을 함께 하고 싶다.

다음 프로젝트에서는 낯선 기술을 도입한 이유와 확인할 조건을 짧게라도 기록하고, 사용자 테스트에서 발견한 문제를 재현 가능한 형태로 남기고, 배포 뒤에 확인할 항목까지 작업에 포함하려고 한다. 덤핏의 경우 꾸준히 내 스스로도 사용해보면서 남아있는 문제들을 개선해보고, 비용과 운영 조건에 맞춘 기술 선택도 계속 돌아보려고 한다.

아무튼, 몇 번의 예고 끝에 덤핏 회고를 남긴다. 다음에 다시 회고를 하게 된다면 여기 적어둔 숙제들이 조금은 줄어 있기를 바란다. 그때도 이 글을 보면서 "이 인간... 미래의 나에게 일을 넘겼다!"고 생각하게 될지는... 앞으로의 나에게 달렸다.


덤핏 사용해 보기 · 프로젝트 저장소

profile
백엔드 개발자가 되고 싶어서… 그런데 이제 AI를 곁들인.

0개의 댓글