프론트엔드 안티패턴 아티클을 써주는 AI 웹앱 만들기

KINA KIM·2025년 6월 26일
2
post-thumbnail

https://smelly-dev.vercel.app/

요즘 AI가 코딩도 잘하고 글도 잘 쓰고 나도 이걸 잘 활용해서 사이드 프로젝트 하나 빨리 만들어보면 어떨까 싶었다. 그러다 떠오른 게 '프론트엔드 안티패턴 아티클을 하루에 하나씩 자동으로 써주는 건 어떨까?'였다. '냄새난다'라는 의미의 smelly에서 아이디어를 따와서, 프로젝트 이름도 냄새나는 코드를 의미하는 'smelly.dev'로 지음.
이왕 AI 써보는 김에 로고도 AI로 만들어보려고 GPT를 사용했는데 말을 너무 안 들어서 포기하고 맘에 드는 폰트랑 색상 하나 잡아서 대충 내가 만들었다. 원래는 smelly에 m을 돼지코 모양으로 만들고 싶었다.

뭘 하나 요청하면 하나는 들어주고 하나는 안 들어준다. 이걸 여덟 번을 반복했는데 곧 죽어도 m을 돼지코로 안 바꿔주길래 포기했다.

주요기능

📚 콘텐츠 관리

  • 데일리 안티패턴: 매일 새로운 프론트엔드 안티패턴 제공
  • 난이도 분류: 초급/중급/고급으로 난이도 구분
  • 태그 시스템: React, TypeScript, CSS, 성능, 보안 등 카테고리별 분류

🤖 AI 자동화

  • 자동 콘텐츠 생성: GitHub Actions를 통해 매일 한국시간 12시에 자동 생성
  • 중복 방지: 기존 콘텐츠 분석을 통한 프롬프트 고도화로 중복 콘텐츠 방지
  • 품질 관리: 태그 사용 빈도 분석을 통한 균형잡힌 콘텐츠 구성
  • 실시간 업데이트: 생성 즉시 Firebase DB에 저장 및 웹앱 반영

인터페이스 및 프로젝트 구조

export interface Antipattern {
  id?: string;
  title: string;
  whyWrong: string;
  howToFix: string;
  summary: string;
  beforeCode: string;
  afterCode: string;
  links: string[];
  tags: string[];
  type: "프론트엔드" | "백엔드" | "데이터베이스" | "기타";
  difficulty: "초급" | "중급" | "고급";
  updatedAt?: Date;
  viewCount?: number;
}

문제 원인, 해결 방법, 요약, 전후 코드 비교 등이 포함된다. type, difficulty, tags는 현재는 아티클을 소개하는 UI에만 사용되고 있지만 추후에 type, difficulty, tags로 필터링해서 볼 수 있는 기능을 추가할 예정이라 미리 넣어두었다. 나중에 '인기 안티패턴' 같은 페이지를 만들 수도 있을 것 같아서 안티패턴 항목에 조회수 필드도 추가해뒀다. 타입도 같은 맥락으로 백엔드 등의 아티클이 작성될 수도 있어서 미리 추가해놨다.

관련 공식 문서나 참고할 수 있는 아티클 링크를 담은 links 배열도 넣긴 했는데 몇 개는 멀쩡한 링크를 가져오고, 어떤 건 아예 없는 링크를 가져와서 이건 프롬프트를 다시 손봐야 할 거 같다. '근거 있는 유효한 링크만 가져와라'고 명시했는데 잘 안 먹히는 듯.

FSD도 처음엔 적용해봤는데, 프로젝트가 작다 보니 오히려 관리 포인트만 늘어나길래 쓰는 부분만 적당히 커스터마이징해서 활용했다.

AI 선택

내가 사용한 모델은 Gemini 2.5 Flash다 원래는 GPT를 쓰려고 했는데 무료 토큰을 안 줘서 Gemini로 갈아탔다. 토큰을 꽤 많이 잡아먹긴 하는데 하루에 하나 정도는 무리 없이 생성 가능하다.

프롬프트

