🚀 실전 프로젝트 — 포트폴리오 완성부터 취업까지 A-Z
📋 7단계 학습 개요 | 포트폴리오 제작 + GitHub + Vercel 배포 | 예상 학습 기간: 4~6주
드디어 마지막 단계! 지금까지 배운 HTML, CSS, JavaScript, React를 총동원하여
실제로 동작하는 포트폴리오 웹사이트를 만들고 인터넷에 배포합니다.
이 단계를 마치면 전 세계 어디서나 접속 가능한 나만의 URL이 생깁니다!
🐙 1. Git & GitHub 완전 정복
Git이란? 코드의 변경 이력을 관리하는 버전 관리 시스템입니다.
마치 게임의 세이브 포인트처럼, 언제든지 예전 상태로 돌아갈 수 있습니다.
GitHub는 Git 저장소를 온라인에 올려두는 클라우드 서비스입니다 (코드의 구글 드라이브).
📌 Git 처음 설정부터 첫 커밋까지
# 1. Git 설치 확인
git --version
# 2. 최초 1회 사용자 정보 설정
git config --global user.name "홍길동"
git config --global user.email "hong@example.com"
# 3. 프로젝트 폴더에서 저장소 초기화
cd my-project
git init
# 4. GitHub에서 새 저장소 생성 후 연결
git remote add origin https://github.com/username/my-project.git
# 5. 첫 커밋 & 푸시
git add .
git commit -m "feat: 프로젝트 초기 설정"
git branch -M main
git push -u origin main
🔄 일상적인 Git 워크플로우 (현업 표준)
# 매일 작업 시작할 때
git pull origin main # 최신 코드 내려받기
# 새 기능 작업 시
git checkout -b feature/login # 새 브랜치 생성
# 작업 중 수시로 커밋
git add .
git commit -m "feat: 로그인 폼 UI 구현"
git commit -m "fix: 이메일 유효성 검사 버그 수정"
git commit -m "style: 버튼 색상 변경"
# 작업 완료 후 GitHub에 올리기
git push origin feature/login
# GitHub에서 Pull Request (PR) 생성 → 코드 리뷰 → main에 머지
💡 커밋 메시지 컨벤션 (현업 표준 - Conventional Commits)
• feat: 새 기능 추가
• fix: 버그 수정
• style: 코드 포맷 변경 (기능 변화 없음)
• refactor: 코드 리팩토링
• docs: 문서 수정
• chore: 빌드/패키지 수정
예시:feat: 로그인 기능 구현/fix: 모바일 레이아웃 깨짐 수정
🌿 .gitignore 설정 (중요!)
# .gitignore — Git이 무시할 파일 목록
node_modules/ # 패키지 폴더 (용량 큼, npm install로 재설치 가능)
.env # 환경 변수 (API 키 등 비밀 정보!)
.env.local
dist/ # 빌드 결과물
.DS_Store # macOS 시스템 파일
*.log # 로그 파일
# GitHub에 올리면 절대 안 되는 것들:
# API 키, 비밀번호, 개인정보, 결제 정보
🖥️ 2. 포트폴리오 사이트 기획 & 구현
📁 프로젝트 구조 설정
npm create vite@latest portfolio -- --template react
cd portfolio
npm install react-router-dom
# 추천 추가 패키지
npm install framer-motion # 애니메이션
npm install react-icons # 아이콘
npm install @emailjs/browser # 이메일 전송
src/
├── components/
│ ├── Navbar/
│ ├── Hero/
│ ├── Skills/
│ ├── Projects/
│ ├── About/
│ └── Contact/
├── data/
│ └── projects.js # 프로젝트 데이터 (배열로 관리)
├── hooks/
│ └── useScrollSpy.js
├── App.jsx
└── main.jsx
📌 Hero 섹션 — 첫인상이 전부!
import { useState, useEffect } from 'react';
function Hero() {
const [displayText, setDisplayText] = useState('');
const roles = ['프론트엔드 개발자', 'React 개발자', 'UI/UX 구현자'];
const [roleIndex, setRoleIndex] = useState(0);
// 타이핑 애니메이션
useEffect(() => {
const currentRole = roles[roleIndex];
let charIndex = 0;
const timer = setInterval(() => {
setDisplayText(currentRole.slice(0, charIndex + 1));
charIndex++;
if (charIndex === currentRole.length) {
clearInterval(timer);
setTimeout(() => {
setRoleIndex(prev => (prev + 1) % roles.length);
setDisplayText('');
}, 1500);
}
}, 100);
return () => clearInterval(timer);
}, [roleIndex]);
return (
<section id="hero" className="hero">
<div className="hero-content">
<p className="hero-greeting">안녕하세요 👋</p>
<h1 className="hero-name">저는 <span>홍길동</span>입니다</h1>
<h2 className="hero-role">
{displayText}<span className="cursor">|</span>
</h2>
<p className="hero-desc">
사용자 경험을 최우선으로 생각하는 프론트엔드 개발자입니다.
</p>
<div className="hero-buttons">
<a href="#projects" className="btn-primary">프로젝트 보기</a>
<a href="/resume.pdf" download className="btn-secondary">이력서 다운로드</a>
</div>
</div>
</section>
);
}
📌 Projects 섹션 — 데이터 분리 패턴
// src/data/projects.js
export const projects = [
{
id: 1,
title: '날씨 앱',
description: 'OpenWeather API를 활용한 실시간 날씨 확인 앱',
image: '/projects/weather.png',
techStack: ['React', 'fetch API', 'CSS Modules'],
githubUrl: 'https://github.com/username/weather-app',
liveUrl: 'https://weather-app.vercel.app',
featured: true,
},
{
id: 2,
title: '영화 검색 앱',
description: 'TMDB API 기반 영화 검색 및 즐겨찾기 기능',
image: '/projects/movie.png',
techStack: ['React', 'React Query', 'React Router'],
githubUrl: 'https://github.com/username/movie-app',
liveUrl: 'https://movie-app.vercel.app',
featured: true,
},
];
// ProjectCard.jsx 컴포넌트
function ProjectCard({ project }) {
return (
<div className="project-card">
<div className="project-image">
<img src={project.image} alt={project.title} loading="lazy" />
<div className="project-overlay">
<a href={project.liveUrl} target="_blank" rel="noreferrer">
🌐 라이브 데모
</a>
<a href={project.githubUrl} target="_blank" rel="noreferrer">
🐙 GitHub
</a>
</div>
</div>
<div className="project-info">
<h3>{project.title}</h3>
<p>{project.description}</p>
<div className="tech-stack">
{project.techStack.map(tech => (
<span key={tech} className="badge">{tech}</span>
))}
</div>
</div>
</div>
);
}
📌 Contact 섹션 — EmailJS로 실제 이메일 전송
import emailjs from '@emailjs/browser';
import { useRef, useState } from 'react';
function Contact() {
const formRef = useRef(null);
const [status, setStatus] = useState('idle'); // idle | sending | success | error
const handleSubmit = async (e) => {
e.preventDefault();
setStatus('sending');
try {
await emailjs.sendForm(
import.meta.env.VITE_EMAILJS_SERVICE_ID,
import.meta.env.VITE_EMAILJS_TEMPLATE_ID,
formRef.current,
import.meta.env.VITE_EMAILJS_PUBLIC_KEY,
);
setStatus('success');
formRef.current.reset();
} catch {
setStatus('error');
}
};
return (
<section id="contact">
<h2>연락하기</h2>
<form ref={formRef} onSubmit={handleSubmit}>
<input name="user_name" placeholder="이름" required />
<input name="user_email" type="email" placeholder="이메일" required />
<textarea name="message" placeholder="메시지" rows={5} required />
<button type="submit" disabled={status === 'sending'}>
{status === 'sending' ? '전송 중...' : '메시지 보내기'}
</button>
{status === 'success' && <p className="success">✅ 메시지가 전송되었습니다!</p>}
{status === 'error' && <p className="error">❌ 전송 실패. 다시 시도해주세요.</p>}
</form>
</section>
);
}
▲ 3. Vercel 배포 완전 가이드
🔧 Vercel 배포 단계별 가이드
# 방법 1: Vercel CLI (터미널에서 바로 배포)
npm install -g vercel
vercel login
vercel # 대화형으로 설정
vercel --prod # 프로덕션 배포
# 방법 2: GitHub 연동 (권장 — 자동 배포)
# 1. vercel.com → "Add New Project"
# 2. GitHub 저장소 선택
# 3. Build Command: npm run build
# 4. Output Directory: dist
# 5. "Deploy" 클릭
# 이후 git push 할 때마다 자동 배포!
# vercel.json — 라우팅 설정 (React Router 사용 시 필수!)
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
# 없으면 /about 직접 접속 시 404 에러 발생!
🔑 환경 변수 설정 (Vercel 대시보드)
Vercel 프로젝트 → Settings → Environment Variables에서 추가
• VITE_API_KEY = your_api_key
.env.local 파일의 내용을 그대로 붙여넣기하면 됩니다.
⚠️ 절대 GitHub에 API 키를 올리지 마세요!
🎬 4. 추천 실전 프로젝트 — 단계별 구현 가이드
🟢 프로젝트 1: 영화 검색 앱 (입문)
TMDB API + React + React Query 조합으로 만드는 영화 검색 앱
API 키는 tmdb.org에서 무료로 발급 가능합니다.
// 핵심 기능 구현
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
const TMDB_BASE = 'https://api.themoviedb.org/3';
const API_KEY = import.meta.env.VITE_TMDB_KEY;
function MovieSearch() {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState('');
// 검색 debounce
useEffect(() => {
const timer = setTimeout(() => setDebouncedQuery(query), 400);
return () => clearTimeout(timer);
}, [query]);
const { data, isLoading } = useQuery({
queryKey: ['movies', debouncedQuery],
queryFn: async () => {
const url = debouncedQuery
? TMDB_BASE + '/search/movie?api_key=' + API_KEY + '&query=' + debouncedQuery + '&language=ko-KR'
: TMDB_BASE + '/movie/popular?api_key=' + API_KEY + '&language=ko-KR';
const res = await fetch(url);
return res.json();
},
enabled: true,
});
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="영화 검색..."
/>
{isLoading && <p>로딩 중...</p>}
<div className="movie-grid">
{data?.results?.map(movie => (
<div key={movie.id} className="movie-card">
<img
src={'https://image.tmdb.org/t/p/w300' + movie.poster_path}
alt={movie.title}
loading="lazy"
/>
<h3>{movie.title}</h3>
<p>⭐ {movie.vote_average.toFixed(1)}</p>
</div>
))}
</div>
</div>
);
}
🟡 프로젝트 2: 쇼핑몰 UI (중급)
// 장바구니 전역 상태 (Zustand)
import { create } from 'zustand';
import { persist } from 'zustand/middleware'; // localStorage 자동 저장
const useCartStore = create(
persist(
(set, get) => ({
items: [],
addToCart: (product) => set(state => {
const exists = state.items.find(i => i.id === product.id);
if (exists) {
return {
items: state.items.map(i =>
i.id === product.id ? { ...i, qty: i.qty + 1 } : i
)
};
}
return { items: [...state.items, { ...product, qty: 1 }] };
}),
removeFromCart: (id) => set(state => ({
items: state.items.filter(i => i.id !== id)
})),
updateQty: (id, qty) => set(state => ({
items: qty === 0
? state.items.filter(i => i.id !== id)
: state.items.map(i => i.id === id ? { ...i, qty } : i)
})),
clearCart: () => set({ items: [] }),
totalPrice: () => get().items.reduce((sum, i) => sum + i.price * i.qty, 0),
totalCount: () => get().items.reduce((sum, i) => sum + i.qty, 0),
}),
{ name: 'cart-storage' } // localStorage 키
)
);
🔴 프로젝트 3: Next.js 블로그 (고급)
# Next.js 프로젝트 생성
npx create-next-app@latest my-blog --typescript --tailwind --app
# 마크다운 파싱
npm install gray-matter next-mdx-remote
// app/blog/[slug]/page.tsx — 정적 생성 (SSG)
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { MDXRemote } from 'next-mdx-remote/rsc';
// 1. 빌드 시 모든 slug 미리 생성
export async function generateStaticParams() {
const files = fs.readdirSync(path.join(process.cwd(), 'posts'));
return files.map(file => ({ slug: file.replace('.mdx', '') }));
}
// 2. 각 페이지 렌더링
export default async function BlogPost({ params }) {
const filePath = path.join(process.cwd(), 'posts', params.slug + '.mdx');
const source = fs.readFileSync(filePath, 'utf-8');
const { content, data } = matter(source);
return (
<article>
<h1>{data.title}</h1>
<time>{data.date}</time>
<MDXRemote source={content} />
</article>
);
}
⚡ 5. 성능 최적화 & 배포 전 체크리스트
// React 성능 최적화 실전 패턴
// 1. 코드 스플리팅 — 페이지별 번들 분리 (초기 로딩 속도 향상)
import { lazy, Suspense } from 'react';
const ProjectsPage = lazy(() => import('./pages/Projects'));
const AboutPage = lazy(() => import('./pages/About'));
// App.jsx에서 Suspense로 감싸기
<Suspense fallback={<div>로딩 중...</div>}>
<Routes>
<Route path="/projects" element={<ProjectsPage />} />
</Routes>
</Suspense>
// 2. 이미지 최적화
// <img> 태그에 반드시 loading="lazy" 추가
<img src={url} alt="설명" loading="lazy" width={300} height={200} />
// 3. 오픈 그래프 메타 태그 (index.html)
// <meta property="og:title" content="홍길동 포트폴리오" />
// <meta property="og:description" content="React 개발자 홍길동의 포트폴리오" />
// <meta property="og:image" content="https://mysite.vercel.app/og-image.png" />
// 4. 번들 사이즈 분석
npm run build
npx vite-bundle-visualizer
📝 6. README.md 완성 가이드
좋은 README는 취업에서 기술 이해도와 커뮤니케이션 능력을 동시에 보여줍니다.
프로젝트 설명은 스크린샷과 함께 작성하고, 실행 방법을 명확히 기재하세요.
# 🎬 영화 검색 앱
TMDB API를 활용한 영화 검색 및 즐겨찾기 React 앱입니다.
## 🌐 라이브 데모
[https://movie-app.vercel.app](https://movie-app.vercel.app)
## 📸 스크린샷


## ✨ 주요 기능
- 실시간 영화 검색 (debounce 적용)
- 인기 영화 목록 조회
- 즐겨찾기 추가/삭제 (localStorage 저장)
- 영화 상세 정보 모달
- 반응형 그리드 레이아웃
## 🛠️ 기술 스택
- **Frontend**: React 18, React Query, React Router v6
- **Styling**: CSS Modules
- **API**: TMDB API
- **Deployment**: Vercel
## 🚀 실행 방법
git clone https://github.com/username/movie-app.git
cd movie-app
npm install
# .env.local 파일 생성 후 VITE_TMDB_KEY=your_key 입력
npm run dev
## 📌 배운 점 / 트러블슈팅
- API 요청 최적화를 위해 debounce 적용 (불필요한 API 호출 80% 감소)
- React Query로 캐싱 구현하여 사용자 경험 향상
💼 7. 취업 준비 완전 가이드
🎤 기술 면접 대비 핵심 답변 예시
// Q1. 클로저(Closure)란?
// 내부 함수가 외부 함수의 변수에 접근할 수 있는 메커니즘
function counter() {
let count = 0; // 외부 함수 변수
return function() { // 내부 함수 (클로저)
count++;
return count; // 외부 변수 기억!
};
}
const add = counter();
add(); // 1
add(); // 2 — count가 사라지지 않고 기억됨!
// Q2. 이벤트 루프란?
// JS는 싱글 스레드 → 비동기 처리를 이벤트 루프가 관리
// Call Stack → Web APIs → Callback Queue → Call Stack 순서
// Q3. Virtual DOM이란?
// 실제 DOM의 메모리 복사본. state 변경 시 새 VDOM을 만들고
// 이전 VDOM과 비교(Diffing)하여 변경된 부분만 실제 DOM 업데이트
// Q4. CORS란?
// 다른 출처(도메인)에서 리소스 요청 시 브라우저가 차단하는 보안 정책
// 해결: 서버에서 Access-Control-Allow-Origin 헤더 설정
// 개발 중: vite.config.js의 proxy 설정 사용
🗺️ 8. 프론트엔드 기술 스택 전체 지도
✅ 7단계 & 전체 시리즈 완료 체크리스트
✅ 7단계 완료 체크리스트
☐ Git 기본 명령어 숙지 (init, add, commit, push, pull)
☐ GitHub 저장소 생성 및 코드 업로드
☐ .gitignore 설정 (node_modules, .env 제외)
☐ 포트폴리오 사이트 4개 섹션 완성
☐ 반응형 디자인 적용 (모바일, 태블릿, 데스크톱)
☐ Vercel 배포 완료 → 나만의 URL 생성 🎉
☐ README.md 작성 (스크린샷, 기술 스택, 실행 방법)
🏆 전체 FrontendDevGuide 시리즈 완료!
☐ Guide 0001 — 인터넷과 웹의 동작 원리
☐ Guide 0002 — HTML 완전 정복
☐ Guide 0003 — CSS 완전 정복
☐ Guide 0004 — JavaScript 기초
☐ Guide 0005 — JavaScript 심화 (DOM & API)
☐ Guide 0006 — React 입문
☐ Guide 0007 — 실전 프로젝트 ← 지금 여기!
🎓 수료를 축하합니다!
🎉 프론트엔드 개발자 커리큘럼 완주!
총 7단계 | 배운 기술: HTML ✅ CSS ✅ JavaScript ✅ React ✅ Git/GitHub ✅ Vercel ✅
이 여정을 완주한 것만으로도 이미 훌륭합니다. 하지만 진짜 실력은 꾸준히 만들면서 쌓입니다.
매일 조금씩이라도 코드를 작성하고, GitHub에 커밋하고, 완성품을 배포해보세요.
다음 단계로 추천하는 것들:
• TypeScript 배우기 (React와 함께 사용)
• Next.js로 풀스택 개발 도전
• 오픈소스 프로젝트 기여 (good first issue)
• 기술 블로그 운영 (배운 것 정리하면서 실력 향상)
• 해커톤 참가 (빠른 완성 경험)
반응형