이력서 PDF를 서버에서 만들지 않고 @react-pdf/renderer로 브라우저에서 ko/en/jp 3개 언어를 즉시 생성해 내려받게 했다
셰이더 히어로와 3개 국어 이력서 생성을 갖춘 원페이지 포트폴리오입니다.
과제
- 만들 수 있는 것을 문장 대신 화면 자체로 보여줘야 함
- 국문·영문·일문 이력서를 요청 즉시 건넬 수 있어야 함
담당 범위
- 원페이지 구성: Next.js 16 App Router (1인)
- GLSL 셰이더 히어로(@react-three/fiber), rAF 궤도·3D 캐러셀
- 브라우저 이력서 PDF 생성, API 기반 MDX 렌더링
설계 판단
- 이력서 PDF를 브라우저에서 생성 → 화면과 같은 데이터 소스라 내용 불일치 없음
- 히어로를 GLSL 셰이더로, 궤도·캐러셀을 rAF 루프로 구현 → 해상도 독립, 매 프레임 계산
- 작업물 본문도 API + MDX로 받고 웹훅으로 갱신 → 블로그와 렌더러·캐시 규칙 통일
결과 지표
- 이력서 3개 언어(ko / en / jp), 브라우저에서 즉시 생성
- TypeScript/TSX 59개 파일, 3,665줄
- 포트폴리오 앱에 닿은 커밋 30건
원문 자세히 보기
과제
프리랜서로 직접 수주하는 입장에서 포트폴리오는 '무엇을 만들 수 있는지'를 문장이 아니라 화면 자체로 보여줘야 했고, 동시에 상대가 요구하는 형식(국문·영문·일문 이력서 PDF)으로도 즉시 건네줄 수 있어야 했다. 인터랙션을 넣되 그 비용이 초기 로딩과 저사양 기기에서 감당 가능한 선에 머무르게 하는 것이 과제였다.
담당 범위
원페이지 · 소스 3,665줄 · 3개 국어 이력서 생성1인 개발. (1) Next.js 16 App Router 기반 단일 페이지 구성, (2) @react-three/fiber + drei로 GLSL 셰이더 히어로 구현, (3) requestAnimationFrame 기반 스킬 궤도·3D 캐러셀 인터랙션, (4) @react-pdf/renderer로 ko/en/jp 이력서를 브라우저에서 즉시 생성·내려받게 구현, (5) Framer Motion 기반 스크롤 인터랙션, (6) 작업물 콘텐츠를 백엔드 API에서 받아 MDX(next-mdx-remote + Shiki)로 렌더링, (7) /api/revalidate Route Handler로 어드민 저장 시 캐시 태그·경로 무효화.
선택안 · 대안 · 근거 · 비용
히어로 배경을 이미지나 동영상이 아니라 Three.js GLSL 셰이더로 그리고, 스킬 궤도·캐러셀 애니메이션은 CSS 트랜지션이 아니라 requestAnimationFrame 루프로 직접 돌렸다
작업물 본문도 블로그와 같은 방식으로 API에서 받아 MDX를 런타임 렌더링하고, 갱신은 어드민 저장 웹훅 → /api/revalidate(태그·경로 선택 무효화)로 통일했다
packages/mdx와 packages/shared를 공유하는 구조라, 소스만 통일하면 두 앱이 같은 규칙 위에서 돈다.비용변경이 드문 콘텐츠까지 백엔드 가용성에 묶였다. 블로그와 마찬가지로 apiFetch 실패가 예외가 아니라 빈 화면으로 degrade되므로, 포트폴리오가 비어 보이는 상태와 백엔드 장애가 화면상 구별되지 않는다.모두 재현 명령과 함께 표기
git ls-files 'apps/portfolio/' | grep -E '\.tsx?$' | wc -l → 59 / 같은 목록에 xargs wc -l | tail -1 → 3665
git log --oneline -- apps/portfolio | wc -l → 30
git ls-files | grep -E 'apps/portfolio/.*page\.tsx' → apps/portfolio/src/app/page.tsx
apps/portfolio/package.json의 @react-pdf/renderer 의존성 및 이력서 생성 컴포넌트
git ls-files | grep -E 'apps/portfolio/.*\.(test|spec)\.[tj]sx?$' → 출력 없음
npx lighthouse@12 <포트폴리오 도메인> --only-categories=performance,accessibility,best-practices,seo --chrome-flags='--headless=new'