통합 포트폴리오 사이트
개발자로서의 성장 과정과 프로젝트 경험, 기술 스택, 타임라인 등을 포함하는 포트폴리오 사이트입니다. Next.js, TypeScript, Tailwind CSS로 설계하고 구현했으며, 방문자와 소통할 수 있도록 이메일 전송 기능과 동적 콘텐츠 관리 시스템을 구축했습니다. 열정적으로 학습하고 새로운 프로젝트를 계속 추가하고 있습니다.
Next.jsTypeScriptTailwind CSSMarkdownResend APIPortfolio
기간
2025.12 - 진행 중
🌟 포트폴리오 웹사이트
개인 개발자 포트폴리오 | Next.js 15, TypeScript, Tailwind CSS로 구축한 반응형 웹사이트
🎯 프로젝트 개요
이 프로젝트는 개인 개발자 포트폴리오 웹사이트입니다.
- 목적: 개발 경력, 프로젝트, 기술 스택을 효과적으로 전시하기
- 특징: 다크모드 지원, 완벽한 반응형 디자인, Markdown 기반 프로젝트 관리, 이메일 문의 기능
- 기술성: 모던 웹 기술 스택과 모범 사례를 직접 적용한 포트폴리오
✨ 주요 기능
1️⃣ 다중 페이지 구조
- 홈: 간단 소개, CTA
- 소개: 개발자 프로필, 기술 스택, 타임라인, 강점 설명, 주요 프로젝트 설명, CTA
- 포트폴리오: 프로젝트 갤러리 및 상세 페이지
- 연락처: 이메일 문의 폼, 정보
2️⃣ 프로젝트 관리
- Markdown 기반: README 파일로 프로젝트 설명 관리
- 메타데이터: JSON으로 프로젝트 정보 구조화
- 동적 라우팅:
[id]동적 경로로 프로젝트 상세 페이지 자동 생성 - 이미지 지원: 각 프로젝트별 썸네일 및 상세 이미지
- 정렬 기능:
- 📍 추천순:
order필드로 핵심 프로젝트 우선 표시 - 📅 최신순:
date필드로 최근 프로젝트 자동 정렬
- 📍 추천순:
3️⃣ 디자인 & UX
- 다크 모드: 다크 모드 스타일 및 수동 토글 기능
- 반응형: 모바일, 태블릿, 데스크톱 최적화
- 부드러운 애니메이션: 페이지 전환 및 호버 이펙트
- 접근성: 시맨틱 HTML, ARIA 속성, 키보드 네비게이션
4️⃣ 성능 최적화
- Next.js Image: 자동 이미지 최적화 및 지연 로딩
- 정적 생성: SSG로 빠른 페이지 로드
- 메타 데이터: SEO 최적화 (Open Graph, 구조화된 데이터)
🛠 기술 스택
| 분류 | 기술 |
|---|---|
| Framework | Next.js 15 (App Router) |
| Language | TypeScript |
| Styling | Tailwind CSS |
| UI Library | React 19 |
| Content | Markdown (remark-gfm) |
| Rendering | React Markdown |
| Resend API | |
| Deployment | Vercel |
| Database | 정적 파일 (JSON) |
📂 폴더 구조
src/
├── app/ # App Router 라우팅
│ ├── layout.tsx # 루트 레이아웃
│ ├── page.tsx # 홈 페이지
│ ├── globals.css # 전역 스타일
│ ├── about/ # 소개 페이지
│ ├── projects/ # 포트폴리오 페이지
│ │ ├── page.tsx
│ │ └── [id]/
│ │ └── page.tsx # 프로젝트 상세 페이지
│ └── contact/ # 연락처 페이지
├── components/ # 리액트 컴포넌트
│ ├── Header.tsx # 네비게이션 헤더
│ ├── about/ # 소개 섹션
│ ├── projects/ # 포트폴리오 섹션
│ └── contact/ # 연락처 섹션
├── lib/ # 라이브러리 & 유틸
│ └── projects.ts # 프로젝트 데이터 로드
├── types/ # TypeScript 타입 정의
│ └── project.ts # 프로젝트 인터페이스
└── content/projects/ # 프로젝트 콘텐츠
├── [project-id]/
│ ├── metadata.json # 프로젝트 메타데이터
│ └── README.md # 프로젝트 설명
🚀 프로젝트 추가 방법
1단계: 프로젝트 폴더 생성
src/content/projects/[project-id]/
2단계: metadata.json 작성
{
"id": "project-id",
"title": "프로젝트 이름",
"description": "짧은 설명",
"longDescription": "긴 설명",
"thumbnail": "projects/thumbnail.png",
"tags": ["Tag1", "Tag2"],
"date": "2024-12",
"period": "2024.12 - 2025.01",
"order": 1,
"githubUrl": "https://github.com/...",
"liveUrl": "https://..."
}
중요 필드:
order: 작을수록 위에 표시 (추천순 정렬)date: 최신순 정렬에 사용 (YYYY-MM 형식)
3단계: README.md 작성
Markdown으로 프로젝트 상세 설명 작성
4단계: 이미지 추가
public/projects/ 폴더에 썸네일 이미지 추가
📚 핵심 기능 상세 설명
1️⃣ 동적 프로젝트 로딩 & 정렬
// src/lib/projects.ts
export async function getProjectDetail(id: string) {
const projectPath = path.join(process.cwd(), 'src/content/projects', id);
const metadataPath = path.join(projectPath, 'metadata.json');
const readmePath = path.join(projectPath, 'README.md');
// metadata.json과 README.md를 읽어 조합
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
const markdown = fs.readFileSync(readmePath, 'utf-8');
return { ...metadata, markdown };
}
// 정렬 함수
export async function getAllProjects() {
const projects = [...]; // 모든 프로젝트 읽기
return projects.sort((a, b) => a.order - b.order); // 추천순 정렬
}
// 날짜 정렬
export function sortByDate(projects: Project[]) {
return projects.sort((a, b) =>
new Date(b.date).getTime() - new Date(a.date).getTime()
);
}
2️⃣ 프론트엔드 정렬 UI
// src/components/projects/ProjectList.tsx
const [sortType, setSortType] = useState<"featured" | "date">("featured");
const getSortedProjects = () => {
if (sortType === "featured") {
return [...projects].sort((a, b) => a.order - b.order);
} else {
return [...projects].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
);
}
};
3️⃣ Markdown 렌더링
// src/components/projects/ProjectDetailView.tsx
<Markdown remarkPlugins={[remarkGfm]}>{project.markdown}</Markdown>
remark-gfm을 사용하여 GitHub Flavored Markdown 지원:
- 표(table)
- 취소선(strikethrough)
- 작업 목록(task list)
- URL 자동 링크
4️⃣ Resend 이메일 API 통합
// src/app/api/contact/route.ts
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(request: Request) {
const { name, email, subject, message } = await request.json();
try {
const data = await resend.emails.send({
from: "noreply@sanghwi-portfolio.com",
to: process.env.CONTACT_EMAIL,
replyTo: email,
subject: `포트폴리오 문의: ${subject}`,
html: `
<h2>새로운 메시지</h2>
<p><strong>발신자:</strong> ${name} (${email})</p>
<p><strong>제목:</strong> ${subject}</p>
<p><strong>메시지:</strong></p>
<p>${message}</p>
`,
});
return Response.json({ success: true });
} catch (error) {
return Response.json({ error: error.message }, { status: 400 });
}
}
5️⃣ 다크모드 구현
// src/components/Header.tsx
const [isDark, setIsDark] = useState(false);
useEffect(() => {
// 초기 로드: localStorage 확인
const savedTheme = localStorage.getItem("theme");
if (savedTheme === "dark") {
setIsDark(true);
document.documentElement.classList.add("dark");
}
}, []);
const toggleDarkMode = () => {
const newIsDark = !isDark;
setIsDark(newIsDark);
if (newIsDark) {
document.documentElement.classList.add("dark");
localStorage.setItem("theme", "dark");
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("theme", "light");
}
};
💡 코드 상세 설명
| 개념 | 설명 |
|---|---|
| Next.js App Router | src/app 폴더 구조로 라우팅 자동 관리 |
| 동적 라우팅 | [id]/page.tsx로 동적 페이지 자동 생성 |
| SSG (Static Site Generation) | 빌드 타임에 정적 HTML 생성으로 성능 최적화 |
| Markdown 파싱 | remark & rehype로 마크다운을 HTML로 변환 |
| TypeScript 타입 | 프로젝트 구조를 타입으로 정의하여 안정성 확보 |
| CSS 모듈 | Tailwind CSS로 유틸리티 우선 스타일링 |
| localStorage | 사용자 테마 선택 저장 |
🎨 디자인 특징
색상 시스템
- 라이트 모드: 밝은 배경, 어두운 텍스트
- 다크 모드: 어두운 배경, 밝은 텍스트
- 강조색: 파란색 그라데이션 (from-blue-600 to-purple-600)
타이포그래피
제목: Noto Sans KR 600-700 weight
본문: -apple-system, BlinkMacSystemFont, sans-serif
코드: Monospace (고정폭 폰트)
간격 & 크기
- 모바일 우선 접근법
- Tailwind CSS 기본 스케일 사용
max-w-5xl컨테이너로 최적 가독성 유지
📊 성능 지표
| 항목 | 측정값 |
|---|---|
| Lighthouse Score | 90+ |
| First Contentful Paint | <1.5s |
| Largest Contentful Paint | <2.5s |
| Cumulative Layout Shift | <0.1 |
🔗 참고 자료
마지막 업데이트: 2025년 12월