⏱️ 예상 읽기 시간: 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!');
        }
        ```
        
        ---
        
        ### 이미지
        
        ![대체 텍스트](assets/images/sample.jpg)
    </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의 유연성과 결합하여 기존 슬라이드 도구로는 불가능했던 인터랙티브하고 매력적인 프레젠테이션을 만들 수 있습니다.

핵심 장점 요약

  1. 웹 네이티브: 브라우저에서 실행되는 완전한 웹 애플리케이션
  2. 무한한 확장성: 플러그인과 커스텀 개발을 통한 기능 확장
  3. 반응형 디자인: 모든 디바이스에서 완벽한 표시
  4. 개발자 친화적: Git 버전 관리 및 협업 가능
  5. 비용 효율적: 오픈소스로 무료 사용 가능

실무 활용 팁

  1. 기획 단계: 스토리보드와 와이어프레임 먼저 작성
  2. 개발 단계: 점진적 개선과 반복 테스트
  3. 발표 단계: 스피커 노트와 백업 계획 준비
  4. 유지보수: 정기적인 업데이트와 성능 모니터링

다음 단계

  1. 고급 기능 탐색: WebGL, WebXR 등 최신 웹 기술 활용
  2. AI 통합: ChatGPT API를 활용한 실시간 Q&A 시스템
  3. 실시간 협업: WebRTC를 이용한 멀티유저 프레젠테이션
  4. 데이터 시각화: D3.js, Chart.js 등과의 통합

Reveal.js로 여러분만의 독창적이고 효과적인 프레젠테이션을 만들어 청중들에게 잊을 수 없는 경험을 선사하세요.

추가 리소스