티스토리 뷰
🎉 지금 바로 사용해보기

https://parkcoool.github.io/notion-export/
⚠️ 주의
토이 프로젝트입니다.
아직 부족한 기능도 많고, 버그도 많이 보입니다.
이러한 부분들은 추후에 차차 보완할 예정이니 양해해주세요 !
💬 기획 배경
노션으로 작성한 문서를 블로그나 웹사이트에 게시할 때, 스타일이 모두 깨져서 일일이 수정하셨던 경험이 있으신가요?
저도 노션으로 작성한 글을 티스토리 블로그로 옮길 때 겪는 이 불편함이 너무 컸습니다.
이미 노션에서는 문서를 HTML로 내보내는 기능을 제공하고 있지만,
H1(제목1)을 원하는 글꼴 크기로, 인용구를 원하는 배경색으로 바꾸는 등,
제가 원하는 대로 문서의 요소별 스타일을 자유롭게 커스터마이징할 수 있는 기능은 없었습니다.
그래서 만들었습니다.
notion-export는 노션에서 내보낸 "Markdown & CSV" 압축 파일을 업로드하기만 하면,
원하는 테마와 커스텀 스타일을 적용하여 단일 HTML 파일로 만들어주는 100% 클라이언트 사이드 웹 앱입니다.
이 글에서는 제가 이 서비스를 만들게 된 이유와 함께,
어떤 기술을 사용해 핵심 기능들을 구현했는지 그 대략적 과정을 공유해 드리고자 합니다.
🚀 notion-export의 핵심 기능
- 서버 없는 100% 클라이언트 앱: 모든 파일 처리가 사용자의 브라우저 내에서 이루어져 빠르고 안전합니다.
- 자유로운 스타일 커스터마이징: 'Minimal', 'Academic' 등 기본 프리셋은 물론, 제목, 인용구, 코드 블럭 등 모든 마크다운 요소를 개별적으로 커스터마이징할 수 있습니다.
- WYSIWYG 스타일 편집: 미리보기 화면에서 스타일을 바꾸고 싶은 요소를 클릭하면, 해당 위치에 바로 팝 오버가 떠서 직관적으로 디자인을 수정할 수 있습니다.
- 단일 HTML 파일로 내보내기: 모든 스타일(인라인 또는 <style> 태그)과 본문이 포함된 단일 html 파일을 생성하여 Tistory 글 쓰기에서 HTML에 그대로 붙여넣기할 수 있습니다.
🛠️ 구현
핵심 기능들이 어떤 기술 스택으로 구현되었는지 코드를 기반으로 조금 더 깊게 살펴보겠습니다.
1. 무거운 ZIP 파일 처리: Web Workers와 JSZip
Notion에서 이미지가 많은 문서를 내보내면 ZIP 파일 용량이 수십 MB에 달할 수 있습니다.
이 무거운 파일을 브라우저(메인 스레드)에서 직접 압축 해제하고 이미지 데이터를 처리하면,
사용자는 앱이 멈추는 경험을 하게 될 수도 있습니다.
이 문제를 해결하기 위해 웹 워커 (Web Worker)를 도입했습니다.
- 사용자가 FileUploader 컴포넌트를 통해 파일을 업로드하면 App.tsx의 handleFileUpload 함수가 실행됩니다.
- 이 함수는 useZipAnalyzer 커스텀 훅의 analyzeZipFile 함수를 호출합니다.
- useZipAnalyzer 훅은 zipAnalyzer.worker.ts라는 별도의 Web Worker 스크립트를 생성합니다.
- 파일 데이터(ArrayBuffer)를 메인 스레드에서 Web Worker 스레드로 전송합니다.
- Web Worker는 메인 스레드와 독립적으로 작동하며, JSZip 라이브러리를 사용해 ZIP 파일의 압축을 풉니다.
- 특히 Notion은 export-xxx.zip 안에 real-content-xxx.zip이 들어있는 중첩 압축 구조를 가질 때가 있는데, worker는 이 파일(.zip)을 감지하고, 내부 ZIP을 다시 loadAsync 하여 실제 콘텐츠에 접근합니다.
- Worker는 .md 파일의 텍스트와 모든 이미지 파일(Base64 데이터로 변환)을 ZipAnalysisResult 객체로 묶어 메인 스레드로 postMessage를 보냅니다.
- App.tsx는 useEffect로 이 result를 감지하고, previewContent 상태를 업데이트하여 PreviewArea 컴포넌트를 리렌더링합니다.
이 구조 덕분에 100MB가 넘는 ZIP 파일을 처리하는 동안에도
사용자는 로딩 UI를 통해 현재 파일이 처리되고 있음을 알 수 있습니다.
2. 실시간 미리보기: markdown-it과 동적 스타일 주입

