티스토리 뷰

🎉 지금 바로 사용해보기

https://parkcoool.github.io/notion-export/

⚠️ 주의

토이 프로젝트입니다.

아직 부족한 기능도 많고, 버그도 많이 보입니다.

이러한 부분들은 추후에 차차 보완할 예정이니 양해해주세요 !

💬 기획 배경

노션으로 작성한 문서를 블로그나 웹사이트에 게시할 때, 스타일이 모두 깨져서 일일이 수정하셨던 경험이 있으신가요?

저도 노션으로 작성한 글을 티스토리 블로그로 옮길 때 겪는 이 불편함이 너무 컸습니다.

 

이미 노션에서는 문서를 HTML로 내보내는 기능을 제공하고 있지만,

H1(제목1)을 원하는 글꼴 크기로, 인용구를 원하는 배경색으로 바꾸는 등,

제가 원하는 대로 문서의 요소별 스타일을 자유롭게 커스터마이징할 수 있는 기능은 없었습니다.

 

그래서 만들었습니다.

notion-export는 노션에서 내보낸 "Markdown & CSV" 압축 파일을 업로드하기만 하면,

원하는 테마와 커스텀 스타일을 적용하여 단일 HTML 파일로 만들어주는 100% 클라이언트 사이드 웹 앱입니다.

 

이 글에서는 제가 이 서비스를 만들게 된 이유와 함께,

어떤 기술을 사용해 핵심 기능들을 구현했는지 그 대략적 과정을 공유해 드리고자 합니다.

🚀 notion-export의 핵심 기능

  1. 서버 없는 100% 클라이언트 앱: 모든 파일 처리가 사용자의 브라우저 내에서 이루어져 빠르고 안전합니다.
  2. 자유로운 스타일 커스터마이징: 'Minimal', 'Academic' 등 기본 프리셋은 물론, 제목, 인용구, 코드 블럭 등 모든 마크다운 요소를 개별적으로 커스터마이징할 수 있습니다.
  3. WYSIWYG 스타일 편집: 미리보기 화면에서 스타일을 바꾸고 싶은 요소를 클릭하면, 해당 위치에 바로 팝 오버가 떠서 직관적으로 디자인을 수정할 수 있습니다.
  4. 단일 HTML 파일로 내보내기: 모든 스타일(인라인 또는 <style> 태그)과 본문이 포함된 단일 html 파일을 생성하여 Tistory 글 쓰기에서 HTML에 그대로 붙여넣기할 수 있습니다.

🛠️ 구현

핵심 기능들이 어떤 기술 스택으로 구현되었는지 코드를 기반으로 조금 더 깊게 살펴보겠습니다.

1. 무거운 ZIP 파일 처리: Web Workers와 JSZip

Notion에서 이미지가 많은 문서를 내보내면 ZIP 파일 용량이 수십 MB에 달할 수 있습니다.

이 무거운 파일을 브라우저(메인 스레드)에서 직접 압축 해제하고 이미지 데이터를 처리하면,

사용자는 앱이 멈추는 경험을 하게 될 수도 있습니다.

 

이 문제를 해결하기 위해 웹 워커 (Web Worker)를 도입했습니다.

  1. 사용자가 FileUploader 컴포넌트를 통해 파일을 업로드하면 App.tsx의 handleFileUpload 함수가 실행됩니다.
  2. 이 함수는 useZipAnalyzer 커스텀 훅의 analyzeZipFile 함수를 호출합니다.
  3. useZipAnalyzer 훅은 zipAnalyzer.worker.ts라는 별도의 Web Worker 스크립트를 생성합니다.
  4. 파일 데이터(ArrayBuffer)를 메인 스레드에서 Web Worker 스레드로 전송합니다.
  5. Web Worker는 메인 스레드와 독립적으로 작동하며, JSZip 라이브러리를 사용해 ZIP 파일의 압축을 풉니다.
  6. 특히 Notion은 export-xxx.zip 안에 real-content-xxx.zip이 들어있는 중첩 압축 구조를 가질 때가 있는데, worker는 이 파일(.zip)을 감지하고, 내부 ZIP을 다시 loadAsync 하여 실제 콘텐츠에 접근합니다.
  7. Worker는 .md 파일의 텍스트와 모든 이미지 파일(Base64 데이터로 변환)을 ZipAnalysisResult 객체로 묶어 메인 스레드로 postMessage를 보냅니다.
  8. App.tsx는 useEffect로 이 result를 감지하고, previewContent 상태를 업데이트하여 PreviewArea 컴포넌트를 리렌더링합니다.

