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)가 있는 페이지로 이동합니다. 블로그에서 이미 만들어둔 MDXContent와 TableOfContents 컴포넌트를 그대로 재활용했습니다.
카테고리 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.webp— 1.2MBtanchjim-4u-hero.webp— 930KBapple-watch-ultra-gen1-hero.webp— 954KB
목록 페이지를 열면 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 # 카테고리별 필터 페이지
새 아이템을 추가할 때는:
content/things/에 마크다운 파일 작성public/media/things/에 hero/thumb 이미지 추가npm run optimize-images실행 (리사이즈 + blur 생성)
돌아보며
- 블로그에서 만들어둔 마크다운 처리 인프라(
MDXContent,TableOfContents,gray-matter)를 거의 그대로 재활용할 수 있었습니다. 기존 시스템을 잘 만들어두면 새 기능 추가가 빨라집니다. - 이미지 최적화는 가장 투자 대비 효과가 큰 성능 개선입니다. 코드 한 줄 바꾸지 않고 로딩 속도를 극적으로 줄일 수 있습니다.
- Blur placeholder는 체감 성능(perceived performance)에 큰 차이를 만듭니다. 이미지가 갑자기 나타나는 대신 부드럽게 전환됩니다.
npm run optimize-images한 줄이면 새 아이템 추가 시 이미지 최적화가 끝나니, 자동화 스크립트를 처음에 투자해서 만들어둘 가치가 있습니다.