프롬프트 튜닝이 제일 빡세고 시간을 가장 많이 쏟았다. 게다가 분당 요청 횟수랑 하루에 쓸 수 있는 토큰이 제한되어 있어서 테스트 돌리는 것도 쉽지 않은데 AI 응답이 일정하지 않고, 중복된 안티패턴도 계속 뱉어냈다. 처음엔 어느정도 비슷한 포맷으로 주면 파싱하자라는 마인드였지만 역시 마음처럼 되지 않았다. 똑같은 프롬프트로 던져도 응답 포맷이 계속해서 완전히 달라지고 코드블럭 쓰지 말라고 강조까지 했는데도 알아서 코드블럭을 감싸서 주는 경우도 있었다. 여기저기 AI 프롬프트 관련 정보를 찾아보면서 어느정도 일정한 응답을 받을 수 있게 해놓은 상태긴 하지만 완벽하지는 않다.

비슷한 안티패턴을 뱉어내는 문제도 있었는데 유사성 문제는 주제별 태그 기반, summary 기반으로 유사하지 않은 글을 생성해달라고 요청하면서 어느 정도 잡았고 최근 자주 사용된 태그 말고 새로운 주제의 아티클을 요청하는 식으로 다양성도 챙겼다.

프롬프트 튜닝에 시간을 제일 많이 쏟았다. 이렇게 완성된 프롬프트는 firebase db에서 관리하다가 빌드 과정에서 가져오고 있다. 재밌던건 처음에는 script 코드 내부에서 관리하다가 이후에 firebase로 옮긴건데 관리 위치를 옮겼다고 응답이 달라졌다. 덕분에 응답 형태 맞추느라 또 시간을 썼다. 도대체 왜째서?

자동 업로드

하루에 하나씩 자동 업로드는 어떻게 하지? 싶어서 이것저것 찾아봤는데 GitHub Actions의 cron job을 이용하면 스케줄링이 가능하다는 걸 알게 됐고 GitHub Actions도 한번 써보고 싶어서 스케줄링은 이걸로 정했다. 현재는 매일 자정(한국 시간 기준)에 안티패턴을 Firebase DB에 자동 업로드하는 걸로 동작한다.
플로우는 이런 식이다.

  1. Git Action 스케줄러
  2. 기존 콘텐츠 조회
  3. 태그 사용 빈도 분석
  4. AI 프롬프트 생성
  5. Gemini 호출
  6. 응답 파싱 및 검증
  7. 중복 체크
  8. DB 저장
  9. 웹 반영

CSS/UI

UI는 Tailwind랑 shadcn/ui 조합으로 구성했다. shadcn은 이번에 처음 써봤는데 필요한 컴포넌트만 골라 쏙쏙 설치할 수 있고 커스터마이징도 쉬워서 만족스러웠다. 회사 다닐 땐 styled-components 위주로 써봐서 이번 기회에 tailwind도 써보자 싶어서 선택했고 매우 편했다. 반응형이나 상태 기반 유틸리티 클래스들이 특히 마음에 들었다.

etc

페이지네이션을 구현할까, 무한 스크롤을 구현할까 고민하다가 페이지네이션으로 하기로 했다. 아티클을 둘러보다가 뒤로가기를 눌렀을때 내가 있던 페이지에 위치하고 있는게 정적인 리스트 탐색 경험에서는 가장 안정적인 UX 방식이라고 판단했다.

배포까지 끝내고 끝났나 싶었는데 타임존 문제가 발생했다. 로컬에선 updatedAt이 제대로 표시되는데 Vercel에 배포하고 나니까 하루씩 밀려 나오더라. 이유는 Vercel이 UTC 기준으로 스케줄을 돌리기 때문이었다. 그래서 시간 표시할 땐 'Asia/Seoul' 타임존을 명시해주는 방식으로 해결했다.

실제로 사용자가 있을까 싶어서 GA도 붙여봤다. 있으면 나중에 개선할때 동기부여가 많이 될거 같다.

후기

기획도 코드도 급하게 짰고 추가할 아이디어는 많지만 이번 프로젝트의 1순위 목표는 '빠르게 개발해보는 것'이었기 때문에 일단 여기까지를 1차 완료로 두기로 했다. 다크모드, 북마크, 소셜 공유, 태그 기반 필터링 등등 추가 기능은 여전히 머릿속에 있음. 프롬프트도 더 다듬어서 고도화할 예정인데 토큰 제한 때문에 이건 좀 장기적인 플랜이 될 듯하다. 내가 안 보는동안 AI가 혼자 자동으로 뚝딱뚝딱 글 만들어서 아티클 뿌려주는 걸 보는 과정이 제일 재밌었다.

profile
까먹지 않으려고 쓰는 기록장

0개의 댓글