이 구조 덕분에 100MB가 넘는 ZIP 파일을 처리하는 동안에도

사용자는 로딩 UI를 통해 현재 파일이 처리되고 있음을 알 수 있습니다.

2. 실시간 미리보기: markdown-it과 동적 스타일 주입

마크다운 텍스트와 이미지 데이터를 가져오면, 이를 사용자에게 보여줘야 합니다.

PreviewArea 컴포넌트는 useMemo를 사용해 전달받은 content와 images를 HTML로 변환합니다.

이 과정은 markdown.ts 유틸리티가 담당합니다.

  1. markdown-it: 마크다운 텍스트를 HTML로 1차 변환합니다.
  2. replaceImagePaths: 1차 변환된 HTML 내의 <img> 태그를 정규식으로 찾습니다. src="image%20folder/my-pic.png" 처럼 로컬 경로로 되어있는 src 속성을 worker가 전달해 준 이미지 맵(ImageMap)을 참조하여 src="data:image/png;base64,..." 형태의 Base64 URL로 즉시 치환합니다.
  3. fixNotionCallouts: Notion의 콜아웃의 제목 부분은 마크다운으로 변환 시 이모티콘과 텍스트가 분리되어 지저분하게 나옵니다. 이 함수는 <aside> 태그를 찾아 이모티콘과 첫 번째 문단을 <strong> 태그로 묶어 깔끔하게 재조합합니다.

이렇게 완성된 HTML은 dangerouslySetInnerHTML로 렌더링됩니다.

StyleConfig 타입으로 정의된 스타일 상태 객체(customStyles)가 변경될 때마다 styleConverter.ts의 injectCustomStyles 함수가 호출됩니다.

3. 핵심 차별점: 클릭 기반 WYSIWYG 스타일링

단순히 모달을 열어 스타일을 수정하는 것은 직관적이지 않습니다.

저는 사용자가 미리보기에서 직접 요소를 클릭하여 스타일을 수정하길 원했습니다.

PreviewArea 컴포넌트의 useEffect 훅이 이 기능의 핵심입니다.

  1. 미리보기가 렌더링되면 .prose 영역에 mousemove과 click 이벤트 리스너를 추가합니다.
  2. mousemove: 마우스가 움직일 때마다 findEditableElement 함수가 마우스 포인터 아래에 있는 요소(H1, P, LI 등)를 감지하고, 해당 요소에 파란색 outline을 실시간으로 그려줍니다.
  3. click: 사용자가 요소를 클릭하면,
    • event.preventDefault()로 링크 클릭 등 기본 동작을 막습니다.
    • 클릭된 요소의 getBoundingClientRect()를 호출하여 화면상의 정확한 위치(x, y, width, height)를 가져옵니다.
    • window.getComputedStyle(element)을 호출하여 현재 적용된 fontSize, color 등 실제 스타일 값을 읽어옵니다.
    • 이 위치 정보와 스타일 정보를 selectedElement 상태에 저장합니다.
  4. selectedElement 상태가 null이 아니게 되면, StyleEditorPopover 컴포넌트가 렌더링됩니다.
  5. 이 팝오버는 position: fixed 속성을 가지며, getBoundingClientRect로 얻어온 위치 값을 기반으로 클릭된 요소 바로 옆에 영리하게 배치됩니다.
  6. 사용자가 팝오버에서 스타일(예: fontSize)을 변경하고 '모든 H1 요소에 적용'을 체크한 뒤 '적용'을 누르면, onApplyToAll 콜백이 호출됩니다.
  7. 이 콜백은 App.tsx의 handleElementStyleChange를 통해 메인 customStyles 상태를 업데이트하고, 이는 즉시 동적 <style> 태그를 갱신하여 미리보기의 모든 H1 요소에 반영됩니다.

4. 두 가지 방식의 HTML 내보내기

마지막으로, 완성된 문서를 티스토리같은 다른 플랫폼으로 가져갈 수 있도록 옵션을 제공하였습니다.

  1. 기본 (<style> 태그 사용): 가장 깔끔한 방식입니다. extractStyles 함수가 <head>에 주입된 동적 <style> 태그의 CSS 텍스트를 그대로 가져오고, .prose 영역의 innerHTML을 가져와 표준 HTML 템플릿에 삽입합니다.
  2. 인라인 스타일 사용 (최고 호환성): 티스토리처럼 <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/

 

감사합니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함