Reveal.js: HTML 프레젠테이션 프레임워크 완전 가이드 - 웹 기반 멀티미디어 프레젠테이션 마스터하기
⏱️ 예상 읽기 시간: 25분
서론
전통적인 PowerPoint나 Keynote는 더 이상 현대적인 프레젠테이션의 유일한 선택지가 아닙니다. Reveal.js는 HTML, CSS, JavaScript를 기반으로 하는 혁신적인 프레젠테이션 프레임워크로, 웹의 모든 기능을 활용한 인터랙티브하고 아름다운 프레젠테이션을 만들 수 있게 해줍니다.
이 프레임워크는 Hakim El Hattab에 의해 개발되어 수십만 명의 개발자와 발표자들이 사용하고 있으며, 기존 슬라이드 도구의 한계를 뛰어넘는 무한한 가능성을 제공합니다.
Reveal.js 개요
핵심 특징과 장점
🌐 웹 네이티브
- HTML, CSS, JavaScript 기반
- 모든 웹 기술 활용 가능
- 브라우저에서 실행되는 완전한 반응형 디자인
- 모바일 터치 지원
🎨 강력한 커스터마이징
- CSS를 통한 무제한 스타일링
- 내장된 다양한 테마
- 커스텀 애니메이션 및 전환 효과
- 플러그인 시스템
📱 다양한 출력 형태
- 웹 브라우저에서 직접 발표
- PDF 내보내기
- 정적 웹사이트 호스팅
- 스피커 노트 및 타이머 지원
🔧 개발자 친화적
- Git 버전 관리 가능
- 마크다운 지원
- API를 통한 프로그래밍 제어
- 확장 가능한 아키텍처
설치 및 환경 설정
1. 기본 설치 방법
CDN을 통한 빠른 시작
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Reveal.js Presentation</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@4.3.1/dist/reveal.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@4.3.1/dist/theme/black.css">
</head>
<body>
<div class="reveal">
<div class="slides">
<section>
<h1>Hello Reveal.js!</h1>
<p>첫 번째 슬라이드입니다.</p>
</section>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/reveal.js@4.3.1/dist/reveal.js"></script>
<script>
Reveal.initialize({
hash: true
});
</script>
</body>
</html>
NPM을 통한 설치
# 새 프로젝트 디렉토리 생성
mkdir my-presentation
cd my-presentation
# package.json 초기화
npm init -y
# Reveal.js 설치
npm install reveal.js
# 개발 서버 설치 (선택사항)
npm install -g http-server
Git 클론을 통한 설치
# 공식 저장소 클론
git clone https://github.com/hakimel/reveal.js.git
cd reveal.js
# 의존성 설치
npm install
# 개발 서버 시작
npm start
2. 프로젝트 구조 설정
my-presentation/
├── index.html # 메인 프레젠테이션 파일
├── css/
│ ├── theme/ # 커스텀 테마
│ │ └── custom.css
│ └── custom.css # 추가 스타일
├── js/
│ ├── plugins/ # 커스텀 플러그인
│ └── custom.js # 추가 스크립트
├── assets/
│ ├── images/ # 이미지 파일
│ ├── videos/ # 비디오 파일
│ └── audio/ # 오디오 파일
├── lib/
│ └── reveal.js/ # Reveal.js 라이브러리
└── package.json
3. 개발 환경 설정
Live Server 설정
# VS Code Live Server 확장 설치
# 또는 http-server 사용
npx http-server -p 8000
# 브라우저에서 http://localhost:8000 접속
Webpack 기반 설정 (고급)
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/,
use: ['file-loader'],
},
],
},
devServer: {
contentBase: './dist',
port: 8080,
},
};
기본 슬라이드 작성
1. HTML 구조 이해
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Reveal.js 기본 구조</title>
<link rel="stylesheet" href="css/reveal.css">
<link rel="stylesheet" href="css/theme/white.css">
</head>
<body>
<div class="reveal">
<div class="slides">
<!-- 여기에 슬라이드들이 들어갑니다 -->
<!-- 첫 번째 슬라이드 -->
<section>
<h1>제목 슬라이드</h1>
<h3>부제목</h3>
<p>발표자: <a href="#">홍길동</a></p>
</section>
<!-- 두 번째 슬라이드 -->
<section>
<h2>목차</h2>
<ul>
<li>주제 1</li>
<li>주제 2</li>
<li>주제 3</li>
</ul>
</section>
</div>
</div>
<script src="js/reveal.js"></script>
<script>
Reveal.initialize();
</script>
</body>
</html>
2. 수직 슬라이드 구조
<!-- 수평 섹션 1 -->
<section>
<h2>주제 개요</h2>
<p>아래로 스크롤하여 상세 내용을 확인하세요</p>
</section>
<!-- 수평 섹션 2 - 수직 슬라이드 포함 -->
<section>
<!-- 상위 슬라이드 -->
<section>
<h2>상세 주제</h2>
<p>이 주제는 여러 하위 섹션으로 구성됩니다</p>
</section>
<!-- 하위 슬라이드 1 -->
<section>
<h3>하위 주제 1</h3>
<p>첫 번째 상세 내용</p>
</section>
<!-- 하위 슬라이드 2 -->
<section>
<h3>하위 주제 2</h3>
<p>두 번째 상세 내용</p>
</section>
</section>
<!-- 수평 섹션 3 -->
<section>
<h2>결론</h2>
<p>마무리 내용</p>
</section>
3. 마크다운 지원
<!-- 마크다운 슬라이드 -->
<section data-markdown>
<textarea data-template>
## 마크다운 슬라이드
* 목록 아이템 1
* 목록 아이템 2
* 목록 아이템 3
---
### 코드 블록
```javascript
function hello() {
console.log('Hello Reveal.js!');
}
```
---
### 이미지

