개인 포트폴리오 사이트 — Next.js · Sanity 기반 콘텐츠 운영
#frontend#Next.js 16 (App Router)#React 19#TypeScript#Tailwind CSS v4#Sanity#react-markdown#yup
Seoyoung Kim · 설계 · 개발 · 운영 단독 @ 개인 프로젝트
2026-01-29 ~ 진행 중
개인 포트폴리오 사이트 — 블로그에서 포트폴리오로, 직접 운영하는 콘텐츠 구조
지금 보고 있는 이 사이트다. 진행했거나 진행 중인 프로젝트를 한곳에 정리하려고 만들었다. 정적 데이터 기반 블로그로 시작해, Sanity를 콘텐츠 저장소로 붙이고 사이트 안에 관리자 페이지를 만든 뒤, 블로그 구조를 포트폴리오 구조로 전환했다.
- 스택 — Next.js 16 (App Router) · React 19 · TypeScript(strict) · Tailwind CSS v4 · Sanity
- 범위 — 공개 페이지, 관리자 페이지, Route Handler 7개(핸들러 9개), SEO 설정
- 배포 — Vercel + GitHub 연동
진행 과정
| 시기 | 내용 |
|---|---|
| 1월 | 정적 데이터 기반 블로그 초기 구현 |
| 2월 | 문의 폼 (yup 검증 후 메일 발송) |
| 4월 | 리팩터링 |
| 5월 | SEO, favicon |
| 7~8월 | Sanity 연동, 관리자 페이지, CRUD API |
| 9월 | ISR 적용, 에디터 개편, 블로그 → 포트폴리오 구조 전환 |
| 10월 | feature 단위 구조 재편, 세션 방식 변경 |
핵심 구현 포인트
1. 렌더링 — SSR에서 ISR로
처음에는 SSR(no-store)이었다. 방문자가 올 때마다 Sanity를 호출하고 HTML을 새로 만드는 구조인데, 이 사이트는 글이 몇 개 안 되고 가끔만 바뀐다. 바뀌지 않는 내용을 매 요청마다 다시 만들 이유가 없어서 ISR이 더 맞다고 판단했다.
홈과 프로젝트 상세를 ISR(60초)로 바꾸고, 관리자 페이지에서 저장하면 쓰기 핸들러가 revalidatePath("/", "layout")로 캐시를 무효화하도록 했다. 평소에는 만들어 둔 HTML을 그대로 내려주고, 글을 고쳤을 때만 다시 만든다.
- 프로젝트 상세는
generateStaticParams로 빌드 시 생성하고, 목록에 없는 slug는 요청 시 생성 - 캐시 정책은 라우트 세그먼트 설정 대신 Sanity
fetch래퍼 한 곳에서만 제어 (기본 60초) - 반영 경로는 두 가지 — 관리자 저장은 즉시, Sanity Studio에서 직접 수정한 내용은 60초 주기로 반영
2. Sanity 연동 — SDK 없이 HTTP API 직접 호출
@sanity/client 없이 fetch로 조회 · mutation · 이미지 업로드 함수 3개만 만들어 쓴다.
- 쓰기 토큰은 서버 모듈 한 곳에서만 읽고 mutation · 업로드에만 붙인다. 공개 조회는 토큰 없이 수행
3. 관리자 페이지
별도 CMS 화면으로 이동하지 않고 사이트 안에서 글을 쓰고 고칠 수 있게 했다.
- 프로젝트 작성 · 수정 · 삭제. 로그인 상태에서는 공개 페이지의 카드와 상세에도 수정/삭제 버튼이 보인다
- 정렬 순서는 숫자 입력 후 일괄 저장
- 썸네일 업로드, 본문 이미지 업로드 후 마크다운 코드 복사
- Write/Preview 탭 마크다운 에디터
- 미저장 변경 감지와 이탈 경고
4. 인증과 권한
- 로그인 쿠키 값을 비밀번호로 서명한 값(HMAC-SHA256)으로 바꿨다. 초기에는 고정 문자열이라 쿠키를 직접 만들면 관리자로 인식됐는데, 지금은 비밀번호를 모르면 유효한 쿠키를 만들 수 없다
- 쿠키는 자바스크립트에서 읽을 수 없고(httpOnly) HTTPS로만 전송된다
- 글 작성 · 수정 · 삭제 API는 서버에서 로그인 여부를 검사한다. 화면에서 버튼을 숨기는 것은 편의용이고, 실제 차단은 API가 한다
5. 서버/클라이언트 분리, 마크업
- 데이터 조회는 서버 컴포넌트에서 하고 props로 내린다. 상태 · 이벤트 · 브라우저 API가 필요한 파일만
"use client" - Tailwind 기본 브레이크포인트를 모바일 우선으로 사용, 헤더는 좁은 화면에서 햄버거 메뉴로 전환
header / nav / main / section / article시맨틱 마크업, 페이지당 h1 하나- 다크 모드는
.dark클래스 기반.<head>인라인 스크립트가 첫 페인트 전에 저장된 설정과 시스템 설정을 읽어 깜빡임을 막는다
6. SEO
- 루트 metadata(title 템플릿 · Open Graph · Twitter 카드 · robots), 상세 페이지
generateMetadata sitemap.ts,robots.ts(관리자 · API 경로 disallow)
다음 단계
- 테스트 코드 추가 (현재는 TypeScript strict 타입 검사에 의존)
- Studio에서 직접 수정한 내용도 즉시 반영되도록 webhook 연결
- 본문 이미지에
next/image적용