통합 포트폴리오 사이트

개발자로서의 성장 과정과 프로젝트 경험, 기술 스택, 타임라인 등을 포함하는 포트폴리오 사이트입니다. 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, 구조화된 데이터)

🛠 기술 스택

분류기술
FrameworkNext.js 15 (App Router)
LanguageTypeScript
StylingTailwind CSS
UI LibraryReact 19
ContentMarkdown (remark-gfm)
RenderingReact Markdown
EmailResend API
DeploymentVercel
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 Routersrc/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 Score90+
First Contentful Paint<1.5s
Largest Contentful Paint<2.5s
Cumulative Layout Shift<0.1

🔗 참고 자료


마지막 업데이트: 2025년 12월