</textarea>
</section>
<!-- 외부 마크다운 파일 로드 -->
<section data-markdown="content/slides.md"
data-separator="^\r?\n---\r?\n$"
data-separator-vertical="^\r?\n--\r?\n$">
</section>
고급 기능 활용
1. Auto-Animate 기능
<!-- 첫 번째 슬라이드 -->
<section data-auto-animate>
<h1>Auto-Animate</h1>
<div class="box" style="background: red; width: 50px; height: 50px;"></div>
</section>
<!-- 두 번째 슬라이드 - 자동 애니메이션 -->
<section data-auto-animate>
<h1 style="color: blue;">Auto-Animate</h1>
<div class="box" style="background: blue; width: 100px; height: 100px; transform: translateX(200px);"></div>
</section>
<!-- 코드 애니메이션 예시 -->
<section data-auto-animate>
<pre data-id="code-animation"><code data-trim data-line-numbers>
function example() {
console.log('Hello');
}
</code></pre>
</section>
<section data-auto-animate>
<pre data-id="code-animation"><code data-trim data-line-numbers="1-5|7-9">
function example() {
console.log('Hello');
console.log('World');
}
// 새로운 함수 추가
function newFunction() {
return 'Amazing!';
}
</code></pre>
</section>
2. 프래그먼트 (Fragments)
<section>
<h2>단계별 공개</h2>
<p class="fragment">첫 번째로 나타납니다</p>
<p class="fragment fade-in">페이드 인 효과</p>
<p class="fragment fade-out">페이드 아웃 효과</p>
<p class="fragment highlight-red">빨간색 하이라이트</p>
<p class="fragment highlight-blue">파란색 하이라이트</p>
<p class="fragment grow">크기 증가</p>
<p class="fragment shrink">크기 감소</p>
<p class="fragment strike">취소선</p>
<!-- 순서 지정 -->
<p class="fragment" data-fragment-index="3">세 번째로 나타남</p>
<p class="fragment" data-fragment-index="1">첫 번째로 나타남</p>
<p class="fragment" data-fragment-index="2">두 번째로 나타남</p>
</section>
<!-- 커스텀 프래그먼트 애니메이션 -->
<section>
<style>
.custom-fragment {
opacity: 0;
transform: scale(0.5) rotate(180deg);
transition: all 0.8s ease;
}
.custom-fragment.visible {
opacity: 1;
transform: scale(1) rotate(0deg);
}
</style>
<div class="fragment custom-fragment">
커스텀 애니메이션 요소
</div>
</section>
3. 백그라운드 및 미디어
<!-- 색상 배경 -->
<section data-background-color="#ff0000">
<h2>빨간 배경</h2>
</section>
<!-- 그라데이션 배경 -->
<section data-background-gradient="linear-gradient(to bottom, #283048, #859398)">
<h2>그라데이션 배경</h2>
</section>
<!-- 이미지 배경 -->
<section data-background-image="assets/images/background.jpg">
<h2>이미지 배경</h2>
</section>
<!-- 이미지 배경 옵션 -->
<section data-background-image="assets/images/pattern.png"
data-background-repeat="repeat"
data-background-size="300px">
<h2>패턴 배경</h2>
</section>
<!-- 비디오 배경 -->
<section data-background-video="assets/videos/background.mp4"
data-background-video-loop
data-background-video-muted>
<h2>비디오 배경</h2>
</section>
<!-- iframe 배경 -->
<section data-background-iframe="https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1&controls=0&showinfo=0&autohide=1">
<h2>YouTube 배경</h2>
</section>
4. 스피커 노트
<section>
<h2>발표 슬라이드</h2>
<p>청중에게 보이는 내용</p>
<aside class="notes">
이것은 스피커 노트입니다.
- 핵심 포인트 1
- 핵심 포인트 2
- 질문에 대한 답변 준비
- 시간 배분: 5분
</aside>
</section>
<!-- 마크다운 스피커 노트 -->
<section data-markdown>
<textarea data-template>
## 슬라이드 제목
내용...
Note:
이것은 마크다운 스피커 노트입니다.
- 포인트 1
- 포인트 2
</textarea>
</section>
테마 및 스타일링
1. 내장 테마 사용
<!-- 다양한 내장 테마 -->
<link rel="stylesheet" href="css/theme/black.css">
<link rel="stylesheet" href="css/theme/white.css">
<link rel="stylesheet" href="css/theme/league.css">
<link rel="stylesheet" href="css/theme/sky.css">
<link rel="stylesheet" href="css/theme/beige.css">
<link rel="stylesheet" href="css/theme/simple.css">
<link rel="stylesheet" href="css/theme/serif.css">
<link rel="stylesheet" href="css/theme/blood.css">
<link rel="stylesheet" href="css/theme/night.css">
<link rel="stylesheet" href="css/theme/moon.css">
<link rel="stylesheet" href="css/theme/solarized.css">
2. 커스텀 테마 제작
/* css/theme/custom.css */
/* 기본 변수 설정 */
:root {
--main-font: 'Helvetica Neue', Helvetica, Arial, sans-serif;
--heading-font: 'Montserrat', sans-serif;
--code-font: 'Fira Code', monospace;
--background-color: #2c3e50;
--main-color: #ecf0f1;
--heading-color: #3498db;
--link-color: #e74c3c;
--selection-background-color: #34495e;
}
/* 기본 스타일 */
.reveal {
font-family: var(--main-font);
font-size: 40px;
font-weight: normal;
color: var(--main-color);
background: var(--background-color);
}
.reveal .slides section {
text-align: left;
font-weight: inherit;
}
/* 제목 스타일 */
.reveal h1,
.reveal h2,
.reveal h3,
.reveal h4,
.reveal h5,
.reveal h6 {
margin: 0 0 20px 0;
color: var(--heading-color);
font-family: var(--heading-font);
font-weight: 600;
line-height: 1.2;
letter-spacing: normal;
text-transform: none;
text-shadow: none;
word-wrap: break-word;
}
.reveal h1 { font-size: 2.5em; }
.reveal h2 { font-size: 1.8em; }
.reveal h3 { font-size: 1.3em; }
/* 링크 스타일 */
.reveal a {
color: var(--link-color);
text-decoration: none;
transition: color 0.15s ease;
}
.reveal a:hover {
color: #c0392b;
text-shadow: none;
border: none;
}
/* 코드 스타일 */
.reveal pre {
display: block;
position: relative;
width: 90%;
margin: 20px auto;
text-align: left;
font-size: 0.55em;
font-family: var(--code-font);
line-height: 1.2em;
word-wrap: break-word;
box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.15);
}
.reveal code {
font-family: var(--code-font);
text-transform: none;
background: #34495e;
padding: 2px 6px;
border-radius: 3px;
}
/* 커스텀 클래스 */
.reveal .highlight {
background: #f39c12;
color: #2c3e50;
padding: 0 10px;
border-radius: 5px;
}
.reveal .text-center { text-align: center; }
.reveal .text-right { text-align: right; }
.reveal .big { font-size: 1.5em; }
.reveal .small { font-size: 0.7em; }
/* 애니메이션 */
.reveal .fade-up {
transform: translateY(20px);
opacity: 0;
transition: all 0.8s ease;
}
.reveal .fade-up.visible {
transform: translateY(0);
opacity: 1;
}
3. 반응형 디자인
/* 반응형 미디어 쿼리 */
@media screen and (max-width: 768px) {
.reveal {
font-size: 28px;
}
.reveal h1 { font-size: 2em; }
.reveal h2 { font-size: 1.5em; }
.reveal h3 { font-size: 1.2em; }
.reveal .slides section {
padding: 20px;
}
}
@media screen and (max-width: 480px) {
.reveal {
font-size: 24px;
}
.reveal pre {
font-size: 0.45em;
}
.reveal .controls {
bottom: 20px;
}
}
/* 인쇄용 스타일 */
@media print {
.reveal {
background: white;
color: black;
}
.reveal h1,
.reveal h2,
.reveal h3 {
color: black;
}
.reveal .progress,
.reveal .controls {
display: none;
}
}
플러그인 및 확장
1. 내장 플러그인 활용
// Reveal.js 초기화 및 플러그인 설정
Reveal.initialize({
// 기본 설정
hash: true,
controls: true,
progress: true,
center: true,
transition: 'slide',
// 내장 플러그인 로드
plugins: [
RevealMarkdown, // 마크다운 지원
RevealHighlight, // 코드 하이라이팅
RevealNotes, // 스피커 노트
RevealMath, // 수학 수식
RevealSearch, // 검색 기능
RevealZoom // 줌 기능
],
// 플러그인별 설정
markdown: {
smartypants: true
},
highlight: {
highlightOnLoad: true
},
math: {
mathjax: 'https://cdn.jsdelivr.net/gh/mathjax/mathjax@2.7.8/MathJax.js',
config: 'TeX-AMS_HTML-full'
}
});
2. 커스텀 플러그인 개발
// 커스텀 플러그인: 자동 진행
const AutoProgress = () => {
let intervalId;
let isAutoPlaying = false;
let duration = 5000; // 5초
const start = () => {
if (isAutoPlaying) return;
isAutoPlaying = true;
intervalId = setInterval(() => {
if (!Reveal.isLastSlide()) {
Reveal.next();
} else {
stop();
}
}, duration);
console.log('자동 진행 시작');
};
const stop = () => {
if (!isAutoPlaying) return;
clearInterval(intervalId);
isAutoPlaying = false;
console.log('자동 진행 중지');
};
const toggle = () => {
if (isAutoPlaying) {
stop();
} else {
start();
}
};
// 키보드 이벤트 리스너
document.addEventListener('keydown', (event) => {
if (event.key === 'p' || event.key === 'P') {
toggle();
}
});
return {
id: 'autoprogress',
init: () => {
console.log('AutoProgress 플러그인 초기화됨');
// 컨트롤 버튼 추가
const button = document.createElement('button');
button.innerHTML = '⏯️';
button.style.position = 'fixed';
button.style.top = '20px';
button.style.right = '20px';
button.style.zIndex = '1000';
button.onclick = toggle;
document.body.appendChild(button);
}
};
};
// 플러그인 등록
Reveal.initialize({
plugins: [AutoProgress()]
});
3. 고급 인터랙션 플러그인
// 실시간 폴링 플러그인
const LivePolling = () => {
let socket;
let currentPoll = null;
const createPollSlide = (question, options) => {
return `
<section class="poll-slide">
<h2>${question}</h2>
<div class="poll-options">
${options.map((option, index) =>
`<button class="poll-option" data-option="${index}">
${option}
</button>`
).join('')}
</div>
<div class="poll-results" style="display: none;">
<canvas id="poll-chart"></canvas>
</div>
</section>
`;
};
const showResults = (results) => {
const resultsDiv = document.querySelector('.poll-results');
const optionsDiv = document.querySelector('.poll-options');
if (resultsDiv && optionsDiv) {
optionsDiv.style.display = 'none';
resultsDiv.style.display = 'block';
// Chart.js를 사용한 결과 시각화
const ctx = document.getElementById('poll-chart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: currentPoll.options,
datasets: [{
data: results,
backgroundColor: ['#ff6384', '#36a2eb', '#cc65fe', '#ffce56']
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false }
}
}
});
}
};
return {
id: 'livepolling',
init: () => {
// WebSocket 연결
socket = new WebSocket('ws://localhost:8080');
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'poll_results') {
showResults(data.results);
}
};
// 폴링 옵션 클릭 이벤트
document.addEventListener('click', (event) => {
if (event.target.classList.contains('poll-option')) {
const option = event.target.dataset.option;
socket.send(JSON.stringify({
type: 'poll_vote',
option: parseInt(option)
}));
}
});
}
};
};
실전 프로젝트: 종합 프레젠테이션
1. 기술 발표용 프레젠테이션
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>React 18의 새로운 기능</title>
<link rel="stylesheet" href="css/reveal.css">
<link rel="stylesheet" href="css/theme/custom-tech.css">
</head>
<body>
<div class="reveal">
<div class="slides">
<!-- 타이틀 슬라이드 -->
<section data-background-gradient="linear-gradient(45deg, #1e3c72, #2a5298)">
<h1 class="title-main">React 18</h1>
<h2 class="title-sub">새로운 기능과 성능 개선</h2>
<p class="speaker-info">
<strong>발표자:</strong> 홍길동<br>
<strong>일시:</strong> 2025년 8월 3일
</p>
<aside class="notes">
인사말과 발표 개요 소개
- 발표 시간: 30분
- Q&A: 10분
</aside>
</section>
<!-- 목차 -->
<section>
<h2>발표 목차</h2>
<ol class="toc">
<li class="fragment">React 18 개요</li>
<li class="fragment">Concurrent Features</li>
<li class="fragment">자동 배칭</li>
<li class="fragment">Suspense 개선사항</li>
<li class="fragment">실전 예제</li>
<li class="fragment">마이그레이션 가이드</li>
</ol>
</section>
<!-- React 18 개요 -->
<section>
<section>
<h2>React 18 개요</h2>
<div class="stats-grid">
<div class="stat-item fragment">
<h3>출시일</h3>
<p>2022년 3월</p>
</div>
<div class="stat-item fragment">
<h3>주요 특징</h3>
<p>Concurrent Rendering</p>
</div>
<div class="stat-item fragment">
<h3>성능</h3>
<p>최대 30% 향상</p>
</div>
</div>
</section>
<section>
<h3>주요 변경사항</h3>
<ul>
<li class="fragment highlight-blue">새로운 root API</li>
<li class="fragment highlight-green">자동 배칭</li>
<li class="fragment highlight-red">Concurrent Features</li>
<li class="fragment highlight-yellow">Strict Mode 개선</li>
</ul>
</section>
</section>
<!-- Concurrent Features -->
<section>
<section data-auto-animate>
<h2>Concurrent Features</h2>
<div class="code-container">
<pre data-id="concurrent-code"><code data-trim data-line-numbers>
// React 17
ReactDOM.render(<App />, container);
</code></pre>
</div>
</section>
<section data-auto-animate>
<h2>Concurrent Features</h2>
<div class="code-container">
<pre data-id="concurrent-code"><code data-trim data-line-numbers="1-5">
// React 18
import { createRoot } from 'react-dom/client';
const root = createRoot(container);
root.render(<App />);
</code></pre>
</div>
<p class="fragment">새로운 root API로 Concurrent Features 활성화</p>
</section>
</section>
<!-- 자동 배칭 -->
<section>
<h2>자동 배칭 (Automatic Batching)</h2>
<div class="comparison-grid">
<div class="before">
<h3>React 17 이전</h3>
<pre><code data-trim>
// setTimeout 내부에서는 배칭 안됨
setTimeout(() => {
setCount(1); // 리렌더링
setFlag(true); // 리렌더링
}, 0);
</code></pre>
</div>
<div class="after">
<h3>React 18</h3>
<pre><code data-trim>
// 모든 경우에 자동 배칭
setTimeout(() => {
setCount(1); // 배칭됨
setFlag(true); // 한 번만 리렌더링
}, 0);
</code></pre>
</div>
</div>
</section>
<!-- 실시간 데이터 시각화 -->
<section data-background-iframe="performance-demo.html">
<div class="overlay-content">
<h2>성능 비교 데모</h2>
<p>실시간 성능 측정 결과</p>
</div>
<aside class="notes">
라이브 데모 설명
- React 17 vs 18 성능 차이
- 실제 애플리케이션에서의 개선 효과
</aside>
</section>
<!-- Q&A -->
<section data-background-color="#2c3e50">
<h1>질문과 답변</h1>
<div class="qr-code">
<img src="assets/images/qr-feedback.png" alt="피드백 QR 코드">
<p>피드백을 남겨주세요</p>
</div>
</section>
</div>
</div>
<!-- 커스텀 CSS -->
<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
margin: 2rem 0;
}
.stat-item {
text-align: center;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.comparison-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin: 2rem 0;
}
.before, .after {
padding: 1rem;
border-radius: 10px;
}
.before { background: rgba(231, 76, 60, 0.2); }
.after { background: rgba(46, 204, 113, 0.2); }
.overlay-content {
background: rgba(0, 0, 0, 0.8);
padding: 2rem;
border-radius: 10px;
color: white;
}
.qr-code {
text-align: center;
margin-top: 2rem;
}
.qr-code img {
width: 200px;
height: 200px;
}
</style>
<script src="js/reveal.js"></script>
<script src="js/plugins/markdown.js"></script>
<script src="js/plugins/highlight.js"></script>
<script src="js/plugins/notes.js"></script>
<script>
Reveal.initialize({
hash: true,
plugins: [RevealMarkdown, RevealHighlight, RevealNotes],
// 발표 설정
controls: true,
progress: true,
center: true,
transition: 'slide',
transitionSpeed: 'default',
// 키보드 단축키
keyboard: {
32: 'next', // 스페이스바로 다음 슬라이드
37: 'prev', // 왼쪽 화살표
39: 'next' // 오른쪽 화살표
}
});
// 발표 시간 추적
let startTime = Date.now();
let timeDisplay = document.createElement('div');
timeDisplay.style.position = 'fixed';
timeDisplay.style.top = '10px';
timeDisplay.style.left = '10px';
timeDisplay.style.background = 'rgba(0,0,0,0.7)';
timeDisplay.style.color = 'white';
timeDisplay.style.padding = '5px 10px';
timeDisplay.style.borderRadius = '5px';
timeDisplay.style.fontSize = '14px';
timeDisplay.style.zIndex = '1000';
document.body.appendChild(timeDisplay);
setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
timeDisplay.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
}, 1000);
</script>
</body>
</html>
2. 교육용 인터랙티브 프레젠테이션
<!-- 수학 수업용 프레젠테이션 -->
<section data-background-color="#f8f9fa">
<h2>이차방정식의 해</h2>
<!-- MathJax를 사용한 수식 -->
<div class="math-container">
<p>일반형: $ax^2 + bx + c = 0$ (단, $a \neq 0$)</p>
<div class="fragment">
<p>해의 공식:</p>
<p>$$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$</p>
</div>
</div>
<!-- 인터랙티브 계산기 -->
<div class="calculator fragment">
<h3>직접 계산해보기</h3>
<div class="input-group">
<label>a: <input type="number" id="coeff-a" value="1"></label>
<label>b: <input type="number" id="coeff-b" value="0"></label>
<label>c: <input type="number" id="coeff-c" value="-1"></label>
<button onclick="calculateRoots()">계산하기</button>
</div>
<div id="result"></div>
</div>
<!-- 그래프 시각화 -->
<div class="graph-container fragment">
<canvas id="parabola-graph" width="400" height="300"></canvas>
</div>
<script>
function calculateRoots() {
const a = parseFloat(document.getElementById('coeff-a').value);
const b = parseFloat(document.getElementById('coeff-b').value);
const c = parseFloat(document.getElementById('coeff-c').value);
if (a === 0) {
document.getElementById('result').innerHTML = '계수 a는 0이 될 수 없습니다.';
return;
}
const discriminant = b * b - 4 * a * c;
let result;
if (discriminant > 0) {
const x1 = (-b + Math.sqrt(discriminant)) / (2 * a);
const x2 = (-b - Math.sqrt(discriminant)) / (2 * a);
result = `두 개의 실근: x₁ = ${x1.toFixed(3)}, x₂ = ${x2.toFixed(3)}`;
} else if (discriminant === 0) {
const x = -b / (2 * a);
result = `중근: x = ${x.toFixed(3)}`;
} else {
const realPart = (-b / (2 * a)).toFixed(3);
const imagPart = (Math.sqrt(-discriminant) / (2 * a)).toFixed(3);
result = `복소수근: x = ${realPart} ± ${imagPart}i`;
}
document.getElementById('result').innerHTML = result;
drawParabola(a, b, c);
}
function drawParabola(a, b, c) {
const canvas = document.getElementById('parabola-graph');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 좌표계 설정
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const scale = 20;
// 축 그리기
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
// x축
ctx.beginPath();
ctx.moveTo(0, centerY);
ctx.lineTo(canvas.width, centerY);
ctx.stroke();
// y축
ctx.beginPath();
ctx.moveTo(centerX, 0);
ctx.lineTo(centerX, canvas.height);
ctx.stroke();
// 포물선 그리기
ctx.strokeStyle = '#007bff';
ctx.lineWidth = 2;
ctx.beginPath();
for (let px = 0; px < canvas.width; px++) {
const x = (px - centerX) / scale;
const y = a * x * x + b * x + c;
const py = centerY - y * scale;
if (px === 0) {
ctx.moveTo(px, py);
} else {
ctx.lineTo(px, py);
}
}
ctx.stroke();
}
// 초기 그래프 그리기
setTimeout(calculateRoots, 500);
</script>
<style>
.math-container {
margin: 2rem 0;
font-size: 1.2em;
}
.calculator {
background: #e9ecef;
padding: 1.5rem;
border-radius: 10px;
margin: 1rem 0;
}
.input-group {
display: flex;
gap: 1rem;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
}
.input-group label {
display: flex;
align-items: center;
gap: 0.5rem;
}
.input-group input {
width: 80px;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 5px;
}
.input-group button {
padding: 0.5rem 1rem;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#result {
font-weight: bold;
color: #28a745;
margin-top: 1rem;
}
.graph-container {
text-align: center;
margin-top: 2rem;
}
#parabola-graph {
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
</section>
3. 마케팅 프레젠테이션
<!-- 제품 소개 프레젠테이션 -->
<section data-background-video="assets/videos/product-intro.mp4"
data-background-video-loop
data-background-video-muted>
<div class="hero-content">
<h1 class="hero-title">혁신적인 새 제품</h1>
<p class="hero-subtitle">세상을 바꿀 기술</p>
<button class="cta-button fragment" onclick="showProductDetails()">
자세히 알아보기
</button>
</div>
</section>
<!-- 제품 특징 -->
<section>
<h2>핵심 특징</h2>
<div class="features-grid">
<div class="feature-card fragment" data-fragment-index="1">
<div class="feature-icon">🚀</div>
<h3>초고속 성능</h3>
<p>기존 대비 10배 빠른 처리 속도</p>
<div class="metric">
<span class="number" data-target="1000">0</span>
<span class="unit">ops/sec</span>
</div>
</div>
<div class="feature-card fragment" data-fragment-index="2">
<div class="feature-icon">🔒</div>
<h3>완벽한 보안</h3>
<p>군사급 암호화 기술 적용</p>
<div class="metric">
<span class="number" data-target="99.99">0</span>
<span class="unit">% 보안성</span>
</div>
</div>
<div class="feature-card fragment" data-fragment-index="3">
<div class="feature-icon">💚</div>
<h3>친환경</h3>
<p>50% 적은 에너지 소비</p>
<div class="metric">
<span class="number" data-target="50">0</span>
<span class="unit">% 절약</span>
</div>
</div>
</div>
</section>
<!-- 고객 후기 -->
<section data-background-color="#f8f9fa">
<h2>고객 후기</h2>
<div class="testimonial-slider">
<div class="testimonial active">
<blockquote>
"정말 놀라운 제품입니다. 우리 회사의 생산성이 200% 증가했어요!"
</blockquote>
<div class="customer">
<img src="assets/images/customer1.jpg" alt="김OO 고객">
<div class="customer-info">
<h4>김○○</h4>
<p>ABC 회사 CTO</p>
</div>
</div>
</div>
</div>
<!-- 자동 슬라이더 -->
<script>
const testimonials = [
{
quote: "정말 놀라운 제품입니다. 우리 회사의 생산성이 200% 증가했어요!",
name: "김○○",
title: "ABC 회사 CTO",
image: "assets/images/customer1.jpg"
},
{
quote: "사용하기 쉽고 효과적입니다. 강력히 추천합니다!",
name: "이○○",
title: "XYZ 스타트업 CEO",
image: "assets/images/customer2.jpg"
},
{
quote: "투자 대비 효과가 놀라웠습니다. ROI 500% 달성!",
name: "박○○",
title: "DEF 그룹 CFO",
image: "assets/images/customer3.jpg"
}
];
let currentTestimonial = 0;
function updateTestimonial() {
const testimonial = testimonials[currentTestimonial];
document.querySelector('.testimonial blockquote').textContent = testimonial.quote;
document.querySelector('.customer-info h4').textContent = testimonial.name;
document.querySelector('.customer-info p').textContent = testimonial.title;
document.querySelector('.customer img').src = testimonial.image;
currentTestimonial = (currentTestimonial + 1) % testimonials.length;
}
// 5초마다 후기 변경
setInterval(updateTestimonial, 5000);
</script>
</section>
<!-- 가격 및 연락처 -->
<section data-background-gradient="linear-gradient(135deg, #667eea 0%, #764ba2 100%)">
<div class="pricing-section">
<h2>특별 런칭 가격</h2>
<div class="price-card">
<div class="original-price">₩99,000</div>
<div class="discount-price">₩49,000</div>
<div class="discount-badge">50% 할인</div>
<ul class="features-list">
<li>✅ 모든 기능 포함</li>
<li>✅ 1년 무료 업데이트</li>
<li>✅ 24/7 고객 지원</li>
<li>✅ 30일 환불 보장</li>
</ul>
<button class="order-button" onclick="openOrderForm()">
지금 주문하기
</button>
<p class="limited-time">🔥 한정 시간 특가 (72시간 남음)</p>
</div>
</div>
<!-- 카운트다운 타이머 -->
<div class="countdown-timer">
<div class="time-unit">
<span class="time-value" id="hours">00</span>
<span class="time-label">시간</span>
</div>
<div class="time-unit">
<span class="time-value" id="minutes">00</span>
<span class="time-label">분</span>
</div>
<div class="time-unit">
<span class="time-value" id="seconds">00</span>
<span class="time-label">초</span>
</div>
</div>
</section>
<style>
/* 스타일 정의 생략 - 실제로는 포함됨 */
.hero-content {
text-align: center;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin: 2rem 0;
}
.feature-card {
background: white;
padding: 2rem;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
text-align: center;
}
.countdown-timer {
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 2rem;
}
.time-unit {
text-align: center;
color: white;
}
.time-value {
display: block;
font-size: 3em;
font-weight: bold;
}
</style>
<script>
// 숫자 애니메이션
function animateNumbers() {
const numbers = document.querySelectorAll('.number');
numbers.forEach(number => {
const target = parseInt(number.dataset.target);
const duration = 2000;
const step = target / (duration / 16);
let current = 0;
const timer = setInterval(() => {
current += step;
if (current >= target) {
current = target;
clearInterval(timer);
}
number.textContent = Math.floor(current);
}, 16);
});
}
// 카운트다운 타이머
function startCountdown() {
const endTime = new Date();
endTime.setHours(endTime.getHours() + 72);
function updateTimer() {
const now = new Date();
const timeLeft = endTime - now;
if (timeLeft <= 0) {
document.getElementById('hours').textContent = '00';
document.getElementById('minutes').textContent = '00';
document.getElementById('seconds').textContent = '00';
return;
}
const hours = Math.floor(timeLeft / (1000 * 60 * 60));
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
document.getElementById('hours').textContent = hours.toString().padStart(2, '0');
document.getElementById('minutes').textContent = minutes.toString().padStart(2, '0');
document.getElementById('seconds').textContent = seconds.toString().padStart(2, '0');
}
updateTimer();
setInterval(updateTimer, 1000);
}
// 슬라이드 이벤트 리스너
Reveal.addEventListener('fragmentshown', function(event) {
if (event.fragment.classList.contains('feature-card')) {
setTimeout(animateNumbers, 300);
}
});
// 페이지 로드 시 카운트다운 시작
document.addEventListener('DOMContentLoaded', startCountdown);
</script>
배포 및 공유
1. 정적 웹사이트 호스팅
# GitHub Pages 배포
# 1. GitHub 저장소 생성
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/username/presentation.git
git push -u origin main
# 2. GitHub Pages 설정
# 저장소 Settings > Pages > Source: Deploy from a branch > main
# 3. 커스텀 도메인 설정 (선택사항)
echo "presentation.yourdomain.com" > CNAME
git add CNAME
git commit -m "Add custom domain"
git push
Netlify 배포
# Netlify CLI 설치
npm install -g netlify-cli
# 로그인
netlify login
# 배포
netlify deploy --prod --dir=./
# 또는 Git 연동을 통한 자동 배포
# netlify.toml 파일 생성
# netlify.toml
[build]
publish = "."
command = "echo 'No build command needed'"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
[[redirects]]
from = "/slides/*"
to = "/index.html"
status = 200
2. PDF 내보내기
// PDF 내보내기 설정
Reveal.initialize({
// PDF 전용 설정
pdf: true,
// PDF에서 제외할 요소들
pdfMaxPagesPerSlide: 1,
pdfSeparateFragments: false,
plugins: [RevealNotes, RevealHighlight]
});
크롬 브라우저를 통한 PDF 생성
# URL에 ?print-pdf 파라미터 추가
http://localhost:8000/?print-pdf
# 브라우저 인쇄 설정
# - 대상: PDF로 저장
# - 페이지: 모두
# - 레이아웃: 가로
# - 여백: 없음
# - 배경 그래픽: 체크
Puppeteer를 이용한 자동 PDF 생성
// generate-pdf.js
const puppeteer = require('puppeteer');
const path = require('path');
async function generatePDF() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 프레젠테이션 페이지 로드
await page.goto('http://localhost:8000/?print-pdf', {
waitUntil: 'networkidle0'
});
// PDF 생성
await page.pdf({
path: 'presentation.pdf',
width: '1920px',
height: '1080px',
printBackground: true,
landscape: true,
margin: {
top: '0px',
right: '0px',
bottom: '0px',
left: '0px'
}
});
await browser.close();
console.log('PDF 생성 완료: presentation.pdf');
}
generatePDF().catch(console.error);
3. 고급 배포 설정
Docker를 이용한 배포
# Dockerfile
FROM nginx:alpine
# 프레젠테이션 파일 복사
COPY . /usr/share/nginx/html
# nginx 설정
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
gzip on;
gzip_types text/css application/javascript image/svg+xml;
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# 캐시 설정
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
}
# Docker 이미지 빌드 및 실행
docker build -t my-presentation .
docker run -p 8080:80 my-presentation
성능 최적화 및 팁
1. 로딩 성능 최적화
// 지연 로딩 설정
Reveal.initialize({
// 필요한 플러그인만 로드
dependencies: [
{
src: 'plugin/markdown/marked.js',
condition: function() {
return !!document.querySelector('[data-markdown]');
}
},
{
src: 'plugin/math/math.js',
condition: function() {
return !!document.querySelector('[data-math]');
}
}
],
// 프리로딩 설정
preloadIframes: true,
// 트랜지션 최적화
transition: 'slide',
transitionSpeed: 'fast'
});
이미지 최적화
<!-- WebP 포맷 사용 -->
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.jpg" type="image/jpeg">
<img src="image.jpg" alt="설명">
</picture>
<!-- 지연 로딩 -->
<img src="placeholder.jpg"
data-src="large-image.jpg"
class="lazy-load"
alt="설명">
<script>
// Intersection Observer를 이용한 지연 로딩
const lazyImages = document.querySelectorAll('.lazy-load');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy-load');
observer.unobserve(img);
}
});
});
lazyImages.forEach(img => imageObserver.observe(img));
</script>
2. 접근성 개선
// 접근성 설정
Reveal.initialize({
// 키보드 네비게이션
keyboard: {
27: null, // ESC 키 비활성화
13: 'next', // Enter 키로 다음 슬라이드
32: 'next' // 스페이스바로 다음 슬라이드
},
// 포커스 관리
focusBodyOnPageVisiblityChange: true,
// 스크린 리더 지원
announceControlState: true
});
<!-- ARIA 라벨 추가 -->
<section aria-label="소개 슬라이드">
<h1>제목</h1>
<p>내용...</p>
</section>
<!-- Skip 링크 -->
<a href="#main-content" class="skip-link">메인 콘텐츠로 건너뛰기</a>
<!-- 대체 텍스트 -->
<img src="chart.png" alt="2024년 매출 증가 추이를 보여주는 막대 그래프">
<style>
.skip-link {
position: absolute;
top: -40px;
left: 6px;
background: #000;
color: #fff;
padding: 8px;
text-decoration: none;
z-index: 9999;
}
.skip-link:focus {
top: 6px;
}
/* 고대비 모드 지원 */
@media (prefers-contrast: high) {
.reveal {
background: #000;
color: #fff;
}
.reveal h1, .reveal h2, .reveal h3 {
color: #fff;
}
}
/* 모션 감소 설정 */
@media (prefers-reduced-motion: reduce) {
.reveal .slides section {
transition: none !important;
}
.fragment {
transition: none !important;
}
}
</style>
3. 모바일 최적화
/* 모바일 터치 개선 */
.reveal .controls {
font-size: 24px;
bottom: 20px;
}
.reveal .progress {
height: 6px;
}
/* 모바일 텍스트 크기 */
@media screen and (max-width: 768px) {
.reveal {
font-size: 32px;
}
.reveal h1 { font-size: 2em; }
.reveal h2 { font-size: 1.6em; }
.reveal h3 { font-size: 1.3em; }
/* 터치 친화적인 버튼 크기 */
.reveal button {
min-height: 44px;
min-width: 44px;
padding: 12px 16px;
}
}
/* 가로 모드 최적화 */
@media screen and (orientation: landscape) and (max-height: 500px) {
.reveal {
font-size: 28px;
}
.reveal .slides > section {
padding: 20px;
}
}
// 터치 제스처 개선
Reveal.initialize({
// 터치 설정
touch: true,
touchSwipe: true,
// 모바일에서 줌 비활성화
zoomKey: 'ctrl',
// 가속도계 지원 (실험적)
mouseWheel: false
});
// 디바이스 감지 및 최적화
if (/Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
document.body.classList.add('mobile-device');
// 모바일에서 자동재생 비디오 처리
const videos = document.querySelectorAll('video[autoplay]');
videos.forEach(video => {
video.muted = true;
video.playsInline = true;
});
}
결론
Reveal.js는 단순한 프레젠테이션 도구를 넘어 웹 기술의 모든 가능성을 활용할 수 있는 강력한 플랫폼입니다. HTML, CSS, JavaScript의 유연성과 결합하여 기존 슬라이드 도구로는 불가능했던 인터랙티브하고 매력적인 프레젠테이션을 만들 수 있습니다.
핵심 장점 요약
- 웹 네이티브: 브라우저에서 실행되는 완전한 웹 애플리케이션
- 무한한 확장성: 플러그인과 커스텀 개발을 통한 기능 확장
- 반응형 디자인: 모든 디바이스에서 완벽한 표시
- 개발자 친화적: Git 버전 관리 및 협업 가능
- 비용 효율적: 오픈소스로 무료 사용 가능
실무 활용 팁
- 기획 단계: 스토리보드와 와이어프레임 먼저 작성
- 개발 단계: 점진적 개선과 반복 테스트
- 발표 단계: 스피커 노트와 백업 계획 준비
- 유지보수: 정기적인 업데이트와 성능 모니터링
다음 단계
- 고급 기능 탐색: WebGL, WebXR 등 최신 웹 기술 활용
- AI 통합: ChatGPT API를 활용한 실시간 Q&A 시스템
- 실시간 협업: WebRTC를 이용한 멀티유저 프레젠테이션
- 데이터 시각화: D3.js, Chart.js 등과의 통합
Reveal.js로 여러분만의 독창적이고 효과적인 프레젠테이션을 만들어 청중들에게 잊을 수 없는 경험을 선사하세요.
추가 리소스
- 공식 웹사이트: revealjs.com
- GitHub 저장소: github.com/hakimel/reveal.js
- 플러그인 모음: github.com/reveal.js-plugins
- 커뮤니티: Reveal.js Discord 서버 및 GitHub Discussions
- 비디오 강의: Mastering reveal.js 공식 코스