마크다운쇼 개발기 3편: 배포
마크다운쇼 개발기 3편: 배포
마크다운쇼 개발기 2편: 개발을 마치고 세상에 공개하는 시간. 예상치 못한 이슈들과의 싸움.
Vercel 배포는 쉽다 (원래는)
npm i -g vercel
vercel
끝. GitHub 저장소 연결하면 푸시할 때마다 자동 배포된다.
…라고 생각했다.
첫 번째 이슈: 빌드 실패
로컬에서 npm run build 잘 되는데 Vercel에서 터졌다.
Error: Cannot find module '@marp-team/marp-core'
원인
package.json에서 dependencies와 devDependencies를 구분 안 하고 다 devDependencies에 넣었다.
Vercel은 프로덕션 빌드 시 devDependencies를 설치 안 한다.
해결
npm install @marp-team/marp-core pptxgenjs --save
dependencies로 옮기니까 해결.
두 번째 이슈: Puppeteer의 배신
개발편에서도 언급했지만, 서버 사이드 PDF 생성이 Vercel에서 안 됐다.
Error: spawn ENOEXEC
Chromium 바이너리가 실행이 안 되는 거다.
시도해본 것들:
-
@sparticuz/chromium버전 변경 → 실패 -
puppeteer-core버전 다운그레이드 → 실패 - Vercel 환경변수로 메모리 늘리기 → 실패
결국 서버 사이드 PDF 생성을 포기했다.
최종 해결책
클라이언트에서 브라우저 인쇄 다이얼로그를 띄우는 방식으로 변경.
const printWindow = window.open('', '_blank');
printWindow.document.write(html);
printWindow.print();
오히려 이게 더 깔끔했다:
- 서버 부하 없음
- Marp CSS 100% 적용
- Cold start 걱정 없음
세 번째 이슈: 폰트 깨짐
배포하고 보니 한글이 이상하게 보였다. 기본 폰트로 렌더링되고 있었다.
해결
Google Fonts를 명시적으로 로드.
// export-pdf.ts
const printHTML = `
<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>${css}</style>
</head>
<body>${html}</body>
</html>
`;
네 번째 이슈: 번들 사이즈
첫 배포 후 Lighthouse 돌려봤더니 성능 점수가 낮았다.
First Contentful Paint: 3.2s
Largest Contentful Paint: 4.8s
원인은 모든 라이브러리가 초기 로딩에 포함되고 있었다.
해결: Dynamic Import
PDF, PPTX 내보내기 라이브러리는 버튼 클릭할 때만 로드하도록 변경.
// Before (안 좋음)
import { exportToPDF } from '@/lib/export-pdf';
// After (좋음)
const handleExportPDF = async () => {
const { exportToPDFViaPrint } = await import('@/lib/export-pdf');
await exportToPDFViaPrint(markdown);
};
jsPDF, pptxgenjs 같은 무거운 라이브러리가 초기 번들에서 빠지니까 로딩이 빨라졌다.
배포 결과
Route (app)
┌ ○ / (Static)
├ ○ /_not-found (Static)
└ ƒ /api/export (Dynamic)
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
메인 페이지는 Static으로 빌드되어 CDN에서 서빙된다. 빠르다.
CI/CD 파이프라인
GitHub + Vercel 조합의 장점:
[커밋] → [푸시] → [Vercel 자동 감지] → [빌드] → [배포]
↓
[프리뷰 URL 생성]
PR 열면 프리뷰 환경이 자동으로 만들어진다:
https://marp-editor-git-feature-xxx.vercel.app
코드 리뷰하면서 실제로 돌려볼 수 있어서 편하다.
모니터링
Vercel 대시보드에서 확인 가능한 것들:
- 빌드 로그: 빌드 실패 시 원인 파악
- 함수 로그: API 호출 기록
- 분석: 방문자 수, 페이지 조회
- 성능: Core Web Vitals
무료 플랜인데도 웬만한 건 다 된다.
최종 아키텍처
[사용자] ──────────────────────────────────────────────────
│
▼
┌──────────────┐
│ Vercel │
│ (CDN) │
└──────┬───────┘
│
▼
┌──────────────┐
│ Next.js │
│ Static Page │ ← 서버 사이드 렌더링 없음
└──────┬───────┘
│
▼
┌──────────────────────────────────────────┐
│ 클라이언트 │
├──────────────┬───────────────────────────┤
│ CodeMirror │ Marp Core │
│ (에디터) │ (슬라이드 렌더링) │
├──────────────┼───────────────────────────┤
│ pptxgenjs │ Browser Print API │
│ (PPTX 생성) │ (PDF 생성) │
└──────────────┴───────────────────────────┘
서버 의존성을 최소화했다. 대부분의 로직이 클라이언트에서 돌아간다.
회고
잘한 것
- 서버리스 제약을 빨리 파악함: Puppeteer 안 되는 거 알고 바로 클라이언트로 전환
- Dynamic Import 적극 활용: 초기 로딩 속도 개선
- LocalStorage 자동 저장: 사용자 데이터 보호
아쉬운 것
- Puppeteer 삽질에 시간 낭비: 처음부터 클라이언트 방식 갔으면 3일 아꼈음
- 테스트 코드 없음: 시간에 쫓겨서 테스트 작성 못함
- 접근성 미흡: 키보드 네비게이션이 불완전함
배운 것
- Vercel 서버리스 환경의 제약 (바이너리 실행 제한)
- 브라우저 API만으로도 꽤 많은 게 가능함
- Next.js App Router 사용법
완성!

- GitHub: takjakim/marp_editor
- Live Demo: Vercel 배포 URL
앞으로
MVP는 완성했다. 추가하고 싶은 기능들:
- 협업 기능 (Yjs + WebRTC)
- 클라우드 저장 (Supabase)
- 발표자 노트
- 키보드 단축키 개선
- 모바일 반응형
하나씩 천천히 해볼 예정이다.
마크다운쇼 개발기 시리즈
- 마크다운쇼 개발기 1편: 기획
- 마크다운쇼 개발기 2편: 개발
- 배포 ← 현재 글
읽어주셔서 감사합니다!