-
RecallBuddy 2.0 개발 후기독서 및 기타 활동 2025. 10. 19. 23:38
ChatGPT로 생성한 RecallBuddy 마스코트 황금연휴였던 추석이 벌써 순식간에 지나가버리고 날씨도 어느덧 쌀쌀해지기 시작한 것 같습니다. 다들 휴식도 취하고 여행도 가보고 가족들과 즐거운 시간을 보내기도 하고 평소에 하고 싶었던 일들을 시도해 보면서 시간들을 보내셨을 것 같은데요.
저도 이번 연휴 동안에는 그 동안 바쁘다는 핑계로 미루어왔었던 마지막 남은 사랑니도 뽑아보고 친구들도 만나고 개인적인 일들을 하나씩 처리하는 시간을 보내왔었던 것 같습니다.
그리고 남은 시간에는 거의 대부분 이전에 만들어놓았었던 개인 앱 서비스를 개선하면서 시간을 보냈었는데요. 이번 포스팅에서는 제가 연휴 동안 만들어본 RecallBuddy라는 서비스를 소개드리고 이번 프로젝트에서는 AI를 적극적으로 활용해보았는데 해당 경험 둘과 느낀 점들에 대해서도 공유드리는 시간을 가져보도록 하겠습니다.
목차
1. 서비스 탄생 배경
2. 개발은 혼자지만 마음은 함께하기(solo 포텐데이 소개)
3. AI와 함께 레거시 아키텍처 뜯어고치기
4. Firebase 기반 프로젝트를 모노레포로 통합 관리하기
5. Firebase Authentication를 활용한 Github 로그인 구축하기
6. Firestore Security Rules 적용하기
7. Firebase Cloud functions를 활용한 서버리스 백엔드 구축
8. ChatGPT를 통해 마스코트 캐릭터 디자인하기
9. 마무리하며
🐣 서비스 탄생 배경
RecallBuddy는 2023년 11월에 today-i-learned-alarm(til-alarm)이라는 이름으로 첫 릴리즈된 저만의 개인 공부 앱이었습니다. 말 그대로 혼자서 사용할 목적이었기 때문에 간단한 핵심 로직만 구현되어 있고 외부에 공유할 생각은 없었던 앱이었습니다.
그런데 2024년 초 쯤에 AI가 급격하게 성장하고 상용화되기 시작하면서 이 앱에도 AI를 적용한 서비스로 개선해 보고 시켜보고 싶다는 욕심이 생겼고 HyperCLOVA X를 통해 공부 질문지를 만들어내는 방향으로 풀어내어 v1.2 플래시카드 버전을 출시하였습니다.
이 버전을 만들 당시에는 글또 9기 커뮤니티의 월간 메이커스라는 채널 멤버들과 각자 만든 서비스를 공유하는 시간을 가졌었는데 그 당시 이 앱을 한번 써보고 싶다고 요청해 주신 분들이 많았습니다.
RecallBuddy v1.0(왼쪽) / RecallBuddy v1.2(오른쪽) 기쁜 마음에 흔쾌이 기약 없는 약속을 했지만 그 당시에 로그인과 인증 기능을 추가하기에는 부담이었고 현생의 벽으로 인해 점점 기억의 저편으로 날려버리고 있다가 1년이 지난 지금에서야 다시 떠올리게 된 것이 이번 2.0 버전의 기획 배경입니다.
이제야 갑자기 다시 꺼내든 이유는 현업에서도 최근 프로젝트에서도 로그인, 인증 시스템을 구현하기도 했고 현시점에서는 바이브 코딩이라는 용어가 나올 정도로 AI가 코딩을 워낙 잘하기 지금이라면 빠르고 안정감 있게 구현이 가능하지 않을까? 하는 생각에 서비스를 확장해 보자는 도전을 하게 되었습니다.
🧑🤝🧑 개발은 혼자지만 마음은 함께하기
510 SOLO 포텐데이 연휴동안 어떻게 하면 지치지 않고 목표를 달성해 낼 수 있을까 고민하던 찰나에 때 마침 저에게 구세주 같았던 프로그램 SOLO 포텐데이가 등장하였습니다.
SOLO 포텐데이는 IT 사이드 프로젝트 플랫폼인 비사이드에서 주관하는 프로젝트로 AI와 나 혼자서 도전하는 온라인 챌린지로 소개가 되었습니다. 이번에 실험용으로 공개가 되었는데 참가비는 10만 원이었고 완주하면 환급받을 수 있었기 때문에 부담 없이 참여할 수 있었습니다.
혼자서 진행하는 만큼 굉장히 힘든 여정이 될 수 있지만 중도 포기하지 않도록 이탈방지를 위한 여러 안전장치들이 마련이 되어있었는데요. 날짜별로 간단한 데일리 미션들이 준비되어 있었고 사람들과 이야기 나눌 수 있는 소통 채널과 일정 중간중간에 온라인 모각프(모여서 각자 프로그래밍)도 제공하고 있어 상당히 잘 짜인 프로그램이라는 생각이 들었습니다.
이전에 비사이드에서 운영하는 또 다른 프로그램인 포텐데이에도 2회 가량 참여해 본 경험이 있지만, SOLO 포텐데이의 묘미는 AI 팀 빌딩이었습니다. AI 팀 빌딩은 본인이 생각했을 때 어떤 AI 도구들을 사용하여 개발할 것인지를 채널에 소개하고 공유하는 시간을 가지면 되는데요.
이 때 다른 분들은 어떤 AI들과 함께하는지 파악할 수 있어서 요즘 대세 AI 툴이 어떤 건지? 어떤 툴들을 선호하는지 등도 함께 파악할 수 있어서 유익한 시간이었습니다. 저는 Figma Make와 Claude를 안 써보긴 했지만, 많은 사람들이 선호하고 있다는 점도 이때 처음 깨달았었습니다.
🌱 AI와 함께 레거시 아키텍처 뜯어고치기
프로그램이 시작되면서 가장 처음에는 주요 기능들 위주로 작업 목록을 먼저 작성해보았습니다. 우선, 제가 가장 중요한 목표로 잡았던 것은 로그인&인증 서비스이었습니다. 이번 계기를 통해 다른 사람들도 사용할 수 있게끔 앱을 확장하는 것이 이번 2.0 버전의 목표였습니다.
그러나, 구현에 앞서 가장 큰 문젯거리였던 것은 프로젝트를 새로 시작하는게 아니라 2년도 가까이 된 레거시 프로젝트를 유지보수 하면서 작업하려고 하니 오히려 설계가 복잡해지고 불필요한 리스크들이 동반된다는 점이었습니다.
예를 들어, 지금은 업데이트가 중단된 React CRA(Create React App) PWA 템플릿을 사용하고 있었는데 이 템플릿 위에서 로그인 및 인증서비스를 구현하려면 코드가 더 복잡해지는 문제가 있었습니다.
두 번째 문제는 PUSH 알림 서비스가 불가능한 상황이 생겨버렸는데요. 알림 서비스는 네이버클라우드 SENS(Simple & Easy Notification Service)를 사용 중이었는데 작년 4월부터 사업자등록을 한 사람에 한 해서만 사용이 가능하도록 정책이 바뀌어 버리는 바람에 PUSH 서비스를 더 이상 사용할 수 없게 되었습니다.
초기 모델(v1) 아키텍처 ( https://chucoding.tistory.com/129 ) 따라서, SENS 없이도 PUSH알림을 발송할 수 있도록 아키텍처를 개조해야 했었습니다. 이뿐만 아니라 사실, 기존의 아키텍처는 네이버클라우드 인스턴스에 앱을 직접 배포했었기 때문에 인스턴스 비용, 공인 IP 요금, 네트워크 비용을 추가로 내야 했습니다.
깡통 서버에 Nginx를 사용하여 직접 배포했었기 때문에 도메인도 직접 구매해서 사용했어야 했는데요. 저는 NoIP라는 무료 도메인 호스팅 플랫폼을 사용했지만 달에 한번 주기적으로 만료기간을 체크해야 하는 불편함도 있었습니다.
따라서 이번에 마이그레이션을 기획할 때는 기존 기능들은 잘 유지하면서 어떻게 하면 low cost, high quality, low maintenance를 뽑아낼 수 있는 아키텍처를 설계할 수 있을지 고민을 많이 했습니다. 그래서 저는 ChatGPT에게 제 상황을 전달하면서 아키텍처로만 거의 하루 종일 토론하는 시간을 가졌었는데요. 저는 다음과 같은 조건들을 제시했습니다.
- github 로그인 및 인증기능 구현
- github API 호출할 수 있는 사용자 별 action token 저장
- PUSH 알림 수신할 수 있는 앱 구현
- github API, hyperCLOVA X API 호출
- 매일 8시 알림 발송
위 조건들을 제안했을 때 ChatGPT는 다음과 같은 제안을 하였습니다.
Vercel + Next vs Firebase + Vite Vercel과 Next 조합은 워낙 유명하고 사람들이 많이 쓰는 조합이기 때문에 저도 ChatGPT에게 물어보기 전에는 이 아키텍처로 가야겠다는 생각을 어느 정도하고 있었는데요. 그러나 ChatGPT가 건네준 제안들 중에 유독 제 관심을 끌었었던 조합은 Firebase 원벤더 기반의 아키텍처였습니다.
여태 Firebase를 Messaging 기능만 사용해 보고 다른 서비스들은 사용해 본 적이 없었기 때문에 왜 Firebase기반 아키텍처를 추천하는 거지?라는 의문과 관심이 커지기 시작해서 그 뒤로부터는 ChatGPT와 계속 토론을 하면서 구글 검색도 해보고 의심을 반복하면서 여러 가지 조건들을 뺐다 넣었다 반복하면서 비교를 진행해 보았었는데요.
사람이랑 토론하였으면 “이 정도까지 물어본다고?”라는 생각이 들 정도로 돌다리를 계속 두들겨보면서 씨름한 결과 시간이 굉장히 오래 걸렸지만 나름 만족스러운 이유로 Firebase기반 아키텍처를 최종적으로 선택할 수 있게 되었습니다. Vercel+Next 조합과 Firebase 원벤더 조합은 각각 나름의 장단점이 있었기 때문에 선택하기가 정말 어려웠었는데요. Firebase를 선택했던 이유는 정말 한 끗의 디테일한 차이로 인해 선택을 하게 되었습니다.
만약 Vercel + Next 조합으로 간다고 하면 SSR 방식을 채택하여 API 호출도 하나의 코드베이스로 통합 관리할 수 있고 NextAuth라는 강력한 인증 솔루션을 사용할 수 있기 때문에 github로그인 및 인증도 간편하게 만들 수 있어 개발 속도는 조금 더 빠를 수 있겠다는 생각이 들었습니다.
다만, 이 조합이 아쉬웠던 점은 DB 보안전략이었습니다. 제가 만들려는 서비스는 최초 로그인한 사용자 계정에 대해 github 토큰을 발급받고 안전하게 저장하고 관리할 수 있어야 하는데 로컬스토리지나 쿠키에서 관리하기에는 위험성이 너무 컸기 때문에 저는 가격이 저렴하면서도 보안과 성능이 뛰어난 DB가 필요한 상황이었습니다.
Vercel에서 제공 중인 스토리지 서비스 Vercel KV 또는 Blob을 사용하면 데이터 저장은 가능하지만 Firestore만큼 강력한 권한/보안 규칙을 제공하고 있지 않기 때문에 아쉬움이 있었습니다. Vercel KV나 Blob은 저장소 자체에 유저 단위의 세밀한 권한 DSL이 없고 직접 세션이나 쿠키 JWT 등으로 검증하고 권한을 판단하는 로직을 매 엔드포인트마다 반복 구현해야 합니다. 이는 호출 비용 및 유지보수 리스크가 커질 수 있기 때문에 개발하는 입장에서 조금 부담스러웠습니다.
반면에 Firestore는 Security Rules 직접 지정할 수가 있는데 이를 통해 유저 단위로 DB접근 권한을 제어할 수 있습니다. 그리고 Firebase Auth랑 1급 통합되기 때문에 모바일, 웹 어떤 환경에서도 동일하게 작동합니다. 특히, 규칙에 의해 거부된 요청은 네트워크 전송량(egress) 과금에서 제외되기 때문에 비용면에서도 효율적입니다.
이 외에도 메시지 발송 시스템을 지원하는 강력한 서비스인 FCM을 사용하려면 Firebase를 사용해야 하는데 이 FCM서버가 트리거(API 또는 스케줄러, 크론)들이랑 같은 리전 같은 공간에 있을수록 조금 더 유리하기도 하고 특히, firebase는 스케줄러를 3개까지 지원한다는 장점이 있었습니다.
반면 vercel cron은 무료 플랜은 계정당 2개까지만 지원하고 PUSH 메시지들이 큐잉 처리되어 사용자마다 도착 시간도 보장되지 않기 때문에 이런 디테일한 차이점들로 인해 최종적으로 Firebase 아키텍처로 선택하게 되었습니다.
Firebase 원벤더 기반 아키텍처
☀️ Firebase 기반 프로젝트를 모노레포로 통합 관리하기
Next, Vercel 조합을 포기하고 Firebase 기반의 SPA 아키텍처를 선택했을 때 가장 부담스러웠던 점은 코드 통합 관리가 어려울 수 있다는 점이었는데요. 처음에는 프론트 백엔드 나누어서 레포지토리를 따로 관리해야 되나 하는 고민을 했었는데 다행히도 Firebase에서 지원하는 서비스들을 통합 배포 관리 할 수 있도록 지원을 하고 있었습니다.
만약 통합 관리하고 싶다면 Firebase CLI 도구를 사용하시면 편리한데요. 이 도구를 활용하면 모노레포 기반으로 프로젝트 구조를 세팅할 수 있고 functions, store, hosting을 전부 묶어서 빌드 및 배포가 가능합니다. 다음과 같이 개발환경을 세팅해 주시면 손쉽게 사용이 가능합니다.
- Firebase CLI 설치 npm install -g firebase-tools
- Firebase 프로젝트 만들기 및 앱 등록 (가이드 참고, 또는 이전 포스팅)
- Firebase 로그인 firebase login
- 프로젝트 초기화 firebase init
firebase init 명령어를 입력하면 콘솔에서 어떤 서비스를 초기화할 것인지 나타나는데 여기서 사용하고자 하는 서비스들을 골라서 사용하시면 됩니다. 만약 개별적으로 사용하고 싶다면 firebase init hosting 이런 식으로 서비스를 뒤에 붙여주시면 됩니다.
명령어 실행이 끝나면 다음과 같이 firebase.json 파일이 생성됩니다.
/* firebase.json */ { "hosting": { "public": "public", // 배포할 디렉토리 지정 "ignore": [ // 배포시 무시할 파일 지정 "firebase.json", "**/.*", "**/node_modules/**" ], "rewrites": [ // 404방지, URL 단축 등 설정 { "source": "**", "destination": "/index.html" // 기본값 : 존재하지 않는 파일 또는 디렉터리에 대한 요청에 대해 index.html 제공 } ] } }
해당 파일은 루트 경로에 생기는데 이 구성에 firestore와 functions 서비스를 추가하고 싶다면 firebase init firestore와 firebase init functions를 입력하시면 됩니다. 저는 functions, firestore, hosting 3개의 서비스를 통합 관리하기 위해 3개의 서비스를 초기화하여 다음과 같은 JSON 파일이 구성되었습니다.
/* firebase.json */ { "hosting": { "public": "app/dist", "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ], "rewrites": [ { "source": "**", "destination": "/index.html" } ] }, "firestore": { "rules": "firestore.rules" }, "functions": [ { "source": "functions", "codebase": "default", "ignore": [ "node_modules", ".git", "firebase-debug.log", "firebase-debug.*.log", "*.local" ], "predeploy": [ "pnpm --prefix \\"$RESOURCE_DIR\\" run lint", "pnpm --prefix \\"$RESOURCE_DIR\\" run build" ] } ], }
위와 같이 설정해 놓으면 Firebase CLI를 통해 명령어로 간단하게 통합 빌드 배포 또는 선택적 빌드 배포가 가능해집니다. firebase init 명령어를 사용하면 기본적인 프로젝트 구조도 같이 구성이 되지만 사용하기는 어려워 저는 다음과 같이 변경해서 사용하였습니다.
MyProject |- app /* Firebase Hosting에 배포할 frontent app */ |- public |- src |- index.html |- package.json |- functions /* Firebase Cloud Functions에 배포할 Backend 함수들 */ |- src |- github.ts |- clova.ts |- index.ts |- schedule.ts |- package.json |- firebase.json /* firebase 설정 파일 */ |- firestore.rules /* firestore Security Rules 설정 파일 */ |- package.json |- pnpm-lock.yaml |- pnpm-workspace.yaml
마지막으로 위 모듈들을 하나의 패키지로 통합 관리하기 위한 설정파일은 다음과 같이 구성하였습니다.
/* package.json */ { "private": true, "packageManager": "pnpm@10.18.0", "workspaces": [ "app", "functions" ], "scripts": { "proxy": "node scripts/setup-proxy.js", "serve": "firebase emulators:start --only functions", "dev": "pnpm --filter app dev", "build": "pnpm -r build", "push": "firebase deploy" } }
저는 총 5가지 스크립트 명령어를 사용했는데요. proxy의 경우에는 개발환경이나 프로덕션 환경에서 프론트 앱에서 백엔드(functions) API URL을 환경변수에 자동으로 셋팅해주는 스크립트를 별도로 만들어 놓았고 serve와 dev 명령어로는 각각 functions를 실행하기 위한 애뮬레이터와 vite기반의 프론트 웹 서버를 구동시킬 수 있도록 구성해 놓았습니다.
처음에는 serve와 dev 명령어를 하나로 합치려고 했지만 애뮬레이터와 vite 서버 둘 다 forground로 동작하는 프로세스이기 때문에 동시에 실행시키려면 concurrently 같은 별도의 모듈이 필요한 상황이었고 또 동시에 실행시키면 vite 서버가 먼저 구동되어 API 오류가 발생하는 문제가 있었습니다.
이 문제를 해결하기 위해 애뮬레이터가 먼저 실행시킬 수 있도록 서버 구동 포트 오픈 감지 체크 로직을 추가하였지만 애뮬레이터가 내부적으로 준비되지 않았는지 vite 서버 구동 명령어가 지속적으로 씹히는 현상이 있어서 결국에는 나눠서 실행시키는 로직으로 롤백하게 되었습니다.
따라서 개발할 때는 애뮬레이터가 다 켜진 것을 확인한 후에 새로운 터미널창을 열어서 vite를 기동해야 하는데요. 애뮬레이터를 기동 할 때 주의할 점은 --only 옵션을 사용하지 않으면 firebase.json 파일에 설정해 놓은 모든 firebase 서비스가 기동 되기 때문에 애뮬레이터가 무거워져서 로딩속도가 길어질 수 있습니다.
특히, firestore 애뮬레이터는 실행하려면 Java Runtime이 필요하기 때문에 Java설치 후 PATH를 설정해야 사용가능하다는 불편함이 있습니다. 반면, Functions는 Node.js 기반에서 동작하기 때문에 애뮬레이터를 기동 하려면 NodeJS가 필요합니다. 이러한 문제점들이 있어서 저는 애뮬레이터를 기동 할 때는 --only 옵션을 사용하여 Firebase Cloud Funtcions만 구동될 수 있도록 설정하였습니다.
build와 push는 각각 프로젝트를 빌드하고 배포하는 명령어입니다. 빌드는 tsc와 vite를 활용하여 빌드할 수 있고 배포할 때는 firebase deploy 명령어로 손쉽게 배포할 수 있습니다. 저는 pnpm 명령어 사용이 익숙하여 pnpm push로 배포될 수 있도록 명령어를 추가하였습니다. 참고로 pnpm deploy는 예약어가 이미 있기 때문에 deploy로 정의할 경우 pnpm run deploy 명령어를 사용해야 배포가 가능합니다.
🔥 Firebase Authentication를 활용한 Github 로그인 구축하기
RecallBuddy 로그인화면 NextAuth나 Supabase를 쓰지 않고도 Firebase에서도 Github 소셜 로그인을 간단하게 구축할 수 있습니다. Firebase Authentication을 서비스를 사용하시면 되는데요. Github 외에도 Google이나 facebook, Twitter 등 다양한 소셜 로그인들을 지원하고 있습니다.
Firebase Authentication 로그인 방법 설정 제가 만들고자 하는 앱은 Github 기반으로 동작하기 때문에 처음부터 Github 소셜로그인만 생각하고 있어서 이 서비스가 적합하다고 생각했습니다. Firebase Authentication을 사용하기 위해서는 다음과 같은 순서로 온보딩 해주시면 됩니다.
- Firebase 프로젝트 및 Javascript 앱 생성
- Firebase console에서 인증(Authentication) 섹션 선택
- 로그인 방법 탭에서 GitHub 제공업체를 사용 설정
- GitHub에서 개발자 애플리케이션으로 앱을 등록하고 앱의 OAuth 2.0 클라이언트 ID와 클라이언트 보안 비밀번호를 가져와서 등록
- GitHub 앱 구성의 앱 설정 페이지에서 Firebase OAuth 리디렉션 URI(예: my-app-12345.firebaseapp.com/__/auth/handler)가 승인 콜백 URL로 설정되어 있는지 확인
- 저장 클릭
해당 순서는 아래 공식가이드에서도 확인하실 수 있습니다.
자바스크립트에서 GitHub를 사용하여 인증 | Firebase Authentication
의견 보내기 자바스크립트에서 GitHub를 사용하여 인증 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱에 GitHub 인증을 통합하여 사용자가 GitHub 계정을 통
firebase.google.com
위 설정까지 마무리되었다면 이제 클라이언트를 개발할 차례입니다. 먼저 app 폴더 package 의존성에 다음과 같이 firebase SDK를 추가합니다.
pnpm add firebase --filter app
이 SDK는 hosting에 배포할 firebase client app에서 firebase auth 또는 firestore 등에 접근할 수 있도록 도와주는 라이브러리입니다. 해당 SDK 의존성 모듈을 추가하고 나면 다음과 같이 firebase 앱을 초기화할 수 있는 로직을 추가할 수 있습니다.
/* firebase.ts */ import { initializeApp, getApps, getApp } from 'firebase/app'; import { getAuth, GithubAuthProvider } from 'firebase/auth'; import { getFirestore } from 'firebase/firestore'; const firebaseConfig = { apiKey: import.meta.env.VITE_API_KEY, authDomain: import.meta.env.VITE_AUTH_DOMAIN, projectId: import.meta.env.VITE_PROJECT_ID, storageBucket: import.meta.env.VITE_STORAGE_BUCKET, messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID, appId: import.meta.env.VITE_APP_ID, measurementId: import.meta.env.VITE_MEASUREMENT_ID, }; // Firebase 앱 초기화 (이미 초기화된 경우 기존 앱 사용) const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp(); // Auth 인스턴스 생성 export const auth = getAuth(app); // Firestore 인스턴스 생성 export const store = getFirestore(app); // GitHub 프로바이더 생성 export const githubProvider = new GithubAuthProvider(); githubProvider.addScope('user:email'); githubProvider.addScope('repo');
저는 src 폴더 하위에 firebase.ts라는 파일을 만들어서 이 앱에서 사용할 Firebase 관련 코드를 전부 한 곳으로 모아서 관리하였는데요. Firebase Authentication이나 Firestore 모듈을 사용하려면 Firebase 인스턴스를 매개변수로 전달해야 하기 때문에 메모리를 효율적으로 사용하기 위해서는 위와 같이 싱글톤 패턴으로 정의해 놓는 것이 좋습니다.
그리고 github 로그인 시 사용자로부터 권한을 받도록 설정할 수 있는데 이때 필요한 Scope들을 추가하실 수 있습니다. 예를 들어, 로그인 후 프로필 정보를 받아오고 싶다면 user 스코프를 적용하시면 되고 private repo 접근 권한을 얻고 싶다면 repo 스코프를 추가하면 됩니다.
Github 로그인 기능은 간단하게 signInWithPopup() 함수를 호출할 수 있는 버튼만 구현해 놓으면 팝업창 형태로 github 로그인 연동이 가능합니다. 따라서 앞서 만들어놓은 firebase.ts 파일을 임포트 하여 생성한 authentication 객체와 githubProvider객체를 signInWithPopup() 함수의 매개변수로 넣어주기만 하면 코드 한 줄로 간편하게 로그인 기능을 만들 수가 있습니다.
import { auth, githubProvider } from '../firebase'; const result = await signInWithPopup(auth, githubProvider);
사용자가 github 로그인을 완료했다면 result값에 앞서 설정한 scope에 따른 사용자의 정보가 담겨오는데요. 이 결괏값에 따라 추가적인 처리방식을 원하는 방향에 맞게 구현해 주시면 됩니다. 만약에 제가 만들고자 하는 앱처럼 내부적으로 github API 호출을 사용해야 된다면 OAuth 토큰이 필요하기 때문에 GithubAuthProvider를 통해 액세스토큰을 발급받아 별도로 저장해놓아야 합니다.
다만, 이 토큰을 발급받는 경우 로컬스토리지나 쿠키는 보안상 위험할 수 있기 때문에 DB에 저장하는 것이 가장 안전한데요. 저는 다음과 같이 firestore에 저장하는 로직을 추가했습니다. firestore는 기본적으로 Google의 at-rest encryption으로 자동 암호화 처리가 되기 때문에 안전하게 보관이 가능합니다.
/* Login.tsx */ import { signInWithPopup, signOut, GithubAuthProvider } from 'firebase/auth'; import { doc, setDoc, updateDoc, getDoc } from 'firebase/firestore'; import { auth, githubProvider, store } from '../firebase'; export default function Login { const handleGitHubLogin = async () => { try { /* Github 로그인 팝업창 오픈 */ const result = await signInWithPopup(auth, githubProvider); const credential = GithubAuthProvider.credentialFromResult(result); if (credential && credential.accessToken && result.user) { /* 로그인 성공시 firestore에서 유저 정보 조회*/ const userDocRef = doc(store, 'users', result.user.uid); const userDoc = await getDoc(userDocRef); /* 유저 정보 있는 경우 토큰 갱신 */ if (userDoc.exists()) { await updateDoc(userDocRef, { githubToken: credential.accessToken, updatedAt: new Date().toISOString(), }); } else { /* 최초 로그인인 경우 토큰 저장 */ await setDoc(userDocRef, { githubToken: credential.accessToken, updatedAt: new Date().toISOString(), }); } } console.log('로그인 성공'); } catch (error: any) { console.error('로그인 실패:', error); } }; return ( <button onClick={handleGitHubLogin} className="github-login-button" disabled={loading} > <img src="/github-mark-white.svg" alt="GitHub Logo" className="github-icon" /> GitHub로 로그인 </button> ); }
Github 로그인 UI 같은 경우에는 별도로 공식적인 디자인 가이드가 나와있지는 않지만, 아래 공식로고를 활용하여 디자인해 주시면 됩니다.
GitHub · Build and ship software on a single, collaborative platform
Join the world's most widely adopted, AI-powered developer platform where millions of developers, businesses, and the largest open source community build software that advances humanity.
github.com
로그인 구현까지 완료가 되었다면 이제 인증 기능을 만들어주시면 되는데요. 만약에 인증이 된 유저라면 메인페이지를 보여주고 인증이 안된 유저라면 로그인 페이지를 보여주는 로직이 필요합니다. 원래는 이 로직을 SPA에서 구현하려면 손이 많이 가긴 하지만 다행히도 firebase auth 라이브러리에서 onAuthStateChanged라는 함수를 제공하고 있어 auth 상태를 손쉽게 감지할 수가 있습니다.
import React, { useEffect, useState } from 'react'; import { onAuthStateChanged, User } from 'firebase/auth'; const App: React.FC = () => { const [user, setUser] = useState<User | null>(null); /* 인증 상태 감지 */ useEffect(() => { const unsubscribe = onAuthStateChanged(auth, (user) => { setUser(user); setAuthLoading(false); }); return () => unsubscribe(); }, []); // 로그인되지 않은 경우 if (!user) { return <Login />; } return (<main>...</main>) }
위와 같이 useEffect() 훅 내에서 onAuthStateChanged()를 구독하고 앱 종료 시 clean up 함수를 통해 구독을 해지하시면 됩니다. onAuthStateCanged() 함수가 Unsubscribe 타입을 리턴하도록 구현이 되어있는데 이 타입은 함수로 정의가 되어있어서 이 함수를 호출하면 구독이 취소되도록 subscription 패턴을 갖고 있습니다.
따라서, 앱을 다시 열 때마다 리스너가 계속 쌓여서 메모리 누수가 발생하지 않도록 위와 같은 형태로 구현해 주시면 되고 onAuthStateChanged에서 인증 상태가 감지될 때마다 useState로 정의된 user값의 상태값이 null로 변하는지를 체크해 주시면 됩니다.
이렇게 firebase 및 github 설정, 로그인기능, 인증기능까지 구현을 모두 완료하시면 firebase 기반의 github 로그인 구현이 완료되었습니다. 해당 가이드는 아래 공식문서에서도 참고하실 수 있습니다.
자바스크립트에서 GitHub를 사용하여 인증 | Firebase Authentication
의견 보내기 자바스크립트에서 GitHub를 사용하여 인증 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱에 GitHub 인증을 통합하여 사용자가 GitHub 계정을 통
firebase.google.com
🔥 Firestore Security Rules 적용하기
앞서 Firestore를 사용하면 Security Rules를 적용할 수 있기 때문에 vercel 대신 firebase 기반 아키텍처를 선택했었다고 말씀드렸었는데요. Security Rules는 다음과 같이 프로젝트 루트 경로에 firestore.rules라는 파일을 만들어놓고 사용하시면 됩니다. 저는 다음과 같은 규칙들을 한번 적용해 보았습니다.
/* firestore.rules */ /* Cloud Firestore 보안 규칙 버전 2 사용 */ rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { /* 사용자 컬렉션: 본인 데이터 읽기/쓰기 가능 */ match /users/{userId} { allow read, write: if request.auth != null && request.auth.uid == userId; } /* 탈퇴한 사용자 기록 (재가입 방지용) */ match /deletedUsers/{userId} { allow read, create: if request.auth != null && request.auth.uid == userId; } /* 공지사항: 읽기 전용 (쓰기는 Firebase Console에서만) */ match /notices/{noticeId} { allow read: if request.auth != null; } } }
먼저 저는 다른 사용자들은 본인 데이터에만 접근할 수 있도록 규칙을 적용했고 로그인하지 않으면 무조건 접근할 수 없도록 조치하였습니다. 또한, 탈퇴한 사용자들이 재가입을 반복하며 call 낭비가 되지 않도록 방지하기 위해 삭제 유저들에 대한 테이블을 하나 더 만들어놓았는데요.
해당 테이블은 12시가 지나면 초기화되어 다시 재가입을 할 수 있도록 스케줄을 구성하였습니다. 따라서 그전에 재가입을 시도하는 경우 로그인이 불가능하도록 조치하기 위해 재가입 방지용 DLS를 추가해 놓았습니다.
마지막은 제가 앱 관리를 용이하게 하기 위해 사용자들에게 노티를 간편하게 줄 수 있도록 알림용 테이블을 하나 더 만들어놓았습니다. 해당 테이블은 당연히 쓰기는 허용되면 안 되므로 로그인한 유저에 한해서 읽기만 가능하도록 보안 규칙을 적용하였습니다.
이 처럼 모든 접근은 인증을 필수로 하고 최소 권한만 적용하여 데이터를 완전하게 격리하여 안전하게 보호되도록 구현할 수 있었습니다. firebase.rules는 별도로 빌드할 필요 없이 firebase deploy 명령어로 인해 같이 배포됩니다. 만약, Rules만 별도로 배포하고 싶다면 다음과 같이 명령어를 입력해 주시면 됩니다.
firebase deploy --only firestore:rules
🔥 Firebase Cloud functions를 활용한 서버리스 백엔드 구축
Firebase용 Cloud Functions는 백그라운드 이벤트, HTTPS 요청, Admin SDK 또는 Cloud Scheduler 작업에 의해 트리거 되는 이벤트에 응답하여 백엔드 코드를 자동으로 실행할 수 있는 서버리스 프레임워크입니다. 프로젝트 초기화 명령어 사용 시 typescript, javascript, python 3가지 언어 중에서 선택하여 함수들을 구현할 수 있습니다.
functions 프로젝트를 초기화하고 나면 src폴더 하위에 index.ts 파일이 생성되는데 이 파일에서 서버리스로 배포할 함수들을 re-export 하면 Cloud Functions로 배포할 수 있습니다. 저는 아래와 같이 github API, clova API, schedule API 3개로 구분하여 파이프라인을 구축하였습니다.
import {setGlobalOptions} from "firebase-functions"; import {initializeApp} from "firebase-admin/app"; // For cost control, set the maximum number of containers that can be // running at the same time. setGlobalOptions({maxInstances: 10}); initializeApp(); export * from './github.js'; export * from './schedule.js'; export * from './clova.js';
functions 모듈도 마찬가지로 index.ts 파일 내에서 initializeApp()을 호출하여 프로젝트를 초기화해주어야 하는데요. 클라이언트 SDK(firebase/app)에서 사용할 때는 프로젝트 정보를 환경변수에 저장해 놓고 불러와서 매개변수로 config를 전달했었지만 functions에서는 서버용 Admin SDK(firebase-admin/app)를 사용하기 때문에 빈 매개변수로 전달해도 됩니다.
서버용 Admin SDK는 Google Cloud 환경에서 실행되기 때문에 자동으로 감지가 되고 민감한 설정을 코드에 넣지 않아도 돼서 보안상에도 이점이 있습니다. 반면에 브라우저는 어디에 연결할지 모르는 신뢰할 수 없는 환경에서 사용되기 때문에 반드시 Config로 명시해야 합니다.
setGlobalOptions는 동시에 실행 가능한 함수 인스턴스(컨테이너) 최대 개수를 제한하는 옵션입니다. 트래픽이 증가하면 Firebase에서 자동으로 함수 인스턴스를 늘리는데 10개를 초과하는 요청은 대기하거나 서응을 저하시킵니다. 이 옵션을 추가한 이유는 혹시 모를 비용 폭탄을 방지하기 위해 추가해 놓았습니다.
추가로, functions 모듈은 기본적으로 CommonJS 형태로 빌드가 됩니다. 이는 번들러(tsup, webpack)에게 빌드를 맡기는 최신 모듈러 웹 기반 처리방식과는 거리가 조금 있기 때문에 re-export 할 때는 위와 같이 .js 확장자를 붙여줘야 빌드시 오류가 발생하지 않습니다.
물론 CommonJS 대신 module 타입의 최신 빌드 방식을 선택하고 tsup 등의 번들러를 통해 빌드되도록 변경하면 좀 더 type-safe 하도록 클라이언트와의 타입 공유도 가능한 이점들이 있지만 firebase에서는 권장하지 않는 패턴입니다.
만약 공유 type이 많다면 고려해 볼 만 하지만 그렇지 않다면 자칫 오버엔지니어링이 될 수 있고 tsup의 경우 전체 번들링이 되기 때문에 증분 빌드 방식인 tsc에 비해 빌드 속도가 더 느릴 수도 있습니다. 서버리스의 경우 번들 크기 최적화가 중요하지 않기 때문에 기존 설정을 유지하는 것이 좋습니다.
🦖 ChatGPT를 통해 마스코트 캐릭터 디자인하기
이번 2.0 버전에서는 서비스 로직에 비해 디자인과 퍼블리싱은 전적으로 AI에게 맡겨보았습니다. 별도로 디자인시스템과 스타일 가이드를 주어주지 않고 AI에게 한번 창의력을 발휘하도록 맡겨 보았는데요. 마스코트로 사용할 캐릭터 디자인의 경우에는 최악의 디자인이 나왔던 것 같고 반면에 cursor에게 전체적인 컬러나 스타일링을 맡겼을 때는 만족스럽게 잘 뽑혔던 것 같습니다.
Cursor의 경우 Agent Auto모드를 사용했는데 별도로 추가 프롬프팅을 하지 않아도 코드베이스 인덱싱이 잘 되어있어서 그런지 “로그인 페이지 만들어줘.” 등의 가벼운 질문만 던져도 전반적인 프로젝트의 분위기에 맞게 컬러와 레이아웃을 알맞게 잘 만들어냈다는 생각이 들었습니다.
Cursor를 통해 디자인 및 퍼블리싱 바이브코딩 진행 물론, 디자이너 분들이 보시기에는 이상할 수 있지만 비디자이너인 제가 보았을 때는 생각보다 괜찮네?라는 느낌이 들 정도로 만족스러웠던 것 같습니다.
캐릭터의 경우에는 ChatGPT 5 Thinking 모델을 활용해서 디자인을 했었는데요. 이미지를 만들어달라고 요청을 할 때 프롬프트 내용이 구체적이지 않고 추상적일수록 간단하고 못 생기게 만드는 현상이 나왔던 것 같습니다.
요구사항이 추상적일 수록 이미지 퀄리티가 떨어진다. 프로젝트 내용과 기능들을 아무리 상세히 적어도 해당 현상이 반복되어서 이번에는 제 상상 속의 캐릭터를 하나 정해서 그 특징을 묘사하여 프롬프트로 제공해 보았습니다. 그랬더니 다음과 같이 이전보다는 더 만족스러운 이미지를 생성해 내는 것을 볼 수 있었습니다.
요구사항이 구체적일 수록 이미지 퀄리티가 높아진다. 주의할 점은 특정 위치나 묘사가 마음에 안 든다고 수정해 달라는 요청을 지시하면 이미지가 더 망가지는 현상이 있습니다. ChatGPT로는 아직까지는 디테일한 수정까지는 어려운 것으로 보입니다. 그래서 다른 유료 서비스도 알아보고 써보았는데 KlingAI라는 서비스를 사용해 보았다가 생성은 ChatGPT보다는 잘했지만 시간이 더 오래 걸리고 비용을 지불하지 않으면 워터마크 낙인이 찍히는 문제가 있었습니다.
그래서 ChatGPT로 계속 만족스러운 이미지가 나올 때까지 갓차를 하는 수밖에 없겠다는 생각이 들었습니다. 그래도 ChatGPT가 좋았던 점은 답변 분기가 되기 때문에 이상한 이미지 생성 시 다시 그 위의 대화부터 분기하여 최적화된 이미지를 찾아갈 수 있다는 점인 것 같습니다.
한번 캐릭터가 한번 잘 뽑히고 나면 그 뒤로부터는 제스처를 조금씩 바꿔가면서 멀티턴으로 넣어주시고 언제 또 사용할지 모르니 잘 보관해 두시면 좋습니다.
제스처를 조금씩 바꿔가면서 이미지를 변형
🌠 마무리하며
정말 오랜만에 긴 포스팅을 작성해 보는 것 같습니다. 시작을 하기 전에는 정말 간단하게만 만드려고 생각을 했었는데 만들다 보니 점점 욕심도 생기고 디테일을 계속 챙기려고 하다 보니 시간을 많이 잡아먹게 된 것 같은데요. 그래도 분명한 점은 AI가 없었으면 이 정도까지 구현하는 것은 거의 불가능하지 않았을까 하는 생각이 듭니다.
v2.0 작업목록 (WBS) AI를 통해 기획부터 디자인, 개발까지 거의 대부분에서 바이브 코딩으로 시작하여 덕분에 아무것도 몰랐던 Firebase 기반의 아키텍처로 손쉽게 마이그레이션도 해보고 배운 점과 느낀 점도 많았기에 디테일을 중요시하는 제게 AI의 등장은 정말 단비 같은 존재인 것 같습니다.
그래도 바이브 코딩을 하면서 느꼈던 점은 결국 인간이 가진 창의력과 경험이 중요하다는 점인 것 같습니다. 디자인 도움을 받을 때도 캐릭터가 필요하다고 해서 뚝딱 떨어지는 것이 아니라 무언가를 창조해 내는 데는 결국에는 나의 경험과 창의력으로 상상해서 만들어낼 수밖에 없었고 AI는 제가 생각해 낸 것을 구현해 줄 능력만 갖고 있었습니다.
마찬가지로 개발 쪽에서는 5년간의 경험과 어느 정도의 지식베이스가 갖춰져 있다 보니 AI가 만들어내는 허점들이 계속해서 보일 수밖에 없었고 지속적으로 피드백을 넣어가며 프로젝트를 완성해 나가는 경험을 했습니다. 만약 저에게 개발 지식이 없었더라면 의심을 하지 못하고 자칫하면 책임질 수 없는 위험한 상황까지 닥칠 수 있을 것 같다는 생각이 듭니다.
그래서 아직까지 AI는 바이브까지는 아니고 보조도구 정도로 활용할 수준이 아닐까 하는 생각이 듭니다. 특히, 의심을 많이 할수록 좋은 인사이트들을 많이 챙길 수 있었던 것 같습니다. 저는 AI와 대화를 나눌 때 "나는 이렇게 생각하는데 너는 왜 그렇게 생각했어?"와 같은 화법을 주로 사용하는데 그럴 때마다 제가 놓쳤던 부분들을 발견하기도 하고 몰랐던 부분들을 발견하기도 합니다. 앞으로도 개발자로 지내면서 AI를 활용한다면 이런 식으로 많이 활용하게 되지 않을까 하는 생각이 듭니다.
이로써 RecallBuddy 2.0 개발을 마치고 개발 후기에 대해 공유드려보았는데요. 아직 수정해야 될 부분도 많고 추가하고 싶은 기능들도 많기 때문에 다음에 기회가 된다면 또 2.1 버전이나 3 버전으로 확장해서 앱을 개선해 보는 기회가 생겼으면 좋겠습니다. 혹시 RecallBuddy 서비스가 궁금하신 분들이 계시다면 아래 링크를 통해 앱을 사용해 보셔도 좋을 것 같습니다😊
RecallBuddy
til-alarm.web.app
GitHub - chucoding/recall-buddy: 에빙하우스 망각곡선(1일, 7일, 30일)에 의거하여 공부한 내용을 매일 아
에빙하우스 망각곡선(1일, 7일, 30일)에 의거하여 공부한 내용을 매일 아침 PUSH 알림을 받을 수 있도록 해주는 프로그램 - chucoding/recall-buddy
github.com
'독서 및 기타 활동' 카테고리의 다른 글
네이버 개발자들은 AI Agent를 어떻게 사용하고 있을까? - 네이버클라우드 AI DEVDAY 참여후기 (6) 2025.07.20 테크포임팩트 랩짱을 마치며 (6) 2025.03.25 개발자의 비영리(non-profit) 도메인 이해하기 (3) 2025.01.19 5년차 개발자의 2024년 회고록 (42) 2024.12.22 테크포임팩트 랩짱에 도전하다 (6) 2024.09.16