강의 내용
1. 프로젝트 기획과 설계
성공적인 웹 애플리케이션 개발을 위한 체계적인 접근 방법을 학습합니다.
요구사항 분석
# 개인 포트폴리오 웹사이트 기획서
## 1. 프로젝트 개요
- **목적**: 개인 포트폴리오를 효과적으로 보여주는 반응형 웹사이트
- **대상**: 잠재적 고용주, 클라이언트, 동료 개발자
- **기간**: 3주 (설계 1주 + 개발 2주)
## 2. 기능 요구사항
### 필수 기능 (Must Have)
- 개인 소개 섹션
- 프로젝트 포트폴리오 갤러리
- 연락처 양식
- 반응형 디자인
- 네비게이션 메뉴
### 선택 기능 (Should Have)
- 다크/라이트 테마 전환
- 애니메이션 효과
- 프로젝트 필터링
- 블로그 섹션
### 고려사항 (Could Have)
- 다국어 지원
- PWA 기능
- 소셜 미디어 연동
## 3. 기술 스택
- **프론트엔드**: HTML5, CSS3, JavaScript (ES6+)
- **라이브러리**: 없음 (바닐라 JavaScript)
- **도구**: GitHub, VS Code, GitHub Copilot
- **배포**: GitHub Pages 또는 Netlify
와이어프레임과 프로토타입
<!-- 기본 HTML 구조 설계 -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>김철수 - 프론트엔드 개발자</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<header class="header">
<nav class="navbar">
<div class="nav-brand">김철수</div>
<ul class="nav-menu">
<li><a href="#about">소개</a></li>
<li><a href="#projects">프로젝트</a></li>
<li><a href="#contact">연락처</a></li>
</ul>
</nav>
</header>
<main>
<section id="hero" class="hero">
<h1>안녕하세요, 프론트엔드 개발자 김철수입니다</h1>
<p>사용자 중심의 웹 경험을 만들어갑니다</p>
</section>
<section id="about" class="about">
<h2>소개</h2>
<p>개발자로서의 경험과 기술을 소개합니다</p>
</section>
<section id="projects" class="projects">
<h2>프로젝트</h2>
<div class="project-grid">
<!-- 프로젝트 카드들 -->
</div>
</section>
<section id="contact" class="contact">
<h2>연락처</h2>
<form class="contact-form">
<!-- 연락처 양식 -->
</form>
</section>
</main>
<footer class="footer">
<p>© 2025 김철수. All rights reserved.</p>
</footer>
<script src="js/main.js"></script>
</body>
</html>
2. 모듈화와 컴포넌트 설계
유지보수하기 쉽고 재사용 가능한 코드 구조를 만드는 방법을 학습합니다.
JavaScript 모듈 시스템
// utils.js - 유틸리티 함수들
export const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
export const throttle = (func, wait) => {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, wait);
}
};
};
export const formatDate = (date) => {
return new Intl.DateTimeFormat('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(new Date(date));
};
// api.js - API 관련 함수들
export class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
};
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API 요청 실패:', error);
throw error;
}
}
}
컴포넌트 기반 개발
// components/ProjectCard.js
export class ProjectCard {
constructor(project) {
this.project = project;
this.element = this.createElement();
}
createElement() {
const card = document.createElement('div');
card.className = 'project-card';
card.innerHTML = `
${this.project.title}
${this.project.description}
${this.project.technologies.map(tech =>
`${tech}`
).join('')}
`;
this.setupEventListeners(card);
return card;
}
setupEventListeners(card) {
card.addEventListener('click', (e) => {
if (e.target.classList.contains('btn-demo')) {
window.open(e.target.dataset.url, '_blank');
} else if (e.target.classList.contains('btn-source')) {
window.open(e.target.dataset.url, '_blank');
}
});
}
render(container) {
container.appendChild(this.element);
}
}
// components/ContactForm.js
export class ContactForm {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.form = null;
this.init();
}
init() {
this.createForm();
this.setupEventListeners();
}
createForm() {
this.form = document.createElement('form');
this.form.className = 'contact-form';
this.form.innerHTML = `
`;
this.container.appendChild(this.form);
}
setupEventListeners() {
this.form.addEventListener('submit', this.handleSubmit.bind(this));
}
async handleSubmit(e) {
e.preventDefault();
const formData = new FormData(this.form);
const data = Object.fromEntries(formData.entries());
try {
this.setLoading(true);
await this.submitForm(data);
this.showSuccess('메시지가 성공적으로 전송되었습니다!');
this.form.reset();
} catch (error) {
this.showError('전송 중 오류가 발생했습니다: ' + error.message);
} finally {
this.setLoading(false);
}
}
async submitForm(data) {
// 실제 구현에서는 서버로 데이터 전송
return new Promise((resolve) => {
setTimeout(() => {
console.log('전송된 데이터:', data);
resolve();
}, 1000);
});
}
setLoading(loading) {
const submitBtn = this.form.querySelector('.btn-submit');
submitBtn.disabled = loading;
submitBtn.textContent = loading ? '전송 중...' : '전송';
}
showSuccess(message) {
this.showMessage(message, 'success');
}
showError(message) {
this.showMessage(message, 'error');
}
showMessage(message, type) {
const existingMessage = this.container.querySelector('.form-message');
if (existingMessage) {
existingMessage.remove();
}
const messageEl = document.createElement('div');
messageEl.className = `form-message ${type}`;
messageEl.textContent = message;
this.container.appendChild(messageEl);
setTimeout(() => messageEl.remove(), 5000);
}
}
3. 성능 최적화 기법
웹 애플리케이션의 로딩 속도와 사용자 경험을 개선하는 방법을 학습합니다.
이미지 최적화와 지연 로딩
// ImageLoader.js - 지연 로딩 구현
export class ImageLoader {
constructor() {
this.observer = this.createIntersectionObserver();
this.loadedImages = new Set();
}
createIntersectionObserver() {
return new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target);
}
});
}, {
rootMargin: '50px'
});
}
observe(img) {
if (this.loadedImages.has(img)) return;
this.observer.observe(img);
}
async loadImage(img) {
const src = img.dataset.src;
if (!src) return;
try {
// 이미지 미리 로드
const image = new Image();
await this.preloadImage(image, src);
// 페이드 인 효과
img.style.opacity = '0';
img.src = src;
img.onload = () => {
img.style.transition = 'opacity 0.3s';
img.style.opacity = '1';
this.loadedImages.add(img);
};
} catch (error) {
console.error('이미지 로딩 실패:', error);
img.src = 'images/placeholder.jpg'; // 플레이스홀더 이미지
}
}
preloadImage(img, src) {
return new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = src;
});
}
// 반응형 이미지 처리
static getResponsiveImageSrc(baseSrc, width) {
const breakpoints = {
320: 'small',
768: 'medium',
1024: 'large',
1920: 'xlarge'
};
const size = Object.keys(breakpoints)
.reverse()
.find(bp => width >= bp) || 'small';
const extension = baseSrc.split('.').pop();
const name = baseSrc.replace(`.${extension}`, '');
return `${name}-${breakpoints[size]}.${extension}`;
}
}
// 사용 예제
const imageLoader = new ImageLoader();
// 모든 지연 로딩 이미지 관찰
document.querySelectorAll('img[data-src]').forEach(img => {
imageLoader.observe(img);
});
코드 분할과 번들링
// 동적 import를 사용한 코드 분할
class ModuleLoader {
constructor() {
this.loadedModules = new Map();
}
async loadModule(moduleName) {
if (this.loadedModules.has(moduleName)) {
return this.loadedModules.get(moduleName);
}
try {
let module;
switch (moduleName) {
case 'chart':
module = await import('./components/Chart.js');
break;
case 'gallery':
module = await import('./components/Gallery.js');
break;
case 'contact':
module = await import('./components/ContactForm.js');
break;
default:
throw new Error(`Unknown module: ${moduleName}`);
}
this.loadedModules.set(moduleName, module);
return module;
} catch (error) {
console.error(`Failed to load module ${moduleName}:`, error);
throw error;
}
}
async loadModuleWhenNeeded(moduleName, trigger) {
trigger.addEventListener('click', async () => {
try {
const module = await this.loadModule(moduleName);
// 모듈 사용
this.useModule(module, trigger);
} catch (error) {
console.error('Module loading failed:', error);
}
}, { once: true });
}
useModule(module, context) {
// 로드된 모듈을 실제로 사용
if (module.default) {
new module.default(context);
}
}
}
// 리소스 프리로딩
class ResourcePreloader {
constructor() {
this.preloadQueue = [];
this.loaded = new Set();
}
preloadImage(src) {
if (this.loaded.has(src)) return Promise.resolve();
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
this.loaded.add(src);
resolve();
};
img.onerror = reject;
img.src = src;
});
}
preloadFont(fontFamily, fontWeight = 'normal') {
const font = new FontFace(fontFamily, `url(/fonts/${fontFamily}.woff2)`, {
weight: fontWeight
});
return font.load().then(() => {
document.fonts.add(font);
this.loaded.add(fontFamily);
});
}
async preloadCriticalResources() {
const criticalImages = [
'images/hero-bg.jpg',
'images/profile.jpg'
];
const criticalFonts = [
{ family: 'NotoSansKR', weight: '400' },
{ family: 'NotoSansKR', weight: '700' }
];
const imagePromises = criticalImages.map(src => this.preloadImage(src));
const fontPromises = criticalFonts.map(font =>
this.preloadFont(font.family, font.weight)
);
try {
await Promise.all([...imagePromises, ...fontPromises]);
console.log('Critical resources preloaded');
} catch (error) {
console.error('Preloading failed:', error);
}
}
}
성능 모니터링
// 성능 측정 클래스
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.observer = this.createPerformanceObserver();
}
createPerformanceObserver() {
if (!window.PerformanceObserver) return null;
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
this.processEntry(entry);
});
});
try {
observer.observe({ entryTypes: ['navigation', 'paint', 'largest-contentful-paint'] });
} catch (error) {
console.warn('PerformanceObserver not fully supported');
}
return observer;
}
processEntry(entry) {
switch (entry.entryType) {
case 'navigation':
this.metrics.loadTime = entry.loadEventEnd - entry.loadEventStart;
this.metrics.domContentLoaded = entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart;
break;
case 'paint':
if (entry.name === 'first-contentful-paint') {
this.metrics.fcp = entry.startTime;
}
break;
case 'largest-contentful-paint':
this.metrics.lcp = entry.startTime;
break;
}
}
measureCustomMetric(name, fn) {
const start = performance.now();
const result = fn();
const end = performance.now();
this.metrics[name] = end - start;
console.log(`${name}: ${this.metrics[name].toFixed(2)}ms`);
return result;
}
async measureAsyncMetric(name, asyncFn) {
const start = performance.now();
const result = await asyncFn();
const end = performance.now();
this.metrics[name] = end - start;
console.log(`${name}: ${this.metrics[name].toFixed(2)}ms`);
return result;
}
getWebVitals() {
return {
fcp: this.metrics.fcp,
lcp: this.metrics.lcp,
loadTime: this.metrics.loadTime,
domContentLoaded: this.metrics.domContentLoaded
};
}
reportMetrics() {
console.table(this.metrics);
// Google Analytics 또는 다른 분석 도구로 전송
if (typeof gtag !== 'undefined') {
Object.entries(this.metrics).forEach(([name, value]) => {
gtag('event', 'performance_metric', {
metric_name: name,
value: Math.round(value),
custom_parameter: window.location.pathname
});
});
}
}
}
// 사용 예제
const performanceMonitor = new PerformanceMonitor();
// 페이지 로드 완료 후 메트릭 리포트
window.addEventListener('load', () => {
setTimeout(() => {
performanceMonitor.reportMetrics();
}, 1000);
});
4. 프로젝트 구조와 베스트 프랙티스
확장 가능하고 유지보수하기 쉬운 프로젝트 구조를 설계하는 방법을 학습합니다.
프로젝트 폴더 구조
portfolio-website/
├── index.html
├── css/
│ ├── styles.css
│ ├── components/
│ │ ├── header.css
│ │ ├── hero.css
│ │ ├── projects.css
│ │ └── contact.css
│ └── utilities/
│ ├── variables.css
│ ├── mixins.css
│ └── reset.css
├── js/
│ ├── main.js
│ ├── components/
│ │ ├── Header.js
│ │ ├── ProjectCard.js
│ │ ├── ContactForm.js
│ │ └── ThemeToggle.js
│ ├── utils/
│ │ ├── helpers.js
│ │ ├── api.js
│ │ └── storage.js
│ └── services/
│ ├── ProjectService.js
│ └── ContactService.js
├── images/
│ ├── projects/
│ ├── icons/
│ └── optimized/
├── fonts/
├── data/
│ └── projects.json
├── docs/
│ ├── README.md
│ └── DEPLOYMENT.md
└── tests/
├── unit/
└── integration/
메인 애플리케이션 클래스
// main.js - 애플리케이션 진입점
import { Header } from './components/Header.js';
import { ProjectGallery } from './components/ProjectGallery.js';
import { ContactForm } from './components/ContactForm.js';
import { ThemeToggle } from './components/ThemeToggle.js';
import { PerformanceMonitor } from './utils/PerformanceMonitor.js';
import { ModuleLoader } from './utils/ModuleLoader.js';
class Portfolio {
constructor() {
this.components = new Map();
this.performanceMonitor = new PerformanceMonitor();
this.moduleLoader = new ModuleLoader();
this.init();
}
async init() {
try {
// 성능 모니터링 시작
this.performanceMonitor.measureCustomMetric('app-init', () => {
this.setupComponents();
this.setupEventListeners();
this.loadInitialData();
});
// 컴포넌트 초기화
await this.initializeComponents();
// 테마 설정 복원
this.restoreUserPreferences();
console.log('Portfolio application initialized');
} catch (error) {
console.error('Application initialization failed:', error);
this.handleInitError(error);
}
}
setupComponents() {
// 컴포넌트 인스턴스 생성
this.components.set('header', new Header());
this.components.set('themeToggle', new ThemeToggle());
// 지연 로딩 컴포넌트들
this.setupLazyComponents();
}
setupLazyComponents() {
// 프로젝트 섹션이 뷰포트에 들어올 때 로드
const projectSection = document.getElementById('projects');
if (projectSection) {
this.observeSection(projectSection, () => {
this.loadProjectGallery();
});
}
// 연락처 섹션이 뷰포트에 들어올 때 로드
const contactSection = document.getElementById('contact');
if (contactSection) {
this.observeSection(contactSection, () => {
this.loadContactForm();
});
}
}
observeSection(section, callback) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
callback();
observer.unobserve(section);
}
});
}, { threshold: 0.1 });
observer.observe(section);
}
async loadProjectGallery() {
if (this.components.has('projectGallery')) return;
try {
const projectData = await this.loadProjectData();
const gallery = new ProjectGallery(projectData);
this.components.set('projectGallery', gallery);
} catch (error) {
console.error('Failed to load project gallery:', error);
}
}
async loadContactForm() {
if (this.components.has('contactForm')) return;
const form = new ContactForm('contact-form-container');
this.components.set('contactForm', form);
}
async loadProjectData() {
try {
const response = await fetch('./data/projects.json');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('Failed to load project data:', error);
// 폴백 데이터 반환
return this.getFallbackProjects();
}
}
getFallbackProjects() {
return [
{
id: 1,
title: '샘플 프로젝트',
description: '데이터 로딩 실패 시 표시되는 샘플입니다.',
image: 'images/placeholder.jpg',
technologies: ['HTML', 'CSS', 'JavaScript'],
demoUrl: '#',
sourceUrl: '#'
}
];
}
setupEventListeners() {
// 전역 에러 처리
window.addEventListener('error', this.handleError.bind(this));
window.addEventListener('unhandledrejection', this.handleError.bind(this));
// 네트워크 상태 모니터링
window.addEventListener('online', this.handleNetworkChange.bind(this));
window.addEventListener('offline', this.handleNetworkChange.bind(this));
// 페이지 가시성 변경
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
}
handleError(event) {
console.error('Global error:', event.error || event.reason);
// 에러 리포팅 서비스에 전송
this.reportError(event.error || event.reason);
}
handleNetworkChange(event) {
const isOnline = event.type === 'online';
console.log(`Network status: ${isOnline ? 'online' : 'offline'}`);
// UI에 네트워크 상태 표시
this.updateNetworkStatus(isOnline);
}
handleVisibilityChange() {
if (document.hidden) {
// 페이지가 숨겨졌을 때 (탭 전환 등)
this.pauseAnimations();
} else {
// 페이지가 다시 보일 때
this.resumeAnimations();
}
}
restoreUserPreferences() {
const preferences = JSON.parse(localStorage.getItem('portfolio-preferences')) || {};
// 테마 설정 복원
if (preferences.theme) {
const themeToggle = this.components.get('themeToggle');
themeToggle?.setTheme(preferences.theme);
}
// 기타 사용자 설정 복원
if (preferences.reducedMotion) {
document.documentElement.classList.add('reduced-motion');
}
}
saveUserPreferences() {
const preferences = {
theme: document.documentElement.dataset.theme || 'light',
reducedMotion: document.documentElement.classList.contains('reduced-motion'),
lastVisit: new Date().toISOString()
};
localStorage.setItem('portfolio-preferences', JSON.stringify(preferences));
}
}
// 애플리케이션 시작
document.addEventListener('DOMContentLoaded', () => {
new Portfolio();
});
// 페이지 언로드 시 사용자 설정 저장
window.addEventListener('beforeunload', () => {
if (window.portfolioApp) {
window.portfolioApp.saveUserPreferences();
}
});