HTML 구조 설계
웹 페이지의 골격을 구성하는 HTML은 의미 중심의 태그 활용이 핵심입니다. 404 페이지의 경우 시각적 요소와 메시지를 담을 컨테이너를 명확히 구분하는 것이 중요합니다.
<!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="style.css">
</head>
<body>
<main class="error-container">
<figure class="ghost-wrapper">
<div class="ghost-body"></div>
<div class="ghost-eyes"></div>
</figure>
<h1>404</h1>
<p class="error-message">요청하신 페이지가 사라졌거나 존재하지 않습니다</p>
<a href="/" class="return-btn">홈으로 돌아가기</a>
</main>
<script src="main.js"></script>
</body>
</html>
CSS3 애니메이션 핵심 기법
키프레임 정의와 적용
유령이 위아래로 떠다니는 효과를 @keyframes로 구현합니다. 중간 상태를 세밀하게 조절하면 더 자연스러운 움직임을 얻을 수 있습니다.
@keyframes levitate {
0%, 100% {
transform: translateY(0) rotate(0deg);
}
25% {
transform: translateY(-20px) rotate(2deg);
}
50% {
transform: translateY(-35px) rotate(0deg);
}
75% {
transform: translateY(-15px) rotate(-2deg);
}
}
@keyframes eye-blink {
0%, 90%, 100% {
transform: scaleY(1);
}
95% {
transform: scaleY(0.1);
}
}
.ghost-body {
animation: levitate 3s ease-in-out infinite;
}
.ghost-eyes::after,
.ghost-eyes::before {
animation: eye-blink 4s infinite;
transform-origin: center;
}
시각적 깊이감 추가
그림자와 투명도 변화를 연동하면 입체감을 살릴 수 있습니다.
.ghost-wrapper {
position: relative;
filter: drop-shadow(0 15px 10px rgba(0, 0, 0, 0.15));
}
.ghost-body {
width: 120px;
height: 160px;
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 60px 60px 10px 10px;
position: relative;
}
.ghost-body::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 10px;
background: rgba(0, 0, 0, 0.1);
border-radius: 50%;
filter: blur(4px);
animation: shadow-pulse 3s ease-in-out infinite;
}
@keyframes shadow-pulse {
0%, 100% { transform: translateX(-50%) scale(1); opacity: 0.6; }
50% { transform: translateX(-50%) scale(0.7); opacity: 0.3; }
}
Transform과 Transition 활용
호버 상호작용 구현
.return-btn {
display: inline-block;
padding: 14px 32px;
background: #339af0;
color: #fff;
text-decoration: none;
border-radius: 8px;
transition:
background-color 0.25s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.2s ease-out,
box-shadow 0.25s ease;
}
.return-btn:hover {
background-color: #228be6;
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(51, 154, 240, 0.35);
}
.return-btn:active {
transform: translateY(0) scale(0.98);
}
숫자 404의 동적 효과
.error-container > h1 {
font-size: clamp(8rem, 15vw, 16rem);
font-weight: 900;
line-height: 1;
background: linear-gradient(135deg, #339af0 0%, #845ef7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
transform: perspective(500px) rotateX(10deg);
transition: transform 0.5s ease;
}
.error-container > h1:hover {
transform: perspective(500px) rotateX(0deg) scale(1.05);
}
JavaScript로 동적 제어
DOM 조작과 이벤트 바인딩
const container = document.querySelector('.error-container');
const ghost = document.querySelector('.ghost-body');
const message = document.querySelector('.error-message');
// 마우스 위치에 따른 패럴럭스 효과
document.addEventListener('mousemove', (evt) => {
const xAxis = (window.innerWidth / 2 - evt.pageX) / 25;
const yAxis = (window.innerHeight / 2 - evt.pageY) / 25;
ghost.style.transform = `translateY(${yAxis}px) translateX(${xAxis}px)`;
});
// 클릭 시 유령 흔들림 효과
ghost.addEventListener('click', () => {
ghost.style.animation = 'none';
ghost.offsetHeight; // 리플로우 강제
ghost.style.animation = 'levitate 3s ease-in-out infinite, shake 0.5s ease';
setTimeout(() => {
ghost.style.animation = 'levitate 3s ease-in-out infinite';
}, 500);
});
타이머 기반 동작 제어
class TypeWriter {
constructor(element, text, speed = 80) {
this.element = element;
this.text = text;
this.speed = speed;
this.charIndex = 0;
this.timerRef = null;
}
start() {
this.element.textContent = '';
this.typeNextChar();
}
typeNextChar() {
if (this.charIndex < this.text.length) {
this.element.textContent += this.text.charAt(this.charIndex);
this.charIndex++;
this.timerRef = setTimeout(() => this.typeNextChar(), this.speed);
}
}
destroy() {
clearTimeout(this.timerRef);
}
}
// 페이지 로드 후 타이핑 효과 실행
const writer = new TypeWriter(message, '요청하신 페이지가 사라졌거나 존재하지 않습니다', 60);
document.addEventListener('DOMContentLoaded', () => writer.start());
성능 최적화를 위한 타이머 관리
class AnimationController {
constructor() {
this.observers = new Map();
this.rafId = null;
}
observe(element, callback, interval = 16) {
let lastTime = 0;
const tick = (timestamp) => {
if (timestamp - lastTime >= interval) {
callback(element);
lastTime = timestamp;
}
this.rafId = requestAnimationFrame(tick);
};
this.observers.set(element, tick);
this.rafId = requestAnimationFrame(tick);
}
disconnect(element) {
if (this.observers.has(element)) {
cancelAnimationFrame(this.rafId);
this.observers.delete(element);
}
}
clearAll() {
this.observers.forEach((_, element) => this.disconnect(element));
}
}
// 사용 예시
const controller = new AnimationController();
const floatingElements = document.querySelectorAll('.float-item');
floatingElements.forEach((el, idx) => {
controller.observe(el, (target) => {
const time = Date.now() * 0.001;
const offset = Math.sin(time + idx) * 10;
target.style.transform = `translateY(${offset}px)`;
}, 16);
});
접근성 고려사항
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
사용자의 동작 선호도를 존중하는 미디어 쿼리는 필수입니다. prefers-reduced-motion 미디어 특성을 활용하면 전막 장애 사용자도 불편함 없이 콘텐츠를 소비할 수 있습니다.