프로젝트 섹션 제작기

Alchemist·2025년 8월 19일

포트폴리오에서 가장 눈에 띄는 부분 중 하나는 프로젝트 섹션입니다.
단순히 “프로젝트 나열”이 아니라,

  • 한눈에 요약 정보를 제공하고
  • 성과 지표와 기술 스택을 강조하며
  • 심화 설명(Case Study)을 담을 수 있는 구조
    로 만들고자 했습니다.

1. 데이터 기반 구조

프로젝트 정보는 따로 data/projects.ts 파일에 배열로 관리합니다.
이렇게 하면 프로젝트를 추가/수정할 때 UI를 건드릴 필요 없이 데이터만 수정하면 자동 반영됩니다.

// data/projects.ts
import { Project } from "@/types/project";

export const projects: Project[] = [
  {
    title: "검색 성능 개선 실험",
    description: "Next.js SSR 캐싱과 이미지 최적화를 통해 초기 로딩 속도를 줄인 프로젝트",
    tags: ["Next.js", "TypeScript", "Tailwind"],
    github: "https://github.com/username/search-perf",
    demo: "https://search-perf.vercel.app",
    cover: "/thumbs/search.webp",
    highlights: ["LCP -28%", "A11y 97점", "코드 스플리팅 적용"],
    metrics: [
      { label: "LCP", value: "-28%" },
      { label: "A11y", value: "97" },
      { label: "Build", value: "-15%" },
    ],
    caseStudy: {
      problem: "초기 렌더링 지연으로 사용자 이탈률이 증가",
      approach: [
        "Next/Image와 WebP, AVIF 적용",
        "코드 스플리팅과 dynamic import",
        "SSR 캐싱과 불필요 렌더 제거",
      ],
      result: ["LCP -28%", "접근성 점수 97점 달성"],
      roleStack: "개발 100% · Next.js, React, TypeScript, Tailwind",
    },
  },
];

2. 카드 UI – ProjectCard 컴포넌트

카드 단위로 나누어 정보를 보여주되, 필요하다면 <details>를 이용해
Case Study를 열어볼 수 있도록 했습니다.

// components/ProjectCard.tsx
import Image from "next/image";
import { Project } from "@/types/project";

export default function ProjectCard({
  title, description, tags, github, demo,
  cover, highlights = [], metrics = [], caseStudy,
}: Project) {
  return (
    <article className="rounded-2xl border p-6 shadow-sm transition hover:-translate-y-1">
      {cover && (
        <div className="relative mb-4 h-44 w-full overflow-hidden rounded-xl">
          <Image src={cover} alt={`${title} thumbnail`} fill className="object-cover" />
        </div>
      )}

      <h3 className="text-lg font-semibold">{title}</h3>
      <p className="mt-2 mb-4 text-sm">{description}</p>

      {/* 태그 */}
      <ul className="mb-4 flex flex-wrap gap-2 text-xs">
        {tags.map((tag) => (
          <li key={tag} className="rounded-full bg-zinc-100 px-2 py-1">#{tag}</li>
        ))}
      </ul>

      {/* 성과 지표 */}
      {!!metrics.length && (
        <div className="mb-4 grid grid-cols-3 gap-2 text-center">
          {metrics.map((m) => (
            <div key={m.label} className="rounded-lg border p-2">
              <p className="text-sm font-bold">{m.value}</p>
              <p className="text-[10px] text-zinc-500">{m.label}</p>
            </div>
          ))}
        </div>
      )}

      {/* 링크 */}
      <div className="flex gap-3">
        <a href={github} target="_blank" className="text-blue-500 underline">GitHub</a>
        {demo && <a href={demo} target="_blank" className="text-blue-500 underline">Demo</a>}
      </div>

      {/* Case Study */}
      {caseStudy && (
        <details className="mt-4">
          <summary className="cursor-pointer text-sm font-medium">Case Study</summary>
          <div className="mt-2 space-y-2 text-sm">
            <section>
              <h4 className="font-semibold">문제 정의</h4>
              <p>{caseStudy.problem}</p>
            </section>
            <section>
              <h4 className="font-semibold">접근</h4>
              <ul className="list-disc pl-5">
                {caseStudy.approach?.map((a) => <li key={a}>{a}</li>)}
              </ul>
            </section>
            <section>
              <h4 className="font-semibold">결과</h4>
              <ul className="list-disc pl-5">
                {caseStudy.result?.map((r) => <li key={r}>{r}</li>)}
              </ul>
            </section>
            {caseStudy.roleStack && (
              <section>
                <h4 className="font-semibold">역할 & 스택</h4>
                <p>{caseStudy.roleStack}</p>
              </section>
            )}
          </div>
        </details>
      )}
    </article>
  );
}

👉 성과 지표Case Study까지 담을 수 있어,
단순한 프로젝트 나열이 아니라 문제 해결 과정을 보여줄 수 있습니다.


3. Projects 섹션

이제 Projects 컴포넌트에서는 단순히 데이터를 돌려주면 됩니다.

// app/Projects.tsx
import { projects } from "@/data/projects";
import ProjectCard from "@/components/ProjectCard";

export default function Projects() {
  return (
    <section id="projects" className="min-h-screen py-20">
      <h2 className="mb-8 text-2xl font-bold">📁 프로젝트</h2>
      <div className="grid gap-8 sm:grid-cols-2">
        {projects.map((p) => <ProjectCard key={p.title} {...p} />)}
      </div>
    </section>
  );
}

4. 왜 이렇게 만들었을까?

1. 데이터 중심 구조
→ 프로젝트 추가/수정이 간단 (데이터만 수정하면 UI 자동 반영)

2. 성과 지표 강조
→ 신입 지원자라도 “무엇을 개선했는지”를 수치로 보여줄 수 있음

3. Case Study
→ “무엇을 만들었는가”보다 중요한 “왜, 어떻게 개선했는가”를 전달

4. 확장성
→ 프로젝트가 많아져도 필터/정렬 기능을 쉽게 추가 가능


5. 배운 점

  • 포트폴리오는 단순히 “코드를 잘 짠다”가 아니라 문제 해결 능력을 보여줘야 한다.
  • 프로젝트마다 성과와 지표를 기록해 두는 게 큰 무기다.
  • UI 컴포넌트는 재사용성과 확장성을 고려해 처음부터 설계하는 것이 좋다.

✨ 마무리

오늘 작업한 프로젝트 섹션은, 단순한 목록을 넘어서
성과·지표·케이스 스터디까지 담을 수 있도록 발전시켰습니다.

앞으로는 프로젝트가 늘어나면 스택별 필터 / 정렬 옵션도 넣어서,
리쿠르터나 면접관이 원하는 프로젝트를 더 빠르게 찾을 수 있도록 개선할 계획입니다.

profile
html_programming_language

0개의 댓글