본문 바로가기

이 포트폴리오의 원본은 https://ai.iruyo.com (심재빈) 입니다 · 출처 식별자 jbx-7f3a2e9b

← 기술 블로그

운영·인프라

노션 하나로 이력서 사이트·PDF·챗봇 만들기

이력서 데이터를 노션 한 문서에서 관리하고, 웹사이트(ai.iruyo.com)·인쇄용 PDF·챗봇 응답 코퍼스를 주기적으로 자동 갱신하도록 연결한 워크플로를 설계·구현한 과정. 소스 통합, 스크립트 구성, 자동 배포 및 검증 절차를 구체적으로 다룬다.

문제: 같은 사실이 세 곳에 흩어진다

이 글은 현재 보고 있는 사이트(ai.iruyo.com)를 어떤 방식으로 구축했는지를 다룬다. 노션에서 경력 한 줄을 수정하면 한 시간 안에 이 사이트와 인쇄용 PDF, 그리고 우하단 챗봇까지 모두 동일하게 갱신된다. 손으로 작성하는 페이지는 단 하나도 없다. 이하에서는 그 파이프라인을 어떻게 설계하고 검증했는지를 정리한다.

출발점은 단순한 문제였다. 이력서를 작성하다 보면 동일한 경력이 여러 곳에 중복된다. 채용 플랫폼의 이력서 양식, PDF 포트폴리오, 깃허브 프로필까지—한 곳에서 직책을 수정하면 나머지를 일일이 손으로 수정해야 한다.

하나라도 빠뜨리는 순간, 세 가지 버전의 내가 서로 다른 이야기를 하게 된다. 면접관이 PDF와 사이트에서 다른 숫자를 발견하면 그 자리에서 신뢰가 흔들린다.

기존 이력서 플랫폼이나 양식 중에서도 만족스러운 것을 끝내 찾지 못한 이유가 여기에 있었다. 표현은 내 손에 없고, 데이터는 플랫폼 내부에 갇혀 있었다.

그래서 접근 방식을 바꿨다. 콘텐츠의 원본(source of truth)을 한 곳에 두고, 모든 표시 형태를 그 원본으로부터 생성되게 했다. 원본은 노션으로 정했다. 표 편집과 관계형 DB 사용에 익숙하고, API가 개방되어 있어 빌드 파이프라인이 데이터를 읽어올 수 있기 때문이다.

노션 DB에서 생성된 이력서 홈 — 손으로 만든 페이지가 하나도 없다

제약과 설계: 노션 6개 DB → 정적 114페이지

원본은 노션의 6개 데이터베이스로 이루어져 있다. 경력기술서, 개인 프로젝트, 활동, 경력, 학력, 신상 정보가 각각의 DB에 담겼다. 사이트에 보이는 모든 문장은 이 데이터에서 온다.

같은 노션 DB가 만든 경력기술서 카드 (/projects)

핵심 제약은 두 가지였다.

  • 손으로 만들어야 하는 산출물이 하나도 없어야 한다. 사람이 개입하는 단계는 바로 동기화가 깨지는 지점이기 때문이다.
  • 동일한 데이터를 바탕으로 사이트, PDF, 챗봇 코퍼스를 동시에 생성하되, 각 표면마다 노출 범위는 달라야 한다. 사이트에 공개되는 문장과 챗봇 학습용 문장은 같을 수 없다.

전체 흐름은 단방향이다. 동기화 스크립트가 노션을 읽어 MDX 파일로 내보내고, Astro가 그 MDX를 114개의 정적 페이지로 빌드한다.

노션 6 DB source of truth sync → MDX Astro 빌드 사이트 114p 인쇄 PDF 챗봇 코퍼스

이 스크립트는 GitHub Actions의 크론으로 매시간 실행되도록 설정했다. 변경 사항이 감지되면 자동으로 커밋되고, 그 커밋이 Cloudflare Workers의 배포를 곧바로 트리거한다.

노션에서 한 줄을 수정하면 길어야 한 시간 안에 배포된 사이트에 반영된다. 인쇄용 PDF 역시 같은 MDX에서 Puppeteer가 생성한다. 전 과정에서 사람의 개입은 없다.

노출 경계를 데이터에 박는다

