3D 블로그를 만들다 보면 성능 최적화는 일단 나중 문제라고 생각하기 쉽다.
나도 처음엔 그랬는데, Lighthouse를 돌려보니 생각보다 심각했다.
/blog/all 페이지인데 Performance가 40점.
LCP는 155.7초가 나왔다.
글 카드 몇 개 보여주는 목록 페이지에서 나올 점수는 아니었다.
홈에서는 Three.js로 3D 씬을 사용하고, 블로그에서는 일반적인 글 목록과 상세 페이지만 보여준다.
그런데 Network 탭을 열어보니 블로그 페이지에서 약 35MB를 다운로드하고 있었다.
WalkingAstro.glb, cosmos 텍스처 등 홈에서 사용하는 3D 에셋이었다.
처음에는 "3D 에셋이 원래 무거우니까 그런가?" 싶었다.
그런데 생각해보면 /blog/all에서는 3D를 아예 렌더링하지 않는다.
그래서 어디서 이 파일들을 요청하는지 따라가 봤다.
블로그에서 useToast 하나 import
→ @/shared/ui/index.ts
→ HelloBot, WalkingAvatar re-export
→ useGLTF.preload() 실행
→ 3D 에셋 약 35MB 다운로드
원인은 barrel 파일이었다.
index.ts 하나 만들어서 컴포넌트를 모아 export해두면 import하기 편하다.
나도 그렇게 쓰고 있었는데, 여기서 생각보다 많은 코드가 같이 따라왔다.
특히 useGLTF.preload()가 모듈 로드 시점에 실행되고 있어서 컴포넌트를 실제로 사용하지 않아도 3D 에셋을 미리 받고 있었다.
즉, 블로그에서 3D를 사용하지 않는 것과 블로그에서 3D 에셋을 받지 않는 것은 다른 문제였다.
이것 말고도 몇 가지 문제가 더 있었다.
상세 페이지에서 읽기만 하는데 Novel/TipTap 뷰어를 통째로 로드
페이지 전환 오버레이에서 width를 애니메이션하면서 레이아웃 변화 발생
AI 요약이 붙으면서 본문 위치가 밀림
목록에서 클라이언트 fetch를 기다린 뒤 카드가 렌더링됨
먼저 @/shared/ui, @/widgets/home의 index.ts에서 Three.js 관련 export를 뺐다.
홈에서는 필요한 3D 모델을 직접 import하도록 바꾸고, 블로그에서 사용하는 Macintosh 씬은 dynamic import로 분리했다.
결국 중요한 건 블로그에서 3D를 안 그리는 것보다, 3D 모듈 자체가 로드되지 않도록 만드는 것이었다.
이걸 수정하고 Network 탭을 다시 확인하니 블로그에서 35MB씩 받아가던 문제가 사라졌다.
상세 페이지에서는 글을 읽기만 하면 되는데, 기존에는 Novel/TipTap 관련 코드가 같이 들어가고 있었다.
그래서 읽기 전용 PostContentViewer를 따로 만들었다.
에디터에서 사용하던 CodeBlock UI도 그대로 유지했다.
언어 선택이나 Mermaid 같은 기능도 상세 페이지에서 사용할 수 있도록 했다.
처음에는 PostHtmlViewer도 만들어봤다.
HTML을 그대로 렌더링하면 더 간단할 것 같았는데, Vercel에 배포하니 jsdom이 SSR 번들에 들어가면서 500 에러가 발생했다.
게다가 기존 에디터와 코드블록 UI도 달라졌다.
결국 이 방식은 버리고 지금의 PostContentViewer 구조로 돌아왔다.
/blog/all에서는 처음부터 글 목록을 보여줘야 하니까 데이터를 클라이언트에서 가져올 필요가 없었다.
그래서 서버에서 getPostList를 호출하고 initialData로 넘겼다.
덕분에 첫 번째 카드가 API 요청이 끝난 뒤 나타나는 게 아니라 처음부터 HTML에 포함돼서 내려오게 됐다.
LCP 후보가 늦게 나타나는 문제도 같이 줄일 수 있었다.
이외에도 몇 가지를 같이 정리했다.
/blog에서는 페이지 전환 오버레이 제거
AI 요약 영역에 min-height 적용
optimizePackageImports 적용
GA는 lazyOnload로 로드
초기 로드에 필요하지 않은 코드 정리
Mobile Lighthouse에서 /blog/all을 기준으로 측정했다.
Before | After | |
|---|---|---|
Performance | 40 | 100 |
LCP | 155.7s | 0.8s |
TBT | 1,510ms | 0ms |
전송량 | 약 35MB | 약 0.7MB |


점수를 올리는 데 가장 크게 영향을 준 건 결국 블로그에서 홈의 3D 에셋을 받지 않도록 한 것이었다.
35MB에서 약 0.7MB까지 줄어든 게 가장 컸다.
나머지 SSR, 에디터 분리, CLS 개선 같은 작업들이 거기에 더해졌다.
그건 아니다.
Lighthouse는 특정 환경에서 실행하는 Lab Data이고, Core Web Vitals는 실제 사용자 데이터를 기반으로 보는 Field Data다.
Lighthouse와 CWV에서 겹치는 지표도 있지만, Lighthouse에서 100점이 나왔다고 실제 사용자 환경에서도 문제가 없다고 볼 수는 없다.
이번 작업에서 직접적으로 연결되는 부분을 보면,
3D preload 제거 + 목록 SSR → LCP
오버레이 및 요약 영역 높이 조정 → CLS
에디터 JS 제거 → TBT 감소, 결과적으로 INP에도 도움이 될 가능성
정도다.
INP는 이번에 직접 측정하지 않았기 때문에 개선됐다고 말하기는 어렵다.
결국 Lighthouse 100점과 실제 사용자 환경의 CWV 통과는 별개의 문제다.
필드 데이터는 Search Console이나 CrUX에서 따로 확인해야 한다.
3D 블로그에서 글 페이지가 느리면 처음에는 본문 렌더링이나 이미지부터 의심하기 쉽다.
나도 처음엔 그렇게 생각했다.
그런데 실제 원인은 블로그 페이지에서 사용하지 않는 홈의 3D 에셋을 같이 받고 있던 것이었다.
특히 index.ts로 export를 모아두고, 그 안에서 useGLTF.preload()까지 사용하고 있다면 한 번 확인해볼 만하다.
useToast 하나 import했는데 35MB가 따라오는 상황이 실제로 생길 수 있다.
결국 이번에 가장 많이 확인했던 건 Network 탭이었다.
"이 페이지에서 3D를 안 쓰는데 왜 35MB를 받고 있지?"
이걸 찾고 나니까 Lighthouse 40점의 원인이 생각보다 빠르게 보였다.
3D를 사용하는 프로젝트라면, 단순히 "이 페이지에서는 3D를 렌더링하지 않는다"에서 끝내지 말고 실제로 해당 페이지에서 3D 모듈과 에셋을 로드하고 있지는 않은지까지 확인해보는 게 좋을 것 같다.