Skip to main content
AI

Things 페이지 만들기 — 설계부터 이미지 최적화까지

포트폴리오 사이트에 내가 매일 쓰는 기기와 도구를 기록하는 Things 섹션을 새로 만들었습니다. 페이지 설계, 카테고리 필터, 마크다운 기반 콘텐츠 관리, 이미지 파이프라인까지 — 하루 동안 5번의 커밋으로 완성한 과정을 정리합니다.

왜 Things 페이지를 만들었나

블로그와 프로젝트 외에 "내가 실제로 쓰는 것들"을 기록하고 싶었습니다. 장비를 바꿀 때마다 이전에 뭘 썼는지 잊어버리기도 하고, 누군가 "그 키보드 뭐야?"라고 물었을 때 링크 하나로 보여줄 수 있는 페이지가 있으면 좋겠다고 생각했습니다.

Step 1. 카테고리 필터가 있는 목록 페이지

가장 먼저 /things 경로에 카드 그리드 형태의 목록 페이지를 만들었습니다. 초기 구조는 단순합니다:

  • content/things.json — 카테고리 정의 (EDC, Work, Home 등)
  • ThingsContent — 카테고리별로 아이템을 그룹핑하는 메인 컨테이너
  • CategoryNav — 카테고리 필터 (pill 버튼)
  • ThingCard — 개별 아이템 카드

카테고리는 7개로 나눴습니다:

카테고리 설명
🎒 EDC 매일 들고 다니는 것들
💼 Work 업무용 장비
🏠 Home 집에서 쓰는 것들
👶 Baby 육아용품
🤖 AI & Apps 소프트웨어, 서비스
🛒 Wanted 사고 싶은 것들
📦 Archived 더 이상 안 쓰는 것들

처음에는 things.json에 모든 아이템 데이터를 넣었습니다. 하지만 아이템마다 상세 설명을 쓰고 싶어지면서 금방 한계에 부딪혔습니다.

Step 2. 마크다운 기반 상세 페이지

JSON에 긴 텍스트를 넣는 건 비효율적이므로, 블로그 포스트와 같은 방식으로 전환했습니다:

content/things/
  mac-mini-m4.md
  iphone-16-pro.md
  khaki-field-murph-auto.md
  ...

각 파일의 frontmatter에 메타데이터를, 본문에 상세 리뷰를 작성합니다:

---
name: Mac mini M4
category: home
order: 1
year: 2024
description: "서브 개발 머신. 컴팩트하지만 강력한 성능."
image: /media/things/mac-mini-m4-hero.webp
thumbnail: /media/things/mac-mini-m4-thumb.webp
price: "₩2,390,000"
purchaseUrl: https://apple.com/kr/mac-mini
---

/things/[slug] 상세 페이지도 추가해서, 카드를 클릭하면 히어로 이미지 + 마크다운 본문 + 목차(TOC)가 있는 페이지로 이동합니다. 블로그에서 이미 만들어둔 MDXContentTableOfContents 컴포넌트를 그대로 재활용했습니다.

카테고리 URL 라우팅도 같은 dynamic route에서 처리합니다. /things/work처럼 접근하면 해당 카테고리만 필터링된 목록을 보여줍니다. generateStaticParams에서 아이템 slug와 카테고리 ID를 모두 반환하는 방식입니다:

export async function generateStaticParams() {
  const itemSlugs = getThingSlugs().map(slug => ({ slug }))
  const categorySlugs = getCategoryIds().map(id => ({ slug: id }))
  return [...itemSlugs, ...categorySlugs]
}

Step 3. 이미지 분리 — Thumbnail과 Hero

목록 카드에는 4:3 비율의 작은 썸네일을, 상세 페이지에는 16:9 비율의 큰 히어로 이미지를 보여주도록 분리했습니다. 파일 네이밍 규칙은 단순합니다:

public/media/things/
  mac-mini-m4-hero.webp    # 상세 페이지 (16:9)
  mac-mini-m4-thumb.webp   # 카드 목록 (4:3)

Step 4. 콘텐츠 확장 — 21개 아이템

실제 사용 중인 21개 아이템의 콘텐츠를 작성했습니다. 각 아이템에 실제 제품 사진을 WebP로 변환해서 넣었습니다. 여기서 문제가 시작됩니다 — 42개 이미지 파일의 총 용량이 약 11MB에 달했습니다.

