프론트엔드
백엔드·데이터
콘텐츠·배포
Express(SSR) + MongoDB + Docker + EC2로 직접 운영하던 개인 블로그를 Next.js App Router + MongoDB Atlas + Vercel 조합으로 다시 만든 프로젝트입니다. 지금 보고 계신 이 사이트입니다. 옛 블로그의 게시글을 실제로 이관해서, 예전 링크가 죽지 않은 채로 갈아탔습니다.
태그를 누를 때마다 DB를 다시 왕복하고 있었습니다. 프로덕션 빌드에서도 클릭 한 번에 140~250ms가 걸렸고, 개발 환경에서는 더 느렸습니다.
서버 페이지네이션을 유지하며 쿼리를 최적화할지, 목록 전체를 한 번만 받아 로컬에서 거를지 저울질했습니다. 글이 수십 편 규모라 전부 받아도 부담이 없다고 판단해 후자를 골랐습니다.
클릭 반응이 즉시로 바뀌었습니다. 대신 글이 크게 늘면 되돌려야 하는 선택이라, 서버 페이지네이션 함수를 지우지 않고 남겨뒀습니다.
로그인 링크를 화면 하단 공통 영역에서 서버 측으로 판별했더니, 그 영역이 모든 페이지에 들어가는 탓에 쿠키를 읽는 순간 사이트 전체가 매 요청 렌더링으로 넘어갔습니다. 빌드 출력의 라우트 표시가 정적에서 동적으로 바뀐 것을 보고 알아챘습니다.
인증 표시가 필요한 자리만 클라이언트 컴포넌트로 떼어내 세션을 조회하도록 바꿨습니다. 목록·소개·프로젝트 페이지의 정적 렌더링을 되찾았습니다.
보안이 아니라 표시 용도의 분기라 클라이언트로 내려도 안전합니다. 실제 접근 차단은 미들웨어가 맡습니다.
메신저에 링크를 공유하면 미리보기가 엉뚱하게 나왔습니다. 캐시 문제로 짐작하기 쉬운 증상이었습니다.
프로덕션 주소를 직접 호출해보니 미리보기 이미지 주소가 500을 반환하고 있었습니다 — 운영 로그를 거슬러 올라가 보니 2주 넘게 모든 글이 그런 상태였습니다. 스크래퍼가 실패한 이미지 대신 페이지의 첫 번째 이미지, 즉 작성자 프로필 사진을 가져간 것이었습니다. 런타임 로그에서 폰트 파일을 찾지 못한다는 오류를 확인했고, 원인은 이미지 생성에 쓰는 한글 폰트를 실행 시점에 디스크에서 읽는데 그 폴더가 서버리스 함수 번들에 포함되지 않는다는 것이었습니다.
빌드 도구가 이 읽기를 추적하지 못해 경고조차 없었습니다. 게다가 사이트 공용 이미지 하나는 빌드 시점에 미리 만들어져 정상이었던 탓에, 문제가 더 가려져 있었습니다.
번들에 폰트를 명시적으로 포함시켜 해결했습니다. 더해서 폰트를 못 찾더라도 500 대신 글자 없이라도 응답하도록 바꿔, 같은 실패가 재발해도 프로필 사진이 새지 않게 했습니다. 배포 후 실제 이미지를 내려받아 한글 제목이 들어간 것까지 확인했습니다.
원인이 하나가 아니었습니다. 셋을 분리해 각각 처리한 뒤에야 체감이 달라졌습니다.
첫째, 페이지가 매 방문마다 DB를 조회하고 마크다운을 다시 컴파일하고 있었습니다. 미리 생성해두고 주기적으로 갱신하는 방식으로 바꿨습니다.
둘째, 메타데이터 생성과 페이지 본문이 같은 글을 각각 조회해 한 번의 요청에 중복 쿼리가 나가고 있었습니다. 요청 단위 캐시로 묶었습니다.
셋째, 소유자 전용 버튼을 그리려고 부른 인증 조회가 이 페이지를 매 요청 렌더링으로 묶어두고 있었습니다. 위 설계 판단과 같은 방식으로 떼어냈습니다.
서치 콘솔에 "발견됨 — 현재 색인이 생성되지 않음"이 22건 쌓여 있었습니다. 짐작하는 대신 사이트의 모든 페이지를 렌더된 HTML 그대로 받아 글 링크를 전수 수집해 사이트맵과 대조했습니다. 당시 글 19편 중 5편이 어디에서도 링크되지 않는 고아 페이지였습니다. 목록이 다섯 편씩 잘라 보여주는데 페이지 번호가 버튼이라, 자바스크립트를 실행하는 크롤러조차 2페이지로 넘어갈 방법이 없었던 겁니다.
페이지 번호를 실제 앵커로 바꾸되 클릭하면 기존 클라이언트 페이지네이션을 그대로 타게 해 사용자 경험은 그대로 두고, 크롤러가 밟을 정적 페이지 라우트를 따로 만들었습니다. 수정 후 빌드 산출물 전체에서 같은 대조를 다시 돌려 19편 전부에 링크가 닿는 것을 확인했고, 배포 후 라이브에서도 재확인했습니다.


