때때로 스스로 부족하다고 느끼거나, 일이 마음대로 되지 않을 때가 있다.
그럴 때 작은 성취를 쌓아가며 다시 돌아보면 새로운 감정을 느끼게 되는데, 나는 그 순간을 좋아한다.
2024년 한여름, 달리기를 정말 싫어하는 나는 더 나은 내가 되기위해 런데이 8주 달리기 챌린지를 도전했다.
달리기 인증을 기록하고 공유하며 응원받는 경험이 챌린지 완주를 이끌었고, 달리기를 취미 생활로 갖게 되었다. 이 글을 빌어 감사함을 전달 합니다.
이 경험을 바탕으로, 기록을 남기고 그것을 응원해주는 AI를 통해 다른 사람들도 지속적인 동기부여를 받을 수 있는 서비스를 만들고 싶었다.
실제로 진행한 달리기 인증 기록과 응원 코멘트, 이모지 | 챌린지 완주한 기록 |
---|---|
- 리드미를 확인하면 또 다른 버전으로 자세하게 확인할 수 있다.
Boost Pal은 기록에 대한 따뜻한 응원과 위로의 메시지를 전달하는 서비스
사용자가 일상적인 습관이나 챌린지에 대한 인증을 기록하면 AI가 응원과 위로의 메시지를 자동으로 생성하여 코멘트로 달아준다.
이 서비스를 통해 더 많은 사람들이 작은 성취감을 느끼고 이를 지속할 수 있도록 돕고자 한다.
업로드 과정 및 AI 코멘트 받기 | 결과물 |
---|---|
제일 인증 수가 많은 카테고리 | 전체 인증 | 캘린더로 확인하는 나의 인증 | 내가 작성한 모든 인증 | 검색으로 인증 찾기 |
---|---|---|---|---|
귀여운 동물 친구를 AI BOT으로 계획 했기 때문에 캐릭터 선정은 나의 트레이드 마크인 미색의 햄스터로 정했다.
이 햄스터가 응원의 코멘트를 달아주고, 추후에 고도화를 한다면 다양한 동물을 추가할 계획이다.
직접 그림도 그리고 피그마를 사용해 프로젝트 디자인을 기획해보고 만드는 것은 처음 이었기 때문에 생각보다 시간 투자가 많이 되었다. 모각코 활동하면서 만난 스터디원 분이 UI 피드백도 해주셔서 정말 감사했었다.
디자인 기획 | 유저 플로우 |
---|---|
구분 | 기술 스택 |
---|---|
프론트엔드 | Next.js / TypeScript / TailwindCSS / React Hook Form / Zod / Supabase Realtime / Cloudflare Image |
백엔드 | Prisma / Iron-session / OpenAI / AWS SQS + Lambda / Supabase |
사실, AWS SQS + AWS Lambda는 기술스택의 고려대상이 아니었다.
트러블 슈팅을 진행하다 해결 방법을 강구하다 결국 선택한 것이다.
이런 사항들을 보면, 프론트엔드 개발자여도 웹개발의 전반적인 지식은 겸비해야하지 않을까 라는 생각을 했다.
부제 : 요청하자 open AI 메세지! 달아보자 AI comment!
사실, Open AI을 고려하게 된 계기도 스터디로 알게 된 분의 레포지토리를 구경 덕분이었다.
다양한 언어 뿐만 아니라 새로운 기술들도 빠르게 습득해서 자신만의 토이 프로젝트를 만들어 가시던 분이었다.
나는 프롬프트나 LLM이 너무 생소했기 떄문에, 그 분께 따로가서 어떤 레퍼런스를 보고 Open AI를 사용하게 되었냐고 여쭤봤었다. 유투브에서 Open AI를 검색해 걸리는 영상들을 참고했다는 말씀을 듣고 나도 바로 따라 진행했다.
구글과 유투브에 Open AI와 Node를 키워드 삼아 검색하고 참조된 깃허브링크들을 모두 찾아내서 어떻게 Open AI를 부르고 세팅하는 지를 확인했다.
프로젝트들을 진행한 시기도 달랐기 때문에 사용하는 모델도 달랐다. 사람들의 코드를 탐독하여 세팅방법을 대강 숙지한뒤 Open AI 공식문서에 들어가서 모델과 옵션들을 확인했다.
그렇게 나는 제일 저렴하고도 성능이 좋은 “gpt-4o-mini”를 선택했다.
지금은 파이썬으로 LLM을 배우고 있는데, Lang chain에서 JS/TS 지원도 잘 되어있다는 것을 알게 됐다.
나중에는 이걸 써도 될 것 같다.
부스트팔은 동물들이 글과 이미지를 분석하여 응원과 위로를 전달하는 메세지를 전달한다.
그렇기 때문에 나는 system 문구에서 부터 이를 입력 시켜줘야하는데 다양한 사람들의 세팅을 보고 참고했다.
const SYSTEM_ROLE_MESSAGE: Message = {
role: "system",
content:
"당신은 사람들을 응원하는 것을 좋아하는 귀여운 동물 캐릭터입니다.
누군가가 무언가를 성취했거나 격려가 필요할 때마다 긍정적이고 기운을 북돋아주는 메시지를 전달합니다.
당신은 이름이 담찌이며, 귀엽고 사랑스러운 햄스터 입니다.
당신의 응답은 항상 따뜻하고 친절하며, 열정이 가득합니다.
당신의 목표는 사용자가 동기부여를 느끼게 하여 다음에도 챌린지를 달성하게 하는 것입니다.
메시지는 반드시 존댓말과 구어체 한국어로 전달해야합니다. 두 줄 분량으로 전달해주세요.",
};
LLM 스터디를 하기전에 작성하고 설정한 것인데, 생각보다 나쁘지 않게 작성한 것 같다.
추후에 이 system 문구도 수정할 계획에 있다.
처음 생각해내서 구현 해본 것은 폴링이었다.
Open AI 메시지를 수신하고 테이블에 등록되기까지는 1분이 지연되지 않을 테니 주기적으로 요청을 받아오면 어떨까? 싶었다.
하지만, 등록에 실패 할 경우에는 등록 될 때 까지 무한대로 서버에 요청을 보내는 것을 보면서 이 선택은 잘못 된 것이다. 라는 생각이 들었다.
찾고 찾다보니, Server-Sent-Evnets나 웹소켓이 좋았겠다는 생각을 했다. 서버에서 테이블을 구독하고 있다가 등록이 되면 클라이언트에게 알림을 주면 그것을 렌더링 해주는 방식으로 말이다.
그렇게 찾아 낸 것은 supabase realtime.
마침 현재 사용하고 있던 데이터베이스도 supbase 였기 때문에 선택하기에 제격이었으며, 백엔드에 추가적으로 작업하지 않고도 웹소켓과 같은 기능 사용할 수 있기 때문이다.
작성한 코드를 가져오면 대강 이렇다.
initial data는 서버사이드에서 불러와 프롭으로 내려준다.
만약, 아직 테이블에 등록되지 않은 경우라면 null 값을 반환하여 사용자에게는 로딩상태를 보여준다.
AI comment의 데이터 타입은 supabase realtime 에서 반환되는 값의 기준으로 정의했다.
또한, initial data가 있는 경우라면 AiComment 테이블을 구독하지않아도 되기떄문에 조건문으로 분기 처리하여 최초 작성시에만 구독하는 것으로 설정했다.
이 부분을 해결해 나가면서는 sql도 좀 공부 해봐야겠다는 생각이 들었다.
나의 고난은 여기서 부터 시작되었다.
대충 진행되는 시간을 산출해보자면, 글 등록 1~2초 / AI 메시지 생성 3 ~7초 / AI 코멘트 등록 1 ~ 2초 가 되는 것으로 보인다.
사용자가 글을 쓰고 나서 “작성완료” 버튼을 누르고 약 10초 이상의 시간을 감당하게 되는 끔찍한 일이 발생한다.
숨막히는 너와 나의 눈치 싸움.
그런데 이게 또 백엔드 로직에서 발생하는 지연이었기 때문에 어떻게 해결해야할 지를 몰라했다.
어떻게 검색할지, 어떤 부분을 개선해야할지 전혀 감이 오지 않았다.
알고지내는 백엔드 개발자 분께 현재 상황을 말씀드리고 키워드를 받을 수 있었는데, “Pub/Sub 패턴”을 말씀하시면서, 외부 API를 사용할 때에는 비동기 또는 이 패턴을 많이 사용한다라고 말씀 해주셨다.
“Pub/Sub 패턴”을 검색해보니 해야할 게 많아보여서 비동기라는 정말 쉽고 편해보이는 것으로 선택했다.
그냥 단순하게 setImmediate
함수를 사용했던 것이다.
export async function uploadPost(formData: FormData) {
// form Data 값으로 Post 테이블에 등록
// 글 등록 완료시 해당 콜백함수를 비동기로 실행
setImmediate(async () => {
// open AI 호출
const aiMessage = await postOpenAI(/* 메세지에 넣어줄 정보 */);
// AI comment 테이블에 등록
if (aiMessage.content) {
await db.aiComment.create(/* ai comment 생성 */});
}
});
// 글 등록 완료시에 바로 리다이렉트 시킨다. (ai 코멘트 생성 여부 상관없이 실행)
redirect(`/posts/${post.id}`);
}
}
물론, 이 코드는 에러를 핸들링 할 수 없다는 치명적인 단점이 있다.
그래도 기능구현이 먼저 이기에 이 방법을 택했다.
나중에 ai comment를 조회했을 떄 테이블이 비어있다면 생성하는 로직을 구현해도 되니깐 이라는 안일한 마음을 먹고.
그렇게 안일한 마음으로 진행한 개발은 빠르게 문제가 일어났다.
무료에다가 정말 편하면서도, Next.js와도 잘 맞는 vecel에 테스트할 겸 배포를 진행했다.
하지만 Vercel은 서버리스 배포 환경이기 때문에 비동기 함수 setImmediate
로 감싼 콜백이 완료 되기전에 글 등록 uploadPost
함수는 타임아웃으로 인하여 종료하게된다.
그렇다. 서버리스 환경 배포 와 setImmediate
둘 중에 선택했어야 했으며, 나또한 에러핸들링도 어렵고 그 원인조차 분석하기 어려울 것이 예측되는 setImmediate
를 교체하기로 한다.
일단 조언받은 ‘비동기’, ‘pub/sub패턴’ 키워드로 찾기 시작했다.
프로젝트 간단하게 남에게 소개할 수 있는 기회가 있는 8월 29일까지는 핵심기능인 ai comment 확인까지는 꼭 완성시키는 것이 목표였다. 그렇기 때문에 원리를 완벽히 이해하는 것 보다는 “구현할 수 있는 가?”에 초점을 두었다.
다양한 플랫폼에서 원하는 기능을 제공하고 있었지만, 이전에 AWS로 API Gateway, Lambda, EC2 작업을 해봤던 경험이 있었기 때문에 AWS에서 제공하는 서비스를 선택하기로 했다.
그렇게 고른 것이 AWS SQS + AWS Lmabda 조합이다.
Amazon SQS는 분산 소프트웨어 시스템과 구성 요소를 대기열 서비스로서 분리하고 확장합니다. 일반적으로 단일 구독자를 통해 메시지를 처리하므로 주문 및 손실 방지가 중요한 워크플로에 적합합니다. 더 넓은 배포를 위해 Amazon SQS를 Amazon SNS와 통합하면 팬아웃 메시징 패턴이 구현되어 한 번에 여러 구독자에게 메시지를 효과적으로 푸시할 수 있습니다.
AWS 공식 홈페이지 출처
이를 세팅하는 것은 아래를 참고했다.
선택하기 참 좋았던 이유 중 하나가 Amazon CloudWatch 를 통해서 비동기적으로 동작하는 람다 함수 상태를 확인 할 수 있다는 점이다. consumer 람다함수에 이상 탐지를 켜두어 디버깅 작업에 용이했다.
AWS Lambda | AWS SQS |
---|---|
당시, 풀리퀘스트를 작성할 때 AWS SQS + AWS Lambda가 알맞은 선택인가에 대해서 고민을 많이 했었다.
이런 파트를 고민해 본적도 처음이기도 했고 에러 핸들링을 하고 있지 않는다면, 더 간단하고 가벼운 AWS API GateWay + AWS Lambda 조합도 괜찮지 않았나 생각을 했었다.
하지만, AWS SQS는 기본적으로 에러 발생시 기본적으로 두번 함수를 재실행 시켜주기도 하면서 추가적인 기능 덧붙여 핸들링 할 수 있다는 것을 깨달았다. 추후에 섬세하고 더 안정화된 개발로의 고도화를 진행한다면 해볼 수 있는 것이 많아 보였다.
아래는 동일한 게시글을 배포 환경과 개발 환경에서의 데이터 렌더링 차이를 보이고 있었다.
현재, 글에 대한 조회수 기능을 제공하기 위해서 서버액션으로 조회해오는 글 상세 데이터를 unstable_cache
로 캐싱하고 있었다.
좋아요 버튼과 댓글 상호작용에 따라 리렌더링이 되어 다시 데이터를 받아오게 하지않기 위함이다.
이로 인해 상호작용을 위해 revalidate를 적용한 좋아요, 댓글과 CSR처리를 한 AI 코멘트를 제외한 데이터는 전부 캐싱이 되어서, 글이나 데이터가 삭제되어도 캐싱이 남아 있는 문제가 발생했다.
로컬에서는 빌드파일을 삭제하면 캐싱이 제거가 되어 데이터베이스 값과 일치되게 되지만, 배포 환경에서는 빌드 파일을 삭제하여도 해결 되지 않았다.
캐싱된 데이터를 렌더하는 잘못된 상황(배포 환경의 모든 사용자가 보이는 상황) | 실제 올바르게 렌더한 상황 (개발 환경) |
---|---|
이 문제점 발생원인은 작성자가 글을 삭제하여도 캐싱된 해당 글의 데이터를 revalidate 시키지 않기 때문이었다.
무분별한 캐싱이 아닌 꼭 필요한 캐싱인가를 고민하여, 조회수 기능을 살리기위해서 revadliate 전략을 변경했다.
글을 삭제하고나서 revalidatePath를 부르고 사용자가 프로필 이미지를 수정하게되면 revalidateTags를 불러 캐싱된 데이터를 재검증 하는 것으로 더 섬세하게 관리하는 것으로 채택했다.
또한, revalidate tags를 관리 편의성을 높이기 위해 cacheTags 객체를 만들어 한 곳에서 관리하는 것으로 변경했다.
하지만, 이미 배포 환경에서 캐싱된 값은 어떻게 지울까?
Next.js의 캐싱 데이터는 빌드하기전에 이전 빌드 결과물을 삭제하여도 반영되지 않았다. 그 이유는 더 빠른 로드를 위하여 캐싱 데이터를 독립적인 위치에 두기 때문이다.
이미 배포환경에서 캐싱된 데이터를 제거하기 위해서 Vercel > Setting > Data Cache > Purge everything
작업을 진행했다. 이로써, 배포환경에서 이미 캐싱되어있던 데이터를 삭제하고 데이터베이스와 일치하는 데이터를 렌더링한다.
이 프로젝트에서 정말 만들고 싶었던 것은 두 가지였다.
프론트 작업이나 백엔드 작업이나 둘 다 쉽지만은 않았다.
회고록의 큰 분량을 차지하고 있는것이 1번 항목이며, 아직까지도 이렇게 개발하는 것이 맞는가?라는 의문을 가지고 있다.
2번 항목의 프론트 작업은 이전 프로젝트에서 직접 만든 useCalendar
훅을 그대로 가져와 작업했다. 그렇기 때문에 개발 공수가 적었지만, 직접 쿼리문을 작성하는 것은 생각보다 쉽진 않았다.
개발자는 어떻게든 문제를 해결 해내야한다. 라는 말을 많이 봐왔고 공감한다.
이 프로젝트에서 그 문장을 체감했다. 짧은 시간 동안, 정말 다양한 것을 시도해보고 문제를 만났고 해결해 나갔다.
프론트엔드 범위를 넘어서 다양한 범위를 경험할 수 있었고 만족스러운 결과를 만들어 냈다.
개발을 알게되고 시작하고 나서, 내가 정말 만들어 보고 싶은 것이 없었기에 이런 감정은 색다르다.
리드미에도 작성해뒀던 문구가 있다.
한 명이라도 쓸 수 있는, 쓰고 싶어하는 서비스를 만들고 싶었습니다. 지금 이 서비스의 고객은 개발자인 저를 타겟팅합니다.
8주 달리기 챌린지를 완주하며 받았던 응원을 기억하고 싶었고, 그걸 유지하며 다양한 성취들을 해나가고 싶었다.
그래서 정말 나를 위해 만들었다.
지금은 핵심기능을 구현 MVP 단계를 만족시켰다.
세부적인 에러 핸들링과 다양한 케이스를 고려한 방어 코드가 작성되어있지 않고, 추가적으로 편의성을 담은 기능도 부족하다.
큰 산을 넘었기에 기쁜 마음으로 프로젝트 고도화를 고민 하고있다.
멋진 프로젝트의 개발 회고록 공유해주셔서 감사합니다!!
만들고 싶은 기능을 어떻게든 연구해서 완성시키는게 정말 인상깊었습니다.
덕분에 많이 배워갑니다~ 🌸