Next.js로 포트폴리오 사이트 만들기
포트폴리오 사이트를 만들어야겠다고 생각한 건 채용 공고를 보다가였습니다. "GitHub 링크 또는 포트폴리오 URL"이라는 문구가 반복해서 나왔습니다. GitHub 프로필만으로는 부족하다는 걸 느꼈습니다.
Notion이나 Wordpress로 빠르게 만들 수도 있었지만, 직접 만들기로 했습니다. 개발자 포트폴리오는 그 자체가 프로젝트이기 때문입니다.
기술 스택을 고른 기준
"최신 기술을 써보고 싶다"가 아니라 "이 조합이 이 프로젝트에 가장 맞다"를 기준으로 골랐습니다.
| 기술 | 선택 이유 |
|---|---|
| Next.js 14 | App Router 기반 SSG, 이미지 최적화, 배포가 간단 |
| Tailwind CSS | 별도 CSS 파일 없이 빠른 스타일링 |
| React Three Fiber | 히어로 섹션 3D 인터랙션으로 첫인상 차별화 |
| TypeScript | 컴포넌트 Props 타입 체크, IDE 자동완성 |
| Vercel | git push만으로 자동 배포, 무료 호스팅 |
처음에는 Gatsby도 후보였습니다. 하지만 Next.js 14의 App Router가 파일 기반 라우팅을 더 직관적으로 지원하고, 커뮤니티 지원도 압도적이어서 Next.js를 선택했습니다.
프로젝트 구조 — 관심사 분리가 핵심
src/
├── app/ # 페이지 (라우팅 = 폴더 구조)
│ ├── page.tsx # 홈 (히어로 + 섹션들)
│ ├── blog/ # 블로그 목록 + 상세
│ └── portfolio/ # 포트폴리오
├── components/ # UI 컴포넌트
├── content/posts/ # 마크다운 블로그 글
└── lib/ # 유틸리티, 커스텀 훅
components/에는 UI 렌더링만 담고, 데이터 처리 로직은 전부 lib/으로 분리했습니다. 예를 들어 블로그 글을 파싱하는 함수는 lib/posts.ts에 있고, 그 데이터를 표시하는 건 components/BlogCard.tsx가 합니다.
이렇게 하면 나중에 CMS를 붙이거나 데이터 소스를 바꿔도 컴포넌트는 수정할 필요가 없습니다.
마크다운 기반 블로그 — CMS 없이 가볍게
별도의 CMS 서비스를 쓰면 편하지만, 무료 포트폴리오에 유료 CMS를 붙이는 건 과했습니다. 마크다운 파일을 src/content/posts/에 저장하고, gray-matter로 frontmatter를 파싱하는 방식을 택했습니다.
import matter from "gray-matter";
import fs from "fs";
import path from "path";
const postsDir = path.join(process.cwd(), "src/content/posts");
export function getPostBySlug(slug: string) {
const filePath = path.join(postsDir, `${slug}.md`);
const source = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(source);
return { meta: data as PostMeta, content };
}
빌드 시점에 모든 마크다운을 HTML로 변환하므로 런타임 비용이 없습니다. 글을 추가할 때는 .md 파일을 만들고 git push하면 끝입니다. 데이터베이스도, 관리자 페이지도 필요 없습니다.
단점은 글 작성 시 미리보기가 없다는 것입니다. VS Code의 마크다운 프리뷰로 대체하고 있는데, 충분합니다.
CSS 변수 기반 테마 시스템
색상을 하드코딩하면 나중에 테마를 바꿀 때 모든 파일을 수정해야 합니다. CSS 변수로 한 곳에서 관리하면 이 문제가 해결됩니다.
:root {
--bg-primary: #ffffff;
--text-primary: #333333;
--accent: #3B82F6;
--glass-bg: rgba(245, 247, 250, 0.85);
}
Tailwind에서 이 변수들을 참조할 수 있도록 tailwind.config.ts에 등록합니다.
colors: {
background: "var(--bg-primary)",
charcoal: "var(--text-primary)",
accent: "var(--accent)",
}
이제 className="bg-accent text-charcoal"처럼 쓰면 됩니다. 나중에 다크 모드를 추가하고 싶으면 :root 변수만 오버라이드하면 전체 사이트에 적용됩니다.
스크롤 애니메이션 — IntersectionObserver 활용
섹션이 화면에 들어올 때 페이드인 효과를 주면 사이트가 훨씬 살아 보입니다. 직접 만든 useScrollReveal 커스텀 훅이 이 역할을 합니다.
export function useScrollReveal<T extends HTMLElement>() {
const ref = useRef<T>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
observer.disconnect();
}
},
{ threshold: 0.15 }
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return ref;
}
CSS에서 .animate-on-scroll은 opacity: 0; transform: translateY(24px);으로 시작하고, .is-visible이 추가되면 원래 위치로 돌아옵니다. 라이브러리 없이 네이티브 API만으로 구현했기 때문에 번들 사이즈에 영향이 없습니다.
배포 — Vercel이 가장 간단합니다
Next.js 프로젝트는 Vercel에 배포하는 게 가장 편합니다. GitHub 레포를 연결하면 main 브랜치에 push할 때마다 자동으로 빌드하고 배포합니다.
설정할 것도 거의 없습니다. Framework Preset을 Next.js로 선택하면 빌드 명령어와 출력 디렉토리를 알아서 잡아줍니다.
무료 플랜으로도 커스텀 도메인, HTTPS, CDN이 전부 지원됩니다. 개인 포트폴리오에는 충분하고도 남습니다.
만들면서 깨달은 것들
처음부터 완벽하게 만들려고 하면 시작을 못 합니다. 최소한의 기능(홈, 블로그, 포트폴리오)부터 배포한 다음에 하나씩 추가했습니다. 빈 포트폴리오 페이지를 부끄러워하기보다 일단 올려놓고 채워가는 게 더 효과적이었습니다.
3D는 양날의 검입니다. 눈에 띄는 건 확실하지만, 모바일 성능과 로딩 시간을 잡는 데 예상보다 시간이 많이 걸렸습니다. Dynamic Import와 DPR 제한은 필수입니다.
디자인 시스템을 먼저 잡는 게 빠릅니다. CSS 변수와 Tailwind 토큰을 처음에 정의해두면, 나중에 새 컴포넌트를 만들 때 색상이나 간격을 고민할 필요가 없습니다.
포트폴리오는 완성되는 게 아니라 계속 업데이트되는 프로젝트입니다. 지금 이 글도 그 과정의 일부입니다.