White Place 사이트 업그레이드 기록3D 히어로부터 이미지 최적화까지
포트폴리오 사이트를 만들고 나면 방치하게 된다. "나중에 고쳐야지" 하면서 몇 달이 지나는 게 보통이다.
White Place도 마찬가지였다. Next.js 14로 만든 건 좋은데, 히어로 섹션은 밋밋하고, 이미지는 최적화 없이 날것으로 서빙되고 있었다. 이번에 한번 제대로 손보기로 했다.
3D 히어로 씬
첫 번째로 손댄 건 히어로 섹션이다. 기존에는 텍스트만 덩그러니 있었는데, React Three Fiber로 3D 글래스 구체를 넣었다.

@react-three/fiber와 @react-three/drei를 쓴다. 구체에 MeshTransmissionMaterial을 적용하면 유리처럼 투명한 굴절 효과가 나온다. 궤도를 도는 링은 단순한 TorusGeometry인데, 애니메이션 하나 넣으면 꽤 고급스러워진다.
주의할 점은 SSR이다. Three.js는 브라우저 전용이라 dynamic(() => import(...), { ssr: false })로 감싸야 한다. 안 그러면 빌드에서 터진다. WebGL 미지원 브라우저도 체크해서 Canvas를 조건부 렌더링했다.
About + Features 섹션
히어로 아래에는 "하얀 책상 위에서 코드를 씁니다" 섹션을 배치했다. 글래스모피즘 카드 디자인이다.

CSS 변수 기반으로 만들어서 다크 모드 전환이 자연스럽다. var(--glass-bg), var(--glass-border) 같은 토큰을 쓰면 하드코딩 색상 없이도 테마를 유지할 수 있다.
Features 카드는 AI 뉴스, 포트폴리오, 블로그, 데스크테리어 리뷰 등 사이트의 주요 콘텐츠를 소개한다. 아이콘은 SVG로 직접 그렸고, Coming Soon 뱃지는 준비 중인 섹션에 붙여뒀다.

하단 Pricing 스타일의 Offerings 섹션은 실제 가격표가 아니라 사이트가 제공하는 가치를 정리한 것이다. 가운데 카드에 그라디언트를 넣어서 시선을 끈다.

Markdown 이미지 → next/image 최적화
이번 업그레이드에서 체감이 가장 큰 변경이다.
블로그와 데스크 리뷰 포스트에서 Markdown의  문법으로 넣은 이미지들이 그냥 <img> 태그로 렌더링되고 있었다. Next.js의 이미지 최적화 파이프라인을 전혀 타지 않는 상태.
문제점:
- 원본 WebP/PNG가 그대로 전송 (AVIF 변환 없음)
- lazy loading 미적용
- 반응형 sizes 없이 고정 크기
해결: ReactMarkdown의 components prop으로 img 태그를 next/image 래퍼 컴포넌트로 오버라이드했다.
// MarkdownImage.tsx
"use client";
import Image from "next/image";
export default function MarkdownImage({ src, alt }) {
if (!src) return null;
return (
<span className="block relative w-full my-6">
<Image
src={src}
alt={alt || ""}
width={800}
height={450}
sizes="(max-width: 768px) 100vw, 800px"
className="rounded-xl shadow-glass w-full h-auto"
loading="lazy"
/>
</span>
);
}
// blog/[slug]/page.tsx
<ReactMarkdown
components={{
img: ({ src, alt }) => <MarkdownImage src={src} alt={alt} />,
}}
>
이렇게 하면 Markdown에서 로 작성한 이미지가 자동으로 /_next/image?url=...&w=...&q=75 경로로 서빙된다.
변환 결과를 curl로 확인:
Content-Type: image/avif
Accept 헤더에 image/avif를 넣으면 Next.js가 원본 WebP를 AVIF로 자동 변환해서 응답한다. 브라우저가 AVIF를 지원하지 않으면 WebP로 폴백. 코드 한 줄 안 바꾸고 이미지 포맷 최적화가 끝났다.
수치로 보는 결과

최종 빌드 결과를 정리하면:
| 항목 | 수치 |
|---|---|
| 정적 생성 페이지 | 37 pages |
| next/image 변환 이미지 | 10개 (webp 5 + png 5) |
| TypeScript 에러 | 0 |
| 빌드 시간 | ~15초 |
| 이미지 포맷 | AVIF 자동 변환 확인 |
blog/[slug]와 desk-review/[slug] 두 곳에 동일하게 적용했다. 키보드 리뷰 포스트의 webp 4장, 멀티에이전트 포스트의 png 5장, 데스크 투어의 webp 1장 — 총 10개 이미지가 전부 next/image를 통해 서빙된다.
반응형 sizes 속성 덕분에 모바일에서는 뷰포트 너비에 맞는 작은 이미지를, 데스크톱에서는 800px 이미지를 로드한다. 불필요한 대역폭 낭비가 줄었다.
배운 점
사이트 업그레이드를 하면서 몇 가지 깨달은 게 있다.
CSS 변수가 핵심이다. 하드코딩 색상 하나 남기면 다크 모드에서 반드시 깨진다. var(--bg-primary) 같은 토큰으로 통일하면 테마 전환이 공짜다.
next/image는 ReactMarkdown과 잘 맞는다. components prop 하나로 모든 Markdown 이미지를 최적화 파이프라인에 태울 수 있다. 별도 플러그인이 필요 없다.
3D는 반드시 ssr: false. Three.js 관련 코드는 dynamic import + ssr: false가 기본이다. WebGL 미지원 체크까지 하면 더 안전하다.
다음에는 다크 모드 전용 테마와 이미지 blur placeholder를 추가할 계획이다.