나닮 Web Enhancement — DB, API, 프론트 한 번에 손본 기록
feature/web-enhancement 브랜치에서 DB 성능, 프론트 성능, API 보안, 테스트 품질 네 가지 축으로 개선을 몰아서 진행했다. 한 번에 다 쓰진 않고, 손댄 부분만 요약해 둔다. 각 항목마다 변경 전엔 어떤 한계가 있었고, 바꾼 뒤엔 어떤 특징이 생겼는지, 그래서 어떤 장점이 생기는지까지 적어 두었다.
복합 인덱스 추가
변경 전
PostgreSQL은 “이 조건으로 찾아줘”라고 하면, 인덱스가 잘 맞지 않으면 테이블 전체를 한 줄 한 줄 읽는 sequential scan을 한다. 데이터가 적을 때는 몰라도, 일기·채팅·알림·친구 목록이 쌓이면 “날짜별로”, “최신순으로” 같은 조회가 자주 느려진다. 컬럼마다 단일 인덱스만 있으면, “user_id + 날짜 + 읽음 여부”처럼 여러 조건을 동시에 쓸 때 옵티마이저가 인덱스를 제대로 활용하지 못하는 경우도 있다.
변경 후
자주 쓰는 쿼리 패턴에 맞춰 복합 인덱스 7개를 넣었다. 일기 날짜별 조회, 채팅 목록 최신순, 메시지 시간순, 읽지 않은 알림, 친구 요청/목록, 분석 조회 등이다. 알림 테이블은 사용자·읽음 여부 각각 걸려 있던 단일 인덱스를 빼고, 사용자 + 읽음 여부 + 생성일시 3컬럼 복합 인덱스 하나로 통합했다.
장점
해당 구간은 seq scan 대신 index scan으로 바뀌어서, 조건에 맞는 행만 골라 읽게 된다. 테이블이 커져도 “날짜별/최신순/읽지 않은 것만” 조회하는 비용이 덜 늘어난다. 배포 시엔 alembic revision --autogenerate 후 upgrade head 한 번 돌리면 된다.
N+1 쿼리 정리
변경 전
ORM에서 관계된 데이터를 lazy로 가져오면, “목록 10개 조회 → 각 항목마다 연결된 데이터를 한 번씩 더 조회”처럼 1 + N번 쿼리가 나간다. 목록 한 번 열 때 DB를 1+10번 치는 식이라, 데이터가 늘수록 응답 시간이 선형으로 나빠진다. 반복문 안에서 하나씩 DB를 찔러보는 전형적인 N+1 패턴이었다.
변경 후
관계된 데이터를 미리 한 번에 가져오는 방식으로 바꿨다. SQLAlchemy의 joinedload / selectinload로 JOIN 또는 IN 쿼리 한 번에 가져오거나, ID만 모아서 배치 조회로 처리했다. 목록 N개를 가져올 때 연결된 엔티티를 N번 따로 조회하던 걸 한 번의 조인/배치로 줄였다. 채팅·친구·추천 등 관련 API에서 lazy 로딩을 제거하고 eager load나 배치 조회로 통일했다.
장점
요청당 쿼리 수가 크게 줄어서, 채팅·친구 목록 로딩이 체감될 정도로 빨라졌다. 사용자 수·데이터가 늘어나도 “목록 한 번 조회 = DB 호출 1~2번” 수준으로 유지할 수 있다.
Rate Limiting 확대
변경 전
로그인/회원가입 같은 auth 쪽에만 rate limit이 걸려 있었다. LLM을 쓰는 API(채팅, 페르소나 생성, 퀴즈, 일기 주제 제안, 멘탈 피드백 등)에는 제한이 없어서, 악의적인 반복 호출이나 실수로 인한 비용 폭주, 서비스 남용 가능성이 열려 있었다.
변경 후
LLM을 호출하는 엔드포인트 11곳에 제한을 걸었다. 대화 생성·스트리밍은 20회/분, AI 생성·진화·퀴즈류는 3회/분, 제안·인사이트·피드백·리포트류는 5회/분 식으로 나눴다. 기존 rate limit 라이브러리에 분당 제한 데코레이터만 추가했고, 클라이언트는 이미 429(Too Many Requests) 처리가 되어 있어서 추가 작업은 없었다.
장점
LLM 호출이 분당 횟수로 제한되면서 비용 폭주와 남용을 막을 수 있다. 정상 사용자에게는 거의 걸리지 않고, 이상 트래픽만 걸러진다.
배경 이미지 최적화
변경 전
메인 배경 이미지가 5MB가 넘는 PNG라서, 모바일에서 첫 화면 로딩 시 이 이미지 하나만으로도 몇 초씩 걸리는 느낌이 났다. 데스크톱/모바일 구분 없이 같은 큰 파일을 받고, 포맷도 용량이 큰 PNG라서 네트워크·배터리 부담이 컸다.
변경 후
WebP로 바꾸고 용도별로 나눴다. 데스크톱용·모바일용 두 해상도로 나누어 각각 19KB, 6KB 수준으로 줄이고, 레이아웃에서 반응형으로 화면 크기에 맞는 이미지만 로드한다. WebP 미지원 브라우저를 위해 PNG는 리사이징해 fallback으로만 두었다.
장점
5MB → 19KB/6KB 수준으로 줄어서 첫 로딩 체감이 크게 좋아진다. 모바일에서는 작은 해상도 이미지만 받으므로 데이터 사용량과 배터리 소모도 줄어든다.
React.lazy 코드 스플리팅
변경 전
앱 진입점에서 페이지 컴포넌트를 전부 static import 하고 있어서, 사용자가 첫 화면만 써도 접근하지 않는 페이지 코드까지 초기 번들에 다 들어가 있었다. 그만큼 첫 로딩 시 받아야 할 JS 양이 커졌다.
변경 후
레이아웃·인증 라우트·앱 설치 안내 등 꼭 필요한 컴포넌트만 그대로 두고, **나머지 페이지는 전부 React.lazy**로 바꿨다. 라우트 전체를 Suspense로 감싸서, 해당 페이지로 들어갈 때만 해당 청크를 불러오도록 했다. 한 디렉터리에서 여러 페이지를 한꺼번에 불러오던 barrel import는 제거하고, 페이지별로 개별 lazy import 하도록 정리했다.
장점
초기 번들에서 페이지 코드가 빠져 나가서 첫 로딩이 가벼워진다. 빌드해 보면 청크가 갈라져 나오고, 실제로 필요한 화면에 들어갈 때만 그 코드를 받게 된다. “처음엔 꼭 필요한 것만 로드하고, 나머지는 필요할 때 로드한다”는 패턴이라, 비슷한 구조의 앱을 다룰 때 참고하기 좋다.
백엔드 테스트 추가
변경 전
테스트 설정 파일에 DB·클라이언트 fixture만 있고 실제 테스트는 없었다. API를 수정할 때마다 수동으로 호출해 보거나, 배포 후에야 문제를 발견하는 식이라, 리팩터링이나 의존성 업데이트 시 부담이 컸다.
변경 후
이메일 인증까지 끝낸 테스트 유저 두 명과, 각각의 JWT 인증 헤더를 주는 fixture를 추가했다. 인증 테스트로 회원가입(성공·중복 이메일·중복 사용자명), 로그인(성공·잘못된 비밀번호·없는 사용자·이메일 미인증), 보호된 엔드포인트(토큰 없음·유효·잘못된 토큰) 11개. 핵심 도메인 테스트로 생성(성공·유효하지 않은 날짜·중복 거부), 조회(빈 목록·상세·타인 접근 불가·페이지네이션), 수정·삭제 8개를 넣었다. pytest로 한 번에 돌려서 확인할 수 있다.
장점
인증·일기 같은 핵심 플로우를 자동으로 검증할 수 있어서, 변경 후 회귀를 빠르게 잡을 수 있다. 나중에 다른 엔드포인트나 비즈니스 로직을 손대더라도, 이 테스트들이 안전망 역할을 해 준다.
배포할 때 체크할 것
- DB 마이그레이션 — 복합 인덱스 적용 (Alembic 등 사용 시 revision 생성 후 upgrade)
- 인덱스 적용 여부 — DB에서 해당 테이블 인덱스 확인
- 테스트 실행 — pytest 등으로 회귀 여부 확인
- 프론트 빌드 — 코드 스플리팅 청크가 나오는지 확인
- 캐시·PWA — 새 이미지 파일은 캐시 무효화가 필요 없을 수 있고, PWA라면 앱 버전을 올려두는 걸 권장
변경 파일은 백엔드 15개, 프론트 5개 정도였고, 이번 턴에서 특히 N+1 제거와 배경 이미지·코드 스플리팅이 체감이 컸다.