마크다운 텍스트와 이미지 데이터를 가져오면, 이를 사용자에게 보여줘야 합니다.
PreviewArea 컴포넌트는 useMemo를 사용해 전달받은 content와 images를 HTML로 변환합니다.
이 과정은 markdown.ts 유틸리티가 담당합니다.
- markdown-it: 마크다운 텍스트를 HTML로 1차 변환합니다.
- replaceImagePaths: 1차 변환된 HTML 내의 <img> 태그를 정규식으로 찾습니다. src="image%20folder/my-pic.png" 처럼 로컬 경로로 되어있는 src 속성을 worker가 전달해 준 이미지 맵(ImageMap)을 참조하여 src="data:image/png;base64,..." 형태의 Base64 URL로 즉시 치환합니다.
- fixNotionCallouts: Notion의 콜아웃의 제목 부분은 마크다운으로 변환 시 이모티콘과 텍스트가 분리되어 지저분하게 나옵니다. 이 함수는 <aside> 태그를 찾아 이모티콘과 첫 번째 문단을 <strong> 태그로 묶어 깔끔하게 재조합합니다.
이렇게 완성된 HTML은 dangerouslySetInnerHTML로 렌더링됩니다.
StyleConfig 타입으로 정의된 스타일 상태 객체(customStyles)가 변경될 때마다 styleConverter.ts의 injectCustomStyles 함수가 호출됩니다.
3. 핵심 차별점: 클릭 기반 WYSIWYG 스타일링

단순히 모달을 열어 스타일을 수정하는 것은 직관적이지 않습니다.
저는 사용자가 미리보기에서 직접 요소를 클릭하여 스타일을 수정하길 원했습니다.
PreviewArea 컴포넌트의 useEffect 훅이 이 기능의 핵심입니다.

- 미리보기가 렌더링되면 .prose 영역에 mousemove과 click 이벤트 리스너를 추가합니다.
- mousemove: 마우스가 움직일 때마다 findEditableElement 함수가 마우스 포인터 아래에 있는 요소(H1, P, LI 등)를 감지하고, 해당 요소에 파란색 outline을 실시간으로 그려줍니다.
- click: 사용자가 요소를 클릭하면,
- event.preventDefault()로 링크 클릭 등 기본 동작을 막습니다.
- 클릭된 요소의 getBoundingClientRect()를 호출하여 화면상의 정확한 위치(x, y, width, height)를 가져옵니다.
- window.getComputedStyle(element)을 호출하여 현재 적용된 fontSize, color 등 실제 스타일 값을 읽어옵니다.
- 이 위치 정보와 스타일 정보를 selectedElement 상태에 저장합니다.
- selectedElement 상태가 null이 아니게 되면, StyleEditorPopover 컴포넌트가 렌더링됩니다.
- 이 팝오버는 position: fixed 속성을 가지며, getBoundingClientRect로 얻어온 위치 값을 기반으로 클릭된 요소 바로 옆에 영리하게 배치됩니다.
- 사용자가 팝오버에서 스타일(예: fontSize)을 변경하고 '모든 H1 요소에 적용'을 체크한 뒤 '적용'을 누르면, onApplyToAll 콜백이 호출됩니다.
- 이 콜백은 App.tsx의 handleElementStyleChange를 통해 메인 customStyles 상태를 업데이트하고, 이는 즉시 동적 <style> 태그를 갱신하여 미리보기의 모든 H1 요소에 반영됩니다.
4. 두 가지 방식의 HTML 내보내기
마지막으로, 완성된 문서를 티스토리같은 다른 플랫폼으로 가져갈 수 있도록 옵션을 제공하였습니다.
- 기본 (<style> 태그 사용): 가장 깔끔한 방식입니다. extractStyles 함수가 <head>에 주입된 동적 <style> 태그의 CSS 텍스트를 그대로 가져오고, .prose 영역의 innerHTML을 가져와 표준 HTML 템플릿에 삽입합니다.
- 인라인 스타일 사용 (최고 호환성): 티스토리처럼 <style> 태그를 허용하지 않는 플랫폼을 위한 옵션입니다. 이 옵션을 체크하면, generateFullHtml 함수는 proseElement.cloneNode(true)로 미리보기 DOM을 복제한 뒤, querySelectorAll('*')로 모든 하위 요소를 순회합니다. 그리고 각 요소마다 window.getComputedStyle(el)을 실행해 최종 계산된 스타일 값을 가져와, el.setAttribute('style', 'font-size: 16px; color: #333; ...')처럼 모든 스타일을 인라인으로 구워버립니다.
그런데 글 쓰면서 알게된 건데, <style> 태그를 사용한 HTML 코드 전체를 티스토리에 복붙해도 잘 되더라구요...? ㅎㅎ......
글을 마치며
notion-export는 저의 개인적인 필요로 시작된 토이 프로젝트였지만
Web Worker를 통한 백그라운드 처리, 동적 스타일 주입,
그리고 DOM API(getComputedStyle, getBoundingClientRect)를 활용한 직관적인 UI/UX를 구현해 보면서 저 스스로도 많이 배울 수 있었습니다.
무엇보다 제가 원했던 '노션 문서를 티스토리로 원하는 스타일을 적용해서 옮기기' 문제를 완벽하게 해결했다는 점에서 매우 만족스럽습니다.
하지만 아직 보완해야 할 부분이 정말 많습니다.
사실 만들어야겠다고 마음 먹고 하루 이틀만에 만든 거라서
기능적인 부분이나 사용성 면에서 좋지 않은 부분이 많이 보이는데요.
추후에 여유가 생긴다면 업데이트를 진행해보겠습니다!
여러분들도 지금 바로 체험해보세요!
https://parkcoool.github.io/notion-export/
감사합니다.