Overview
템플릿에 의존하지 않고 원하는 데이터 구조와 표현 방식을 직접 설계. 이 포트폴리오 사이트 자체가 사용한 기술 스택의 동작 증명임.
Portable Text 모듈화
수식·코드·표·이미지 등 6종 커스텀 컴포넌트로 어떤 포맷도 하나의 구조에서 표현 가능
Headless CMS 구조
Sanity Embedded Studio — 코드 수정 없이, SaaS를 통해 실시간 콘텐츠 관리·수정
Next.js SSR
App Router로 로딩 속도 향상, 이미지 Lazy Loading
GROQ 쿼리 설계
페이지 단위 필요 데이터만 선택적 호출 → 이미지 URL·메타데이터 쿼리 단계에서 주입
디자인 시스템
폰트 스케일, 커스텀 컬러 토큰, 브레이크포인트 직접 설계
Problem
템플릿의 한계: 원하는 구조를 표현할 수 없다
| 비교항목 | 템플릿 기반 | 직접 개발 |
|---|---|---|
| 데이터 구조 | 고정 필드 | 직접 설계 |
| 콘텐츠 포맷 | 제한적 | 수식/코드/표 자유로움 |
| 디자인 자유도 | 낮음 | 높음(100%) |
| 유지보수 | 서비스에 종속 | 직접 관리 |
포트폴리오를 노션·벨로그·템플릿 기반 서비스로 만드는 것은 처음부터 배제했음. 프로젝트마다 강조 포인트가 다르고, 수식·코드 블록 같은 학습 자료도 표현해야 했기 때문.
- 고정 필드 구조가 표현의 다양성을 제한함
- 필드 기반 CMS → 프로젝트마다 다른 포맷을 하나의 틀에 강제
- 직접 만들기로 결정 → 데이터 구조·렌더링 방식 모두 직접 설계 필요
일정 없는 개인 프로젝트 = 완성되지 않는 제품
개인 프로젝트라고 기획 없이 개발부터 시작하면, 우선순위가 흔들리고 작업 범위가 계속 늘어나기 쉬움을 경험을 통해 알고있음.
- 완료 기준과 일정 구조 부재
Solution
콘텐츠 구조 → Portable Text로 모듈화
프로젝트마다 포맷이 달라도 하나의 시스템으로 표현하는 구조가 필요.
- Sanity Portable Text 도입 → 콘텐츠를 JSON 배열로 저장, 컴포넌트 단위 조합
- 커스텀 컴포넌트: PortableHeader / PortableCodebox / PortableMath(KaTeX) / PortableTable / PortableImage 등 직접 구현
렌더링 구조 → Next.js SSR + Headless CMS
콘텐츠와 코드 분리, 초기 로딩 속도, GROQ를 동시에 해결.
- SPA → Next.js SSR
- Firebase → Sanity CMS: 코드 수정 없이 실시간 콘텐츠 관리
- GROQ 쿼리: 페이지 단위로 필요 데이터만 선택적 호출
일정 수립
기획 단계에서 요구사항 정의서·IA 구조도·일정표를 작성했다.
- 기능/비기능 요구사항 정의 → MVP 범위 확정
- 단계별 마일스톤 설정


Implementation
Sanity CMS + GROQ 쿼리 구조
- coalesce 활용 → 빈 필드 안전 처리
- 이미지 URL·메타데이터 GROQ 단계에서 직접 주입 → 프론트 enrichment 최소화
디자인 시스템
- 타이포그래피: Pretendard 폰트, clamp() 기반 9단계 반응형 폰트 스케일
- 컬러 토큰: primary·neutral·emerald·lemon 등 커스텀 팔레트 정의
- 브레이크포인트: xs(480px) / md(890px) / xxl(1550px) 커스텀 설정
- 로고: 'ㅅㅈ' 초성 기반 이모티콘형 로고 직접 제작
Portable Text 커스텀 컴포넌트
- PortableHeader: h1 렌더링
- PortableSubheader: h2 렌더링
- PortableCodebox: 코드 블록, dynamic import (SSR 비활성화)
- PortableMath: 수식 (KaTeX)
- PortableTable: 표 (행·열·혼합 헤더 3종)
- PortableImage: 이미지
Problem Solving
코드 블록 SSR hydration 오류
- 문제: PortableCodebox 렌더 시 서버·클라이언트 마크업 불일치로 hydration 경고 반복
- 원인: next-sanity의 PortableText는 서버에서도 실행되는데, 코드 하이라이터가 브라우저 전용 API에 의존
- 해결: dynamic(() => import('./portable-codebox-inner'), { ssr: false }) → 클라이언트 전용 로드, 로딩 중 <pre> 플레이스홀더 표시
- 결과: hydration 오류 제거, SSR 성능 유지
- 인사이트: CMS 렌더러 안에서 브라우저 의존 컴포넌트는 반드시 ssr: false 처리해야 한다
Sanity Embedded Studio 라우트 충돌
- 문제: /studio 경로에 Sanity Studio를 임베드했으나, Next.js App Router의 [[...tool]] catch-all과 충돌해 특정 Studio 페이지에서 404 발생
- 원인: App Router의 동적 라우트 우선순위와 Sanity Studio 내부 라우팅이 동일 경로를 두고 경합
- 해결: client-studio.tsx에 'use client' 분리, <NextStudio> 컴포넌트를 클라이언트 전용으로 격리
- 결과: Studio 전 페이지 정상 접근, 콘텐츠 실시간 수정 가능
- 인사이트: Next.js에 외부 SPA를 임베드할 때는 App Router 라우트 해석 순서를 먼저 검토해야 한다
Insight
확장 가능한 구조
- 프로젝트마다 다른 포맷을 Portable Text 하나로 수용 → 새 포맷 추가 시 컴포넌트만 등록하면 됨
- 데이터 스키마 설계가 먼저 → 화면은 그 결과
개인 프로젝트도 기획 문서가 있어야 된다
- 요구사항 정의서·IA·일정표 작성
- 이후 꾸준한 백업 과정에서, 문서를 읽고 코드 리딩 시간 단축함
이 포트폴리오 자체가 기술 스택의 증명
- Next.js·TypeScript·Sanity·Tailwind CSS로 만든 웹사이트
- 포트폴리오의 동작 자체가 그동안의 기술 역량 레퍼런스
Result
Outcomes
요구사항·IA·일정표 사전 작성으로 일정 이탈 없이 완료
수식·코드·표 등 6종 Portable Text 커스텀 컴포넌트 직접 구현 → 프로젝트마다 다른 포맷을 단일 구조로 표현
Sanity Embedded Studio 도입 → 코드 변경·재배포 없이 실시간 콘텐츠 수정 가능
SSR hydration 오류 제거 → 코드 블록 ssr:false 격리 처리
관련 프로젝트
포트폴리오 웹사이트 기획부터 배포까지 · Portfolio · Sojin Lee


