10회차 — 최종 실습(포트폴리오 페이지 제작) 및 배포
학습 목표
- 기획부터 배포까지 전체 웹 개발 프로세스를 경험할 수 있다
- HTML, CSS, JavaScript를 종합적으로 활용할 수 있다
- 반응형 웹 디자인을 완성도 있게 구현할 수 있다
- 폼 유효성 검사와 시각적 피드백을 구현할 수 있다
- 정적 사이트를 배포할 수 있다
- 유지보수 체크리스트를 이해하고 적용할 수 있다
1. 프로젝트 기획
1.1 목표 설정
포트폴리오 사이트 목표:
- 나를 소개하는 개인 포트폴리오
- 프로젝트를 보여주는 갤러리
- 연락할 수 있는 연락처 폼
- 반응형 디자인 적용
- 접근성 고려
1.2 사이트 구조 설계
페이지 구성:
포트폴리오 사이트
├── 홈 (index.html)
│ ├── 히어로 섹션
│ ├── 소개 섹션
│ ├── 스킬 섹션
│ └── 프로젝트 미리보기
├── 프로젝트 (projects.html)
│ └── 프로젝트 갤러리
└── 연락처 (contact.html)
└── 연락 폼1.3 디자인 컨셉
고려사항:
- 색상 팔레트 선택
- 타이포그래피 선택
- 레이아웃 스타일
- 인터랙션 요소
디자인 도구:
- 펜과 종이 (스케치)
- Figma, Adobe XD
- 브라우저 개발자 도구
1.4 콘텐츠 준비
필요한 콘텐츠:
- 자기소개 텍스트
- 프로젝트 설명 및 이미지
- 스킬 목록
- 연락처 정보
- 프로필 사진
2. 마크업 (HTML)
2.1 기본 구조
index.html 기본 구조:
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="포트폴리오 사이트 설명">
<title>포트폴리오 | 홈</title>
<link rel="stylesheet" href="assets/css/main.css">
</head>
<body>
<!-- 헤더 -->
<header class="header">
<nav class="nav">
<!-- 네비게이션 -->
</nav>
</header>
<!-- 메인 콘텐츠 -->
<main>
<!-- 히어로 섹션 -->
<section class="hero">
<!-- 히어로 콘텐츠 -->
</section>
<!-- 소개 섹션 -->
<section class="about">
<!-- 소개 콘텐츠 -->
</section>
<!-- 스킬 섹션 -->
<section class="skills">
<!-- 스킬 목록 -->
</section>
<!-- 프로젝트 섹션 -->
<section class="projects">
<!-- 프로젝트 미리보기 -->
</section>
</main>
<!-- 푸터 -->
<footer class="footer">
<!-- 푸터 콘텐츠 -->
</footer>
<script src="assets/js/main.js"></script>
</body>
</html>
2.2 시맨틱 HTML 사용
시맨틱 태그 활용:
<header> <!-- 헤더 -->
<nav> <!-- 네비게이션 -->
<main> <!-- 메인 콘텐츠 -->
<section> <!-- 섹션 -->
<article> <!-- 독립적인 콘텐츠 -->
<aside> <!-- 사이드바 -->
<footer> <!-- 푸터 -->
2.3 접근성 고려
접근성 요소:
- alt 속성 (이미지)
- aria-label (아이콘 버튼)
- 시맨틱 태그
- 적절한 제목 구조 (h1-h6)
- 스킵 링크
<!-- 스킵 링크 -->
<a href="#main-content" class="skip-link">메인 콘텐츠로 건너뛰기</a>
<!-- 이미지 alt -->
<img src="profile.jpg" alt="프로필 사진">
<!-- 아이콘 버튼 -->
<button aria-label="메뉴 열기">
<span class="icon-menu"></span>
</button>
3. 스타일링 (CSS)
3.1 CSS 구조
파일 구조:
assets/css/
├── base/
│ ├── reset.css
│ ├── typography.css
│ └── variables.css
├── components/
│ ├── button.css
│ ├── card.css
│ └── form.css
├── layouts/
│ ├── header.css
│ ├── footer.css
│ └── grid.css
└── main.css3.2 CSS 변수 활용
:root {
/* 색상 */
--primary-color: #667eea;
--secondary-color: #764ba2;
--text-color: #2c3e50;
--bg-color: #ffffff;
--light-bg: #f5f5f5;
/* 간격 */
--spacing-unit: 8px;
--spacing-small: calc(var(--spacing-unit) * 1);
--spacing-medium: calc(var(--spacing-unit) * 2);
--spacing-large: calc(var(--spacing-unit) * 4);
/* 타이포그래피 */
--font-size-base: 16px;
--font-size-h1: 3em;
--font-size-h2: 2em;
/* 그림자 */
--shadow-small: 0 2px 5px rgba(0,0,0,0.1);
--shadow-medium: 0 5px 15px rgba(0,0,0,0.2);
}
3.3 BEM 네이밍
/* Block */
.header { }
.card { }
.button { }
/* Element */
.header__logo { }
.card__title { }
.button__icon { }
/* Modifier */
.button--primary { }
.card--highlighted { }
3.4 애니메이션 적용
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.hero__title {
animation: fadeInUp 1s ease-out;
}
4. 반응형 보완
4.1 모바일 우선 설계
/* 기본: 모바일 */
.container {
width: 100%;
padding: var(--spacing-medium);
}
/* 태블릿 */
@media (min-width: 768px) {
.container {
width: 750px;
margin: 0 auto;
padding: var(--spacing-large);
}
}
/* 데스크톱 */
@media (min-width: 1024px) {
.container {
width: 1200px;
}
}
4.2 반응형 네비게이션
/* 모바일: 햄버거 메뉴 */
.nav__menu {
display: none;
}
.nav__toggle {
display: block;
}
/* 태블릿 이상: 가로 메뉴 */
@media (min-width: 768px) {
.nav__menu {
display: flex;
}
.nav__toggle {
display: none;
}
}
4.3 반응형 그리드
.project-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--spacing-medium);
}
@media (min-width: 768px) {
.project-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.project-grid {
grid-template-columns: repeat(3, 1fr);
}
}
4.4 반응형 이미지
img {
max-width: 100%;
height: auto;
}
.hero__image {
width: 100%;
height: auto;
object-fit: cover;
}
5. 폼과 상호작용
5.1 폼 기본 구조
<form class="contact-form" id="contactForm">
<div class="form-group">
<label for="name">이름</label>
<input
type="text"
id="name"
name="name"
required
aria-required="true"
>
<span class="error-message" id="nameError"></span>
</div>
<div class="form-group">
<label for="email">이메일</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
>
<span class="error-message" id="emailError"></span>
</div>
<div class="form-group">
<label for="message">메시지</label>
<textarea
id="message"
name="message"
rows="5"
required
aria-required="true"
></textarea>
<span class="error-message" id="messageError"></span>
</div>
<button type="submit" class="button button--primary">
전송
</button>
</form>
5.2 유효성 검사 (JavaScript)
// 폼 유효성 검사
const form = document.getElementById('contactForm');
const nameInput = document.getElementById('name');
const emailInput = document.getElementById('email');
const messageInput = document.getElementById('message');
// 실시간 유효성 검사
nameInput.addEventListener('blur', () => {
validateName();
});
emailInput.addEventListener('blur', () => {
validateEmail();
});
messageInput.addEventListener('blur', () => {
validateMessage();
});
// 제출 시 검사
form.addEventListener('submit', (e) => {
e.preventDefault();
if (validateForm()) {
// 폼 제출
submitForm();
}
});
// 유효성 검사 함수
function validateName() {
const name = nameInput.value.trim();
const errorElement = document.getElementById('nameError');
if (name === '') {
showError('nameError', '이름을 입력해주세요.');
return false;
} else if (name.length < 2) {
showError('nameError', '이름은 2자 이상 입력해주세요.');
return false;
} else {
hideError('nameError');
nameInput.classList.add('valid');
return true;
}
}
function validateEmail() {
const email = emailInput.value.trim();
const errorElement = document.getElementById('emailError');
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (email === '') {
showError('emailError', '이메일을 입력해주세요.');
return false;
} else if (!emailRegex.test(email)) {
showError('emailError', '올바른 이메일 형식이 아닙니다.');
return false;
} else {
hideError('emailError');
emailInput.classList.add('valid');
return true;
}
}
function validateMessage() {
const message = messageInput.value.trim();
if (message === '') {
showError('messageError', '메시지를 입력해주세요.');
return false;
} else if (message.length < 10) {
showError('messageError', '메시지는 10자 이상 입력해주세요.');
return false;
} else {
hideError('messageError');
messageInput.classList.add('valid');
return true;
}
}
function validateForm() {
const isNameValid = validateName();
const isEmailValid = validateEmail();
const isMessageValid = validateMessage();
return isNameValid && isEmailValid && isMessageValid;
}
// 에러 표시/숨김
function showError(errorId, message) {
const errorElement = document.getElementById(errorId);
errorElement.textContent = message;
errorElement.style.display = 'block';
const input = errorElement.previousElementSibling;
input.classList.add('error');
input.classList.remove('valid');
}
function hideError(errorId) {
const errorElement = document.getElementById(errorId);
errorElement.style.display = 'none';
const input = errorElement.previousElementSibling;
input.classList.remove('error');
}
// 폼 제출
function submitForm() {
// 폼 데이터 수집
const formData = {
name: nameInput.value.trim(),
email: emailInput.value.trim(),
message: messageInput.value.trim()
};
// 성공 메시지
showSuccessMessage('메시지가 성공적으로 전송되었습니다!');
// 폼 리셋
form.reset();
// 실제로는 서버로 전송
// fetch('/api/contact', {
// method: 'POST',
// body: JSON.stringify(formData)
// });
}
function showSuccessMessage(message) {
const successDiv = document.createElement('div');
successDiv.className = 'success-message';
successDiv.textContent = message;
form.insertBefore(successDiv, form.firstChild);
setTimeout(() => {
successDiv.remove();
}, 5000);
}
5.3 시각적 피드백 (CSS)
/* 입력 필드 기본 스타일 */
.form-group input,
.form-group textarea {
width: 100%;
padding: var(--spacing-small);
border: 2px solid #ddd;
border-radius: var(--border-radius);
font-size: var(--font-size-base);
transition: border-color 0.3s, box-shadow 0.3s;
}
/* 포커스 상태 */
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* 에러 상태 */
.form-group input.error,
.form-group textarea.error {
border-color: #e74c3c;
box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.1);
}
/* 유효 상태 */
.form-group input.valid,
.form-group textarea.valid {
border-color: #27ae60;
box-shadow: 0 0 0 3px rgba(39, 174, 96, 0.1);
}
/* 에러 메시지 */
.error-message {
display: none;
color: #e74c3c;
font-size: 0.9em;
margin-top: 5px;
}
.error-message.show {
display: block;
}
/* 성공 메시지 */
.success-message {
background-color: #27ae60;
color: white;
padding: var(--spacing-medium);
border-radius: var(--border-radius);
margin-bottom: var(--spacing-medium);
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
6. 정적 사이트 배포
6.1 GitHub Pages 배포
방법 1: 저장소 설정
GitHub 저장소 생성
git init git add . git commit -m "Initial commit" git remote add origin https://github.com/username/portfolio.git git push -u origin mainGitHub Pages 활성화
- 저장소 Settings → Pages
- Source: main 브랜치 선택
- Save 클릭
접속 URL
https://username.github.io/portfolio/
방법 2: gh-pages 브랜치
# gh-pages 브랜치 생성
git checkout -b gh-pages
# 파일 푸시
git push origin gh-pages
6.2 Vercel 배포
방법 1: Vercel CLI
Vercel CLI 설치
npm install -g vercel배포
vercel프로덕션 배포
vercel --prod
방법 2: GitHub 연동
Vercel 웹사이트 접속
GitHub 연동
- GitHub 계정으로 로그인
- 저장소 선택
자동 배포
- main 브랜치에 푸시하면 자동 배포
6.3 Netlify 배포
Netlify 웹사이트 접속
드래그 앤 드롭
- 빌드된 파일을 드래그 앤 드롭
Git 연동
- GitHub 저장소 연동
- 자동 배포 설정
6.4 배포 전 체크리스트
- 모든 파일이 올바른 경로에 있는가?
- 이미지 경로가 상대 경로인가?
- CSS/JS 파일 경로가 올바른가?
- 모든 링크가 작동하는가?
- 반응형이 모든 기기에서 작동하는가?
- 폼이 정상 작동하는가?
7. 유지보수 체크리스트
7.1 접근성 체크리스트
- 색상 대비가 충분한가? (최소 4.5:1)
- 포커스 스타일이 명확한가?
- 키보드만으로 모든 기능 사용 가능한가?
- 스크린 리더가 이해할 수 있는가?
- 이미지에 alt 속성이 있는가?
- 시맨틱 HTML을 사용하는가?
- 스킵 링크가 있는가?
도구:
- WAVE (Web Accessibility Evaluation Tool)
- axe DevTools
- Lighthouse (접근성 점수)
7.2 크로스 브라우저 체크리스트
- Chrome에서 테스트
- Firefox에서 테스트
- Safari에서 테스트
- Edge에서 테스트
- 모바일 브라우저에서 테스트
테스트 도구:
- BrowserStack
- CrossBrowserTesting
- 실제 기기 테스트
호환성 고려사항:
- CSS Grid (IE 미지원)
- Flexbox (IE 10+)
- CSS 변수 (IE 미지원)
- ES6+ JavaScript
7.3 성능 체크리스트
- 이미지 최적화 (WebP, 압축)
- CSS/JS 파일 최소화
- 불필요한 코드 제거
- 폰트 최적화 (woff2 사용)
- 레이지 로딩 적용
- 캐싱 설정
성능 측정 도구:
- Lighthouse
- PageSpeed Insights
- WebPageTest
목표:
- First Contentful Paint (FCP): < 1.8초
- Largest Contentful Paint (LCP): < 2.5초
- Time to Interactive (TTI): < 3.8초
7.4 SEO 체크리스트
- 메타 태그 설정 (title, description)
- Open Graph 태그
- 구조화된 데이터 (Schema.org)
- 시맨틱 HTML 사용
- 적절한 제목 구조 (h1-h6)
- alt 속성 (이미지)
- 사이트맵 (sitemap.xml)
- robots.txt
7.5 보안 체크리스트
- HTTPS 사용
- 폼 CSRF 보호
- XSS 방지
- 민감한 정보 노출 방지
- 의존성 보안 업데이트
8. 실습 프로세스
8.1 단계별 진행
기획 (1일)
- 목표 설정
- 사이트 구조 설계
- 디자인 컨셉 결정
- 콘텐츠 준비
마크업 (2일)
- HTML 구조 작성
- 시맨틱 태그 사용
- 접근성 고려
스타일링 (3일)
- CSS 변수 설정
- 컴포넌트 스타일링
- 레이아웃 구성
- 애니메이션 추가
반응형 (2일)
- 모바일 우선 설계
- 미디어 쿼리 적용
- 다양한 기기 테스트
상호작용 (2일)
- 폼 유효성 검사
- 시각적 피드백
- JavaScript 기능 추가
배포 및 테스트 (1일)
- 배포 준비
- 배포 실행
- 체크리스트 확인
8.2 개발 팁
점진적 개발:
- 기본 구조부터 시작
- 기능을 하나씩 추가
- 각 단계마다 테스트
버전 관리:
- Git으로 버전 관리
- 기능별 커밋
- 브랜치 활용
테스트:
- 자주 테스트하기
- 다양한 기기에서 테스트
- 다른 사람에게 피드백 받기
9. 실습 체크리스트
프로젝트 완성도
- 모든 페이지가 완성되었는가?
- 디자인이 일관된가?
- 모든 링크가 작동하는가?
- 이미지가 모두 로드되는가?
기능
- 네비게이션이 작동하는가?
- 폼 유효성 검사가 작동하는가?
- 반응형이 모든 기기에서 작동하는가?
- 애니메이션이 부드러운가?
접근성
- 색상 대비가 충분한가?
- 포커스 스타일이 명확한가?
- 키보드만으로 사용 가능한가?
- 스크린 리더 호환인가?
성능
- 페이지 로딩이 빠른가?
- 이미지가 최적화되었는가?
- 불필요한 코드가 없는가?
배포
- 배포가 완료되었는가?
- 모든 기능이 배포 환경에서 작동하는가?
- URL이 올바른가?
다음 단계
- 백엔드 연동 (폼 제출)
- 데이터베이스 연동
- 사용자 인증
- 고급 애니메이션
- PWA (Progressive Web App)
'FrontEnd > Html Css' 카테고리의 다른 글
| Position:absolute를 이용한 라이언(ryan 캐릭터만들기) (1) | 2026.01.15 |
|---|---|
| 6회차 — CSS Grid 실전 (0) | 2026.01.07 |
| 9회차 — 프로젝트 구조화 및 워크플로우 (1) | 2026.01.02 |
| 8회차 — 고급 CSS 기술 및 UI 패턴 (1) | 2026.01.01 |
| 7회차 — 반응형 웹(Responsive)과 모바일 우선 설계 (0) | 2025.12.31 |