Guider/Frontend/FrontendDevGuide0007
Frontend#07

FrontendDevGuide0007

실전 프로젝트

🚀 실전 프로젝트 — 포트폴리오 완성부터 취업까지 A-Z

📋 7단계 학습 개요 | 포트폴리오 제작 + GitHub + Vercel 배포 | 예상 학습 기간: 4~6주

드디어 마지막 단계! 지금까지 배운 HTML, CSS, JavaScript, React를 총동원하여
실제로 동작하는 포트폴리오 웹사이트를 만들고 인터넷에 배포합니다.
이 단계를 마치면 전 세계 어디서나 접속 가능한 나만의 URL이 생깁니다!

실전 프로젝트 전체 로드맵


🐙 1. Git & GitHub 완전 정복

Git이란? 코드의 변경 이력을 관리하는 버전 관리 시스템입니다.
마치 게임의 세이브 포인트처럼, 언제든지 예전 상태로 돌아갈 수 있습니다.
GitHub는 Git 저장소를 온라인에 올려두는 클라우드 서비스입니다 (코드의 구글 드라이브).

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 배포 파이프라인

🔧 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)

## 📸 스크린샷
![메인 화면](./screenshots/main.png)
![검색 결과](./screenshots/search.png)

## ✨ 주요 기능
- 실시간 영화 검색 (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)
• 기술 블로그 운영 (배운 것 정리하면서 실력 향상)
• 해커톤 참가 (빠른 완성 경험)
반응형