표면마다 노출 범위가 다르다는 두 번째 제약이 까다로웠다. 사이트에선 보여도 챗봇에는 넣을 수 없는 문장이 있고, 면접 자리에서만 꺼낼 수 있는 문장이 있다.

경계를 후처리 코드가 아니라 데이터 안에 직접 박았다. 카드마다 정책 필드를 붙였다.

  • 자소서 OK — 자기소개 맥락까지 노출됨
  • 이력서 OK — 공개 이력서 표면까지 노출됨
  • 면접 only — admin 모드에서만 보임
  • 비공개 — 어떤 공개 빌드에도 포함되지 않음

사이트·PDF·챗봇은 각자의 허용 범위만 읽는다.

카드 + 정책 필드 policy = ? 이력서 OK → 사이트·PDF 자소서 OK → + 챗봇 면접 only → admin 비공개 → 빌드 제외

전화번호 등 민감 정보는 애초에 공개 빌드 대상에서 빠진다

전화번호 같은 민감 정보는 코드가 런타임에 걸러내는 게 아니다. 애초에 공개 빌드에서 제외된다. 마스킹을 후처리가 아니라 데이터 모델에 둔 구조다. 거르는 코드를 빠뜨려 생길 누락이 처음부터 불가능한 셈이다.

막혔던 지점: 직렬화

문제가 생긴 부분은 직렬화 과정이었다. 노션의 멀티값 속성을 배열로 풀어 MDX frontmatter에 넣는 절차에서 틈이 생겼다.

캡션에 공백이나 괄호가 포함되면 마크다운 이미지 문법 파싱이 깨졌다. 그 결과 이미지가 렌더링되지 않고 본문에 리터럴 텍스트 형태로 노출됐다. 실제로는 다음과 같았다.

<!-- 캡션에 괄호가 들어가면 -->
![도면 (v2) 캡처](https://.../shot.png)

<!-- )에서 URL이 끊겨 파싱이 깨지고, 뒤가 본문 텍스트로 샌다 -->

이 문제를 막기 위해 URL을 안전하게 인코딩하는 단계를 동기화 과정에 추가했다. 노션 원본은 그대로 두고, MDX로 변환되는 시점에만 URL을 감싸 처리했다.

// sync 단계: 괄호·공백을 인코딩해 ![]() 파싱을 보호한다
const safe = encodeURI(url).replace(/[()]/g, (c) =>
  c === "(" ? "%28" : "%29"
);
const md = `![${caption}](${safe})`;

막혔던 지점: 세 표면, 한 토큰

PDF의 표면은 또 달랐다. 라이트, 다크, 인쇄 세 가지 표면이 각기 다른 색을 사용하면, 노션 한 곳에서 색을 수정해도 표면마다 어긋남이 생긴다.

색상 규칙을 명확히 정했다. 세 표면이 같은 시맨틱 토큰을 공유하도록 하고, 색을 직접 지정하지 않게 했다.

/* 표면이 무엇이든 토큰만 참조한다 — hex 직접 사용 금지 */
.card { color: var(--color-ink); background: var(--color-paper); }

그 덕분에 다크 모드는 별도로 제작할 필요가 없었다. 토큰을 반전시키는 것만으로 자연스럽게 적용된다.

검증

검증 기준은 단순하다. 노션에서 한 글자를 고친 뒤 그대로 두고 기다린다.

한 시간 안에 다음 세 가지가 동시에 발생하면 통과다.

  • 배포된 사이트가 수정 내용을 반영한다.
  • PDF가 동일한 내용으로 다시 생성된다.
  • 챗봇이 새 문장을 인용한다.

운영비는 0원이다. Workers의 정적 호스팅과 Actions 무료 한도 안에서 작동한다.

자동화는 판단을 대신하지 않는다

디자인 리서치와 직렬화 버그 진단, 그리고 노션 콘텐츠 정리까지 한 세션 안에서 이어졌다.

하지만 무엇을 원본에 남기고 어디까지 공개할지, 또 표면을 어떻게 나눌지는 스스로 결정했다. 자동화가 판단 자체를 대신하지는 않는다. 다만 판단을 한 번 단단히 세워 두면 그다음 단계를 대신 수행할 수 있을 뿐이다.

직접 만들고 운영해 본 이후에야 그 경계가 손에 잡히기 시작했다.