큰 원인은 원본 해상도가 불필요하게 높았기 때문입니다:

  • magic-keyboard-touch-id-hero.webp1.2MB
  • tanchjim-4u-hero.webp930KB
  • apple-watch-ultra-gen1-hero.webp954KB

목록 페이지를 열면 20개 이상의 썸네일을 동시에 받아야 하니 모바일에서 체감이 확연했습니다.

Step 5. 이미지 최적화

sharp 기반의 자동화 스크립트(scripts/optimize-thing-images.mjs)를 만들어 세 가지를 한 번에 처리합니다:

리사이즈

화면에서 실제로 표시되는 최대 크기 기준으로 축소합니다:

유형 최대 크기 용도
Hero 1024×576 상세 페이지 상단
Thumbnail 560×420 카드 목록
await sharp(filePath)
  .resize(maxWidth, maxHeight, {
    fit: 'inside',
    withoutEnlargement: true
  })
  .webp({ quality: 82 })
  .toFile(tempPath)

fit: 'inside'로 비율을 유지하면서, withoutEnlargement: true로 이미 작은 이미지는 건드리지 않습니다.

Blur Placeholder

이미지 로딩 전에 보여줄 블러 프리뷰를 생성합니다. 20×20px에 quality 20으로 극단적으로 압축한 뒤 base64 인코딩합니다:

const buffer = await sharp(filePath)
  .resize(20, 20, { fit: 'inside' })
  .webp({ quality: 20 })
  .toBuffer()

return `data:image/webp;base64,${buffer.toString('base64')}`

생성된 42개의 blur data URL은 src/generated/image-blur-data.json에 저장됩니다. 빌드 타임에 getBlurDataURL() 함수가 이 JSON을 읽어서 각 아이템에 주입하고, Next.js Image의 placeholder="blur"로 적용합니다:

<Image
  src={item.thumbnail || item.image}
  alt={item.name}
  fill
  placeholder={item.thumbnailBlurDataURL ? 'blur' : 'empty'}
  blurDataURL={item.thumbnailBlurDataURL}
/>

결과

지표 Before After 변화
총 이미지 용량 ~11MB ~672KB -94%
평균 hero 이미지 ~500KB ~20KB -96%
평균 thumbnail ~70KB ~6KB -91%

가장 극적인 사례:

  • magic-keyboard-touch-id-hero.webp: 1,255KB → 17KB (-98.6%)
  • mac-mini-m4-thumb.webp: 115KB → 1.6KB (-98.6%)

전체 구조 정리

최종적으로 Things 섹션은 이런 구조가 되었습니다:

content/
  things.json              # 카테고리 정의
  things/
    mac-mini-m4.md         # 개별 아이템 (frontmatter + 마크다운)
    iphone-16-pro.md
    ...

public/media/things/
  mac-mini-m4-hero.webp    # 최적화된 히어로 이미지
  mac-mini-m4-thumb.webp   # 최적화된 썸네일

src/generated/
  image-blur-data.json     # blur placeholder base64 데이터

src/lib/
  things.ts                # 데이터 로딩 + blur 주입
  image-blur.ts            # blur data URL 조회

src/components/things/
  ThingsContent.tsx        # 카테고리별 그리드 컨테이너
  CategoryNav.tsx          # 카테고리 필터 (Link 기반)
  ThingCard.tsx            # 아이템 카드 (blur placeholder 적용)
  ThingsCategoryPage.tsx   # 카테고리별 필터 페이지

새 아이템을 추가할 때는:

  1. content/things/ 에 마크다운 파일 작성
  2. public/media/things/에 hero/thumb 이미지 추가
  3. npm run optimize-images 실행 (리사이즈 + blur 생성)

돌아보며

  • 블로그에서 만들어둔 마크다운 처리 인프라(MDXContent, TableOfContents, gray-matter)를 거의 그대로 재활용할 수 있었습니다. 기존 시스템을 잘 만들어두면 새 기능 추가가 빨라집니다.
  • 이미지 최적화는 가장 투자 대비 효과가 큰 성능 개선입니다. 코드 한 줄 바꾸지 않고 로딩 속도를 극적으로 줄일 수 있습니다.
  • Blur placeholder는 체감 성능(perceived performance)에 큰 차이를 만듭니다. 이미지가 갑자기 나타나는 대신 부드럽게 전환됩니다.
  • npm run optimize-images 한 줄이면 새 아이템 추가 시 이미지 최적화가 끝나니, 자동화 스크립트를 처음에 투자해서 만들어둘 가치가 있습니다.