본문으로 건너뛰기
← 블로그로 돌아가기
개발>웹 개발
next.jsreact-three-fibernext-imageperformanceportfolioweb-dev

White Place 사이트 업그레이드 기록3D 히어로부터 이미지 최적화까지

5분 읽기

포트폴리오 사이트를 만들고 나면 방치하게 된다. "나중에 고쳐야지" 하면서 몇 달이 지나는 게 보통이다.

White Place도 마찬가지였다. Next.js 14로 만든 건 좋은데, 히어로 섹션은 밋밋하고, 이미지는 최적화 없이 날것으로 서빙되고 있었다. 이번에 한번 제대로 손보기로 했다.


3D 히어로 씬

첫 번째로 손댄 건 히어로 섹션이다. 기존에는 텍스트만 덩그러니 있었는데, React Three Fiber로 3D 글래스 구체를 넣었다.

White Place 3D 히어로 — React Three Fiber로 만든 글래스 구체가 배치된 메인 화면

@react-three/fiber@react-three/drei를 쓴다. 구체에 MeshTransmissionMaterial을 적용하면 유리처럼 투명한 굴절 효과가 나온다. 궤도를 도는 링은 단순한 TorusGeometry인데, 애니메이션 하나 넣으면 꽤 고급스러워진다.

주의할 점은 SSR이다. Three.js는 브라우저 전용이라 dynamic(() => import(...), { ssr: false })로 감싸야 한다. 안 그러면 빌드에서 터진다. WebGL 미지원 브라우저도 체크해서 Canvas를 조건부 렌더링했다.


About + Features 섹션

히어로 아래에는 "하얀 책상 위에서 코드를 씁니다" 섹션을 배치했다. 글래스모피즘 카드 디자인이다.

About White Place — 기록, 공유, 탐험 세 가지 키워드로 정리한 소개 섹션

CSS 변수 기반으로 만들어서 다크 모드 전환이 자연스럽다. var(--glass-bg), var(--glass-border) 같은 토큰을 쓰면 하드코딩 색상 없이도 테마를 유지할 수 있다.

Features 카드는 AI 뉴스, 포트폴리오, 블로그, 데스크테리어 리뷰 등 사이트의 주요 콘텐츠를 소개한다. 아이콘은 SVG로 직접 그렸고, Coming Soon 뱃지는 준비 중인 섹션에 붙여뒀다.

Offerings 카드 — 블로그 포스트, 오픈소스, 협업 세 가지 카드로 구성된 소개 섹션

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

Offerings + CTA — 가격표 스타일 소개와 함께 이야기해요 CTA 섹션


Markdown 이미지 → next/image 최적화

이번 업그레이드에서 체감이 가장 큰 변경이다.

블로그와 데스크 리뷰 포스트에서 Markdown의 ![alt](src) 문법으로 넣은 이미지들이 그냥 <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에서 ![alt](/images/foo.webp)로 작성한 이미지가 자동으로 /_next/image?url=...&w=...&q=75 경로로 서빙된다.

변환 결과를 curl로 확인:

Content-Type: image/avif

Accept 헤더에 image/avif를 넣으면 Next.js가 원본 WebP를 AVIF로 자동 변환해서 응답한다. 브라우저가 AVIF를 지원하지 않으면 WebP로 폴백. 코드 한 줄 안 바꾸고 이미지 포맷 최적화가 끝났다.


수치로 보는 결과

최근 포스트와 성장 지표 — 블로그 카드 3개와 프로젝트, 커밋, 경험 숫자 카운터

최종 빌드 결과를 정리하면:

항목수치
정적 생성 페이지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를 추가할 계획이다.

관련 포스트