회사에서 공지사항을 매달 노션에 작성하고, HTML로 내보내서 Hygraph CMS에 올리는 작업을 반복하고 있었다.
파이프라인은 단순하다.
노션 작성 → HTML 추출 → Hygraph CMS 업로드
[ As -IS ]
문제는 노션이 뽑아주는 HTML에 불필요한 코드가 너무 많다는 것이다.
더러운 노션 HTML 코드.
노션은 배포용 HTML이 아니라, 편집기 내부 상태를 보존하기 위한 HTML을 내보낸다. 공지사항 한 문단을 추출하면 실제로 이렇게 생겼다.
<div style="display:contents" dir="auto">
<p id="34169501-a942-809c-97a7-ef7ad389b207" class="">
안녕하세요, 회원 여러분 !
</p>
</div>
필요한 건 아래 이거 한줄인데 ㅜㅜ 💢
<p>안녕하세요, 회원 여러분 !</p>
제거해야 하는 것들
| 요소 | 설명 |
|---|---|
id="34169501-a942-..." | 노션 내부 block id |
data-* 속성 | 노션 에디터 식별용 |
dir 속성 | 불필요한 방향 속성 |
style 인라인 속성 | width: 703px 같은 것들 |
<div style="display:contents"> | 의미 없는 래퍼 |
<head> 안 pdf/print CSS | 웹 배포와 무관한 코드 |
| 목차의 노션 링크 | notion.so/... 로 연결된 링크 |
이미지 감싼 <a> 태그 | 클릭 시 로컬 이미지로 이동 |
이렇게 수정했다
제거 외에 손봐야 하는 부분도 있었다.
1. 목차 링크 → 페이지 내 앵커로 교체
노션 목차는 기본적으로 notion.so 링크로 연결된다. CMS에 올리면 당연히 작동하지 않는다.
<!-- before -->
<a href="https://www.notion.so/페이지id?pvs=21">나노바나나 완전 정복</a>
<!-- after -->
<a href="#나노바나나-완전-정복">나노바나나 완전 정복</a>
노션 내보내기 방식에 따라 두 가지 케이스가 있었다.
- 방법 A:
notion.so링크가 직접 달려있는 경우 → href를 슬러그 기반 앵커로 교체 - 방법 B: "목차" h3 아래
numbered-list의<li>텍스트만 있는 경우 →<li>안에 앵커 링크를 직접 생성
# 방법 A
for a in soup.find_all("a", href=True):
if "notion.so" in a["href"]:
slug = f"section-{len(toc_items) + 1}"
a["href"] = f"#{slug}"
# 방법 B
toc_h3 = next(
(h3 for h3 in soup.find_all("h3") if "목차" in h3.get_text()), None
)
if toc_h3:
for ol in toc_h3.find_all_next("ol", class_="numbered-list"):
for li in ol.find_all("li"):
slug = f"section-{idx}"
a_tag = soup.new_tag("a", href=f"#{slug}")
a_tag.string = li_text
li.append(a_tag)
2. 이동 대상 h2에 id + 클래스 추가, CSS도 함께
목차 링크가 이동할 h2에 id를 붙여줘야 앵커가 작동한다. anchor-offset 클래스도 같이 달아서 스크롤 위치가 헤더에 가리지 않게 했다.
<h2 id="나노바나나-완전-정복" class="anchor-offset">나노바나나 완전 정복</h2>
.anchor-offset { scroll-margin-top: 100px; }
scroll-margin-top이 없으면 앵커로 이동했을 때 고정 헤더 뒤에 제목이 숨어버린다.
CSS는 clean_css() 함수 끝에 자동으로 추가된다.
3. 이미지 감싼 <a> 태그 제거
노션이 이미지를 <a>로 감싸는데, 클릭하면 로컬 파일 경로로 이동해버린다.
불필요한 a 태그 <a> 제거!!
for a in soup.find_all("a", href=True):
if a.find("img"):
a.unwrap() # <a>만 벗기고 <img>는 남김
4. 외부 링크 → 새 탭으로 열기
http로 시작하는 외부 링크는 전부 새 탭에서 열리도록 처리했다.
<a href="https://..." target="_blank" rel="noopener noreferrer">링크</a>
rel="noopener noreferrer"는 보안상 같이 넣어줘야 한다.
noopener는 새 탭에서 원래 페이지에 접근하는 걸 막고, noreferrer는 referrer 정보 노출을 막는다.
이렇게 자동화했다
매달 반복되는 작업이라 Python 스크립트로 자동화했다. beautifulsoup4로 HTML을 파싱해서 위의 처리를 전부 한 번에 실행한다.
# 설치
pip install beautifulsoup4
# 실행
python notion-clean.py 입력파일.html 출력파일.html
같은 폴더에 넣고 쓰는 게 제일 편하다 ㅎㅎ!
📁 notice/
├── notion-clean.py
└── notice.html
실행하면 정제된 HTML 파일이 생성되고 감소율이 출력된다.~~!
(아래 사진은 추후에 다른 파일 변경한 사진!)

수치로 보면
파일 리소스
| 항목 | 전 | 후 |
|---|---|---|
| HTML 파일 크기 | 39,792 bytes | 26,756 bytes |
| 감소율 | 0 | 32.8% |
작업 시간
| 항목 | 전 | 후 |
|---|---|---|
| 건당 작업 시간 | 1시간 이상 | 1분 이내 |
| 월 누적 (3~5건 기준) | 3시간 ~ 5시간 이상 | 5분 이내 |
매달 3시간 이상을 HTML 파일 열어서 코드 눈으로 찾아 지우는 데 썼다는 얘기다.
그리고 입사한지 1달도 안된 만큼 어떤걸 어떻게 수정해야 한다는 메뉴얼을 읽고 그대로 수정하려니 더더욱 공수가 많이 들었었다.
단순히 시간만 줄어든 게 아니라, 수동 작업에서 발생하던 누락(목차 링크 미교체, <a> 태그 일부 남아있음 등)이 사라졌다.
별도로 해야 하는 것
이미지 src는 스크립트가 건드리지 않는다.
Hygraph에 이미지를 먼저 업로드하고, 생성된 URL로 수동 교체가 필요하다.
(노션 이미지 URL은 인증 토큰이 포함된 임시 링크라 만료되기때문!!!)
<!-- 스크립트 실행 후 남아있는 상태 -->
<img src="image.png"/>
<!-- Hygraph URL로 직접 교체 -->
<img src="https://media.graphassets.com/..."/>
정리
- beautifulsoup4 기반 스크립트로 자동화 가능!!!!! 시간 절약 아싸 ㅎㅎ
- 노션 HTML 추출 시 포함되는 내부 식별용 코드(block id, data-*, 인라인 style 등)를 beautifulsoup4 기반 스크립트로 자동 제거!
- 작동하지 않는 목차 노션 링크는 페이지 내 앵커로 교체, 이미지
<a>제거 및 외부 링크target="_blank"처리까지 일괄 적용 - 파일 크기 32.8% 감소, 건당 30분 이상 소요되던 수동 작업이 1분 이내로 단축되어 월 1.5~2.5시간 절감
- 이미지 URL 교체만 수동 작업으로 남김
