← 목록으로

10년차 테크 블로거의 웹 스택 탐험기: 세 개의 Git 레포가 품은 다채로운 이야기

2026. 7. 8.

10년차 테크 블로거의 웹 스택 탐험기: 세 개의 Git 레포가 품은 다채로운 이야기


title: 10년차 테크 블로거의 웹 스택 탐험기: 세 개의 Git 레포가 품은 다채로운 이야기 published: true description: 'mattstratton.com과 speaking.mattstratton.com 뒤편의 스택 탐험기: 두 개의 Astro 사이트와 dev.to 동기화 도구를 품은 모노레포, 20년 역사의 블로그 포스팅, 그리고 이 글과 같은 게시물을 교차 게시하는 파이프라인을 자세히 들여다봅니다.' tags:


제가 운영하는 블로그는 현재 블로그를 구동하는 대부분의 도구들보다 훨씬 오래됐습니다. 가장 오래된 포스팅들은 2000년대 초반 LiveJournal에서 처음 시작되어, 2000년대 중반 어느 시점에 WordPress로 끌려왔죠. 이후 10년 가까이 Hugo 사이트로 지내다가, 약 일주일 전 Astro 5 사이트로 이주하며 옛 URL들을 한 바이트도 빠짐없이 완벽하게 재현했습니다. 이는 2001년부터 2020년까지 2,630개의 게시물이 작성된 방대한 기록으로, 그 자리에 그대로 얼어붙어 보존되고 있습니다.

같은 시기에, 제 발표 사이트(2012년부터 106개의 발표 자료를 포함)도 Notist에서 벗어나 제가 직접 소유한 스택으로 옮겨왔습니다. 그리고 dev.to 초안을 Git과 동기화하는 도구도 드디어 처음부터 의도했던 대로 작동하게 되었죠. 한동안 엉뚱하게 돌아갔던 것을 생각하면 감회가 새롭습니다.

이 세 가지는 모두 한때 개별적인 레포지토리였습니다. 하지만 오늘 일찍이 이들은 하나의 모노레포로 합쳐졌고, 각 레포의 실제 Git 히스토리는 "초기 커밋"으로 뭉뚱그려지지 않고 온전히 보존되었습니다. 지금부터 이 여정을 자세히 살펴보겠습니다. 두 사이트가 어떻게 작동하는지, dev.to가 어떻게 이 시스템에 통합되었는지, 그리고 이 과정에서 흥미롭게 발생했던 몇 가지 문제점들도 함께 다룰 예정입니다.

오늘날의 구조

세 가지 시스템, 한 명의 운영자, 그리고 이제는 하나의 레포입니다.

  • mattstratton.com은 개인 사이트입니다. 2,630개의 방대한 레거시 아카이브와 함께, 아카이브가 아닌 제가 쓰고 싶은 모든 글을 위한 새로운 범용 /writing/ 섹션을 포함합니다.
  • speaking.mattstratton.com은 발표 자료 아카이브입니다. 모든 발표 자료, 모든 영상, 모든 이벤트를 직접 호스팅합니다.
  • mattstratton-dev-to는 웹사이트가 아닙니다. dev.to 초안과 이 Git 레포를 동기화하는 작은 도구죠. 이 도구 덕분에 dev.to에 게시된 글이 mattstratton.com에도 본래부터 있던 것처럼 나타날 수 있습니다. 바로 이 글처럼 말이죠.

이 모든 것이 이제 하나의 모노레포로 병합되었지만, 여전히 각자의 설정과 존재 이유를 가진 세 개의 완전히 독립적인 시스템으로 존재합니다. 그럼 하나씩 자세히 들여다보겠습니다.

mattstratton.com: 20년 역사를 보존하다

현재 사이트는 Astro 5와 Tailwind v4로 구축되어 있습니다. 이번 마이그레이션에서 모든 결정을 지배했던 단 하나의 규칙이 있습니다. 바로 URL 보존이 최우선 목표라는 점입니다. astro.config.mjstrailingSlash: 'always'를 설정하고, 이전 Hugo URL을 정확히 재현하기 위해 디렉터리 형식으로 빌드합니다. 이 Hugo URL은 그 이전의 WordPress URL을 재현한 것이었죠. 2009년 블로그 롤이나 2014년 컨퍼런스 발표 요약 링크 등 오래된 외부 링크들이 여전히 정상적으로 작동합니다. 제가 사이트를 다른 프레임워크로 바꿨다고 해서 누구도 깨진 링크를 고칠 필요가 없습니다.

posts 컬렉션은 2001년부터 2020년까지의 글들을 Hugo에서 일괄 변환하여 '얼려둔' 아카이브입니다. 더 이상 글이 추가되지 않죠. 그저 계속 존재하기만 하면 됩니다. 모든 새로운 콘텐츠는 writing 섹션으로 들어가는데, 이 부분은 제가 이 글을 쓰기 시작했을 때 예상했던 것보다 훨씬 더 많은 이야기가 담겨 있습니다. URL 보존은 정말 중요한데, 제가 예전에 운영하던 서비스에서 URL 구조를 변경했다가 SEO 점수와 유입률이 박살 났던 아픈 기억이 있어서 이 부분은 정말 깊이 공감합니다. 과거의 흔적을 지우지 않는다는 것은 기술적으로나 경험적으로나 큰 의미가 있죠.

필드 가이드: 내 글과 다른 곳에 쓴 좋은 글들

/writing/ 섹션은 이전에는 훨씬 더 좁은 범위로 사용될 예정이었습니다. 이 프로젝트 전에는 Postgres 관련 권위 있는 콘텐츠를 중심으로 기획되었는데, 이는 제 개인 블로그 독자층과는 완전히 다른 잠재 고객을 위해 작성된 콘텐츠 전략 문서에서 계승된 아이디어였습니다. 하지만 이 계획을 뒤엎은 간단한 반론이 있었죠. "나는 다채로운 것을 품고 있고, 단 하나의 주제만 담을 수 있는 개인 사이트는 더 이상 개인적인 것이 아니다"라는 것이었습니다.

하지만 현재 /writing/에서 정말 흥미로운 점은 단순히 제 글만 포함하지 않는다는 것입니다. 이는 두 가지 완전히 다른 요소들의 합집합입니다.

  • src/content/writing/에 있는 본래 글들. content.config.ts의 스키마에 따라 title, description, pubDate, topics, draft, 그리고 큐레이션된 네 부분짜리 Postgres 아크를 위한 part(선택 사항), 원본이 다른 곳에 있는 경우를 위한 canonicalUrl(선택 사항)으로 정의됩니다.
  • 제가 고용주인 Tiger Data를 위해 작성한 외부 글들의 수동으로 관리되는 목록으로, field-guide.ts에 정의되어 있습니다.
export interface FieldGuideLink {
  title: string;
  description: string;
  part: "mechanics" | "limits" | "traps" | "decision";
  url: string;
}

현재 14개의 외부 링크가 있으며, 모두 tigerdata.com을 가리키고 있습니다. 각 링크는 본래 글들이 선택할 수 있는 동일한 네 가지 part 버킷 중 하나에 할당됩니다. writing.ts는 이 두 가지 형태를 GuideItem(title, description, href, external)이라는 하나의 공통 형태로 정규화하여, 템플릿이 이들을 동일하게 렌더링할 수 있도록 합니다. 각 파트 내에서는 본래 글들이 먼저, 그 다음에 외부 링크가 배치되며, 날짜 순서는 섞이지 않습니다. 유일한 시각적 표시는 외부 글 제목 옆에 붙는 작은 "tigerdata.com ↗" 배지입니다. 새 탭으로 열리지는 않지만, 어디로 연결될지 솔직하게 알려주는 역할을 하죠.

뉴스레터: Buttondown은 발송만, Astro는 페이지 제공만

/newsletter/는 이 사이트의 대부분과 동일한 방식으로 작동합니다. 빌드 시 데이터를 가져와서 정적 파일을 영원히 제공하는 방식이죠. buttondown.ts는 Astro 콘텐츠 레이어 로더로, Buttondown API의 GET /emails?ordering=-publish_date 엔드포인트를 호출하고, 페이지가 소진될 때까지 페이지네이션을 따라간 뒤 그 결과를 newsletter 컬렉션으로 Astro에 넘겨줍니다. 모든 발행물 페이지, 아카이브 인덱스, RSS 피드는 일반적인 getCollection() 호출의 정적 출력물이며, 로컬 마크다운 파일과 동일하게 처리됩니다.

이와 같은 방식을 고려하는 분들을 위해 두 가지 주목할 만한 세부 사항을 알려드립니다.

  • BUTTONDOWN_API_KEY가 설정되지 않으면, 로더는 경고를 기록하고 빌드 실패 대신 빈 컬렉션을 반환합니다. 이는 의도된 것입니다. 즉, 제 API 키 없이도 누구든지 이 레포를 클론하여 로컬에서 빌드할 수 있다는 의미죠.
  • Buttondown은 각 발행물의 본문을 마크다운으로 저장하므로, 로더는 템플릿에 도달하기 전에 marked.parse()를 통해 이를 파싱합니다. 사이트는 API에서 가져온 원본 HTML을 직접 다루지 않습니다.
const key = import.meta.env.BUTTONDOWN_API_KEY ?? process.env.BUTTONDOWN_API_KEY;
if (!key) {
  logger.warn("BUTTONDOWN_API_KEY not set: newsletter archive will build empty.");
  return;
}

speaking.mattstratton.com: 내가 소유하지 않은 CDN에서 슬라이드를 되찾다

이전 발표 사이트는 Notist에서 운영되었는데, Notist는 결국 하나의 문제로 귀결되는 두 가지 문제점을 가지고 있었습니다. 비디오 임베드가 조용히 깨지기 시작했고, 모든 슬라이드 데크가 Notist의 CDN을 통해 제공되고 있었다는 점입니다. 이 계정이 만료되는 날, 이미지와 PDF도 함께 사라지는 것이죠. 연사로서의 전문적인 정체성의 핵심이자 제가 했던 모든 발표 자료가 다른 사람의 데이터베이스에, 그것도 빈약한 내보내기 옵션과 함께 살고 있었습니다.

그래서 새로운 사이트의 전체 디자인 목표는 자산 소유권과 영구성입니다. 제 레포에, 제 도메인에 있는 평범한 콘텐츠가, 언제든 중단되거나 조용히 사라질 수 있는 타사 종속성 없이 계속 작동하는 것이죠. 단순히 리디자인이 아니라, 이식성을 위한 프로젝트였습니다.

흥미로운 부분은 슬라이드 파이프라인인데, 이 파이프라인은 커밋이 발생하기 전, 배포 호스트가 아닌 전적으로 로컬에서 실행됩니다.

originals/{id}.pdf --최적화(gs, 300dpi)--> public/slides/{id}.pdf --래스터화--> public/slides/{id}/{n}.webp
   (gitignored, 고해상도)          (커밋됨, 제공됨, 다운로드 가능)     (커밋됨, 제공됨, 뷰어 이미지)

로컬에서 실행해야 하는 이유는 다음과 같습니다. 정적 호스트는 루트 권한을 제공하지 않으며, pdftoppmgs를 실행하려면 루트 권한(또는 최소한 실제 패키지 관리자)이 필요합니다. Netlify 빌드 내부에서 Ghostscript를 셸 아웃하여 실행하려 하면 필연적으로 실패할 것입니다. 제 컴퓨터에서 래스터화를 수행하고 그 결과물을 커밋하면 실제 배포 빌드는 지루하고 빠르며 호스트에 구애받지 않는 astro build가 됩니다.

비디오 임베드도 "소유하고, 프록시하지 않는다"는 동일한 원칙이 적용되었습니다. Notist의 접근 방식(다시 한번 말하지만 이미 깨지기 시작했던 프록시)을 재현하는 대신, 각 발표는 프론트매터에 평범한 제공자와 비디오 ID를 저장하고, 페이지는 클릭 시에만 실제 플레이어를 로드하는 경량 임베드 파사드를 렌더링합니다.

현재 수치는 2012년부터 2026년까지 106개의 발표, 93개의 이벤트, 36개의 영상, 3,684개의 슬라이드 이미지입니다. 이 모든 것이 커밋되어 있으며, 단 한 번의 CDN 장애로 사라질 염려가 없습니다.

발표 자료 검색 기능 구현하기

비디오는 발표 사이트가 온전히 소유하지 못하는 유일한 자산 유형입니다. 레퍼런스는 호스팅할 수 있지만, 파일 자체는 불가능하죠. 하지만 사이트가 소유할 수 있는 것은 바로 스크립트이고, 이는 실제로 유용한 기능으로 발전했습니다. 제가 했던 모든 발표에 대한 전체 텍스트 검색 기능 말입니다.

원본 스크립트는 YouTube의 자동 생성 캡션에서 가져옵니다. scripts/transcripts.tsyt-dlp를 통해 이를 가져와 정리한 다음 public/transcripts/{id}.txt에 기록합니다. 자동 생성된 캡션은 거칩니다. 구두점도 없고, 고유명사는 엉망이며, 모든 "음", "어" 같은 말더듬도 충실히 기록되죠. transcript-cleanup Claude Code 스킬이 구두점, 대소문자, 고유명사를 고치고 말더듬을 제거하는 정리 작업을 처리합니다. 하지만 엄격한 규칙을 따릅니다. 말한 내용을 재구성하거나, 의역하거나, 순서를 바꾸는 일은 절대 없습니다. 의심스러울 때는 추측하기보다 텍스트를 그대로 두라는 지침을 따르죠.

검색 자체는 배포 시점에 생성되는 두 개의 별도 JSON 페이로드를 사용하여 클라이언트 측에서 실행됩니다.

  • search-index.json.ts는 작은 파일입니다. 발표당 제목, 요약, 이벤트, 태그, 연도 정보를 담습니다. 검색창이 처음 열릴 때 로드됩니다.
  • deep-index.json.ts는 더 큰 파일입니다. 각 발표의 스크립트와 슬라이드 텍스트를 하나의 검색 가능한 덩어리로 연결하며, 쿼리를 실제로 입력할 때만 지연 로드됩니다.
const parts = [readTranscript(talk.id), readSlideText(talk.id)].filter(Boolean);
if (!parts.length) continue;
records.push({ url: talkUrl(talk), text: parts.join(" ").replace(/\s+/g, " ") });

Search.astro는 각 페이로드 위에 Fuse.js 퍼지 인덱스를 구축하고 결과를 병합합니다. 그래서 제목에 있는 단어뿐만 아니라 발표 중간에 제가 실제로 했던 문구를 검색해도 올바른 발표를 찾아낼 수 있습니다. 서버도 데이터베이스도 없습니다. 그저 두 개의 정적 JSON 파일과 퍼지 매칭 라이브러리가 브라우저에서 모든 작업을 수행할 뿐입니다.

세 개의 레포가 하나로 합쳐지다

세 개의 레포를 하나의 모노레포로 합친 것은 단순히 깔끔함을 위한 작업은 아니었습니다. 세 개의 레포를 개별적으로 관리하는 것이 약간의 마찰을 일으키긴 했지만, 실제 계기는 고정된 아카이브와 새로 범위가 지정된 /writing/ 섹션 사이에서 제 사이트에 일반적인 글을 위한 공간이 남아있지 않다는 것을 깨달았을 때였습니다. 이것이 진짜 문제가 되자, "레포 몇 개를 합치자"는 "아키텍처를 고치자, 그리고 김에 dev.to 교차 게시 기능도 제대로 작동하게 만들자"로 이어졌습니다.

병합 메커니즘 자체는 squash나 filter-repo rewrite가 아닌 git subtree add를 사용했습니다. 이는 소스 레포들의 전체 커밋 히스토리를 실제적이고 검증 가능한 조상으로 보존하는 것이지, 단순히 "파일이 여기로 왔으니 믿어달라"는 식의 작업이 아닙니다. 두 소스 레포가 비교적 새로웠기(각각 약 50개, 22개 커밋) 때문에 수십 년의 고고학적 작업을 보호할 필요는 없었지만, 원칙에 따라 올바르게 처리하는 것이 나름의 만족감을 주었습니다.

각자 자체 CI를 가지고 있는 레포들을 병합하려고 할 때 주의해야 할 몇 가지 문제점들이 있는데, 다음과 같습니다.

  • GitHub Actions 워크플로는 실제 레포 루트의 .github/workflows/에서만 발견되며, 하위 디렉토리에서는 절대 발견되지 않습니다. 초기 병합 후 dev.to 워크플로는 서브 프로젝트 자체의 .github/workflows/ 아래에 중첩된 채로 남아있었는데, 이는 GitHub에 완전히 보이지 않게 만들었습니다. 오류도, 경고도 없었죠. gh workflow list에서는 나타나지 않았고, 조용히 다시는 실행되지 않았습니다.
  • Netlify는 자체적으로 서브 디렉토리별로 리빌드를 범위 지정하지 않습니다. 모든 푸시는 연결된 모든 사이트를 리빌드합니다. 각 사이트의 netlify.toml에 명시적인 ignore 체크를 추가하지 않는 한 말이죠. 그리고 Netlify가 관련 변경 사항이 없어서 빌드를 건너뛸 때, 이를 "콘텐츠 변경이 없어 빌드 취소됨"이라고 표시하는데, 이는 실제로는 아무 문제가 없는데도 받은 편지함에서는 실패처럼 읽힙니다.
  • "Base directory"와 "Package directory"는 다른 Netlify 설정입니다. npm 워크스페이스가 없는 레포의 재링크 흐름에서는 Base directory만 표시되고, Package directory는 나타나지 않습니다. 매번 몇 년에 한 번씩 재학습해야 하는 문장이지만, 추측하기보다는 실제 문서를 읽는 것이 좋습니다. 이 CI/CD 설정 문제는 제가 모노레포를 처음 도입했을 때 가장 골머리를 앓았던 부분입니다. 특히 Netlify의 "Canceled build due to no content change" 알림은 매번 식은땀을 흘리게 만들었죠. 별 문제가 없다는 것을 알면서도 알림을 받을 때마다 괜히 가슴이 철렁했습니다.
  • astro check는 서브 프로젝트 경계를 존중하지 않습니다. 모노레포 루트에서 실행하면, 관련 없는 서브 프로젝트의 콘텐츠 컬렉션을 다른 서브 프로젝트의 생성된 타입을 사용하여 스캔했고, 200개가 넘는 잘못된 타입 에러를 생성했습니다. tsconfig.json에 명시적인 exclude 한 줄을 추가하자 3개로 줄었고, 이들은 모두 기존에 존재하던 무관한 에러였습니다.

만약 두 개 이상의 문제가 병합하려는 프로젝트에 해당된다면, 오후 시간을 조금 더 할애할 준비를 하세요.

dev.to: 방향이 아닌 소유권

dev.to 동기화 도구는 플랫폼과 Git 간에 콘텐츠를 동기화하는 누구에게나 진심으로 추천하고 싶은 하나의 아이디어를 중심으로 구축되었습니다. 바로 방향이 아닌 소유권입니다. 전체적인 정신 모델은 레포의 CLAUDE.md에 담겨 있지만, 짧게 요약하자면 다음과 같습니다.

이를 양방향 동기화, 즉 dev.to와 Git을 완벽하게 동기화하고 양방향으로 변경 사항을 푸시하는 것으로 생각하기 쉽습니다. 그렇게 하지 마세요. 각 측이 자신이 실제 버전을 가지고 있다고 계속 생각하는 무한한 팬텀-차이 루프를 생성할 뿐입니다. 대신, 게시물은 초안 작성 중에는 dev.to가 소유하며, Git은 이를 완전히 무시합니다. 사람이 가져오기 PR을 병합하면, 해당 게시물은 프론트매터에 id 필드가 존재함으로써 영원히 Git이 소유하게 됩니다. 소유권은 단 한 번, 의도적인 순간에만 이전되며, 자동으로 되돌아가는 일은 없습니다.

두 개의 작은 워크플로가 실제 작업을 수행합니다. devto-import.yml은 크론으로 실행되며, 새로 게시된 dev.to 게시물을 가져와서 검토를 위한 PR을 엽니다. 이미 id를 가진 파일은 절대 건드리지 않습니다. devto-publish.ymlposts/ 아래 파일에 푸시할 때 실행되며, 로컬 편집 내용을 dev.to로 다시 푸시하고, 게시물이 처음 게시될 때 id를 다시 기록합니다. 이들의 도메인은 절대 겹치지 않으며, 이것이 바로 핵심입니다.

교차 게시 파이프라인 (네, 이 글도 이 파이프라인을 거칩니다)

일단 게시물이 Git이 소유하게 되면, 프론트매터에 crosspost: true를 추가하여 교차 게시를 선택할 수 있습니다. 이렇게 하면 mattstratton.com의 /writing/ 컬렉션에 본래 글처럼 다시 게시되고, dev.to 게시물의 canonical_urlcrosspost-devto.ts를 통해 mattstratton.com을 가리키도록 재작성됩니다. 이 플래그는 게시물이 이미 dev.to에 게시되어 id를 가진 후에만 추가됩니다. 이는 초기 초안 템플릿의 일부가 아니라, 의도적인 후속 선택 사항입니다. 이 글은 "이전"의 좋은 예시입니다.

---
title: "I Contain Multitudes (and Also Three Git Repos)"
published: false
description: "..."
tags: [astro, webdev, showdev]
---

id, crosspost, canonical_url이 없습니다. 이 글이 게시되고 mattstratton.com에도 속한다고 판단되면, decouple-release-from-deploy.md가 이미 가지고 있는 것처럼 이 세 가지를 모두 갖게 될 것입니다.

id: 4061454
date: "2026-07-03T15:43:16Z"
crosspost: true
canonical_url: "https://www.mattstratton.com/writing/decouple-release-from-deploy/"

제가 정말 마음에 드는 디자인 세부 사항은 이것이 두 단계의 "게시 후 다시 돌아와 확인" 과정이 아니라, 하나의 원자적인 커밋으로 압축된다는 점입니다. 교차 게시된 글의 mattstratton.com URL은 빌드가 시작되기도 전에 파일 이름으로부터 완전히 결정됩니다. dev.to의 자체 글 ID는 서버에서 할당되며 미리 알 수 없습니다. 따라서 하나의 커밋이 새로운 /writing/ 항목을 생성하고 동시에 dev.to 소스의 canonical_url을 같은 diff에서 재작성합니다. 그리고 한 번의 푸시로 Netlify 배포와 dev.to의 기존 게시 워크플로를 모두 올바르게 트리거하며, 아무도 두 번째 단계를 지켜볼 필요가 없습니다.

하나의 함정은 언급할 가치가 있는데, 괜찮아 보이다가도 그렇지 않은 종류의 문제입니다. writing 스키마에는 첫 Astro 마이그레이션 커밋부터 선택적 canonicalUrl 필드가 있었습니다. 이는 한 번도 발생한 적 없는 경우, 즉 실제 원본이 다른 곳에 있는 복사본(예: 미러링된 Tiger Data 게시물)인 본래 /writing/ 항목을 위한 것이었습니다. 교차 게시 스크립트가 존재하기 전에는, 이 필드는 자리 표시자 템플릿 파일의 주석 처리된 줄(# canonicalUrl: ... # set when cross-posted)에서만 나타났습니다. 실제로 아무도 실제 게시물에 이 필드를 설정한 적이 없었죠. 하지만 dev.to 교차 게시의 경우, mattstratton.com이 정식 소스가 되는 것이므로, 이 필드가 예약된 목적과는 반대입니다. 따라서 교차 게시 스크립트는 습관적으로 이미 이름 붙여진 필드를 사용하려 하지 않고, 생성하는 항목에는 이 필드를 설정하지 않아야 한다는 것을 알아야 합니다.

몇 가지 이상하고 재미있는 버그들

이 글은 "이렇게 설정되었다"를 보여주는 것이지 "버그 보고서"는 아니지만, 그 과정에서 발생했던 몇 가지 실패 사례들은 너무나 재미있어서 간략하게 언급할 가치가 있습니다.

가장 인상 깊었던 것은: 게시 워크플로가 첫 게시 후 canonical_url이 변경된 모든 게시물에 대해 422 오류로 실패하기 시작했다는 것입니다. 오류 본문은 {"error":"Tried to load unspecified class: Time","status":422}였습니다. 근본 원인은 JS YAML 라이브러리의 규칙상 엄격하게 필요하지 않은 타임스탬프에서 YAML 라운드 트립이 따옴표를 조용히 삭제했기 때문입니다.

# 수정 전 (dev.to의 Ruby 백엔드에서 문제 발생)
date: 2026-07-03T15:43:16Z
# 수정 후 (따옴표 처리, 양쪽에서 문제없이 파싱됨)
date: "2026-07-03T15:43:16Z"

JS 라이브러리 규칙에 따르면 어느 쪽이든 완벽하게 유효한 YAML입니다. 하지만 dev.to의 백엔드는 Ruby이며, Ruby의 YAML 파서는 따옴표가 없는 ISO-8601 형식의 스칼라를 기본 Time 객체로 자동 감지하고, 그 안전 로더는 이를 인스턴스화하는 것을 거부했던 것이죠. "동일한" 파일 형식의 두 구현체가 베어 스트링이 무엇이 될 수 있는지에 대해 조용히 의견이 불일치했던 것입니다. canonical_url 라인만 대상으로 한 단일 라인 교체로 전체 파일을 다시 직렬화하는 대신 문제를 해결했습니다.

몇 가지 더 작은 문제점들도 빠르게 살펴보겠습니다.

  • 다른 도구에서 편집된 두 개의 파일이 Windows 줄 바꿈(CRLF)을 가져왔는데, crosspost: true\r가 불리언 true가 아닌 문자열 "true\r"로 파싱되었습니다. 이 두 게시물은 말없이 교차 게시 대상에서 누락되었죠.
  • 7부작 시리즈 중 정확히 2개의 게시물에만 dev.to의 실제 시리즈 기능이 켜져 있었는데, 이는 모든 도구화 작업 이전에 발생한 일이었습니다.
  • 동일한 시리즈 내에서 내부 링크를 재작성하는 과정에서, 오타가 난 슬러그가 다른 올바른 슬러그의 문자열 접두사가 되는 서브스트링 교체 버그가 발생했습니다. 가장 긴 슬러그부터 먼저 교체하도록 정렬하여 해결했습니다.

재미있는 것들: 기억과 얻은 것들

이 투어에 포함하기에는 딱 맞지 않지만, 정말 멋지고 주목할 만한 두 가지 작은 부분이 있습니다.

발표 사이트의 Claude Code 스킬(새 발표 추가, 스크립트 정리, 발표 기억 재동기화)은 memory.build 없이도 잘 작동했습니다. 하지만 이제는 AI 에이전트를 위한 영구적인 기억 계층인 memory.build도 포함하고 있습니다. 덕분에 콘텐츠에 대해 에이전트와 작업할 때, 제가 매번 맥락을 다시 설명할 필요 없이 에이전트가 이미 알고 있는 발표나 이전 세션에 대한 정보를 불러올 수 있게 되었습니다. 필수는 아니지만 있으면 정말 편리하죠.

그리고 /fitness는 빌드 시 Liftosaur API에서 직접 제 운동 기록을 가져옵니다. 이 기능이 존재하는 이유는 Liftosaur의 공개 프로필 페이지 자체가 제가 원하는 만큼 많은 정보를 보여주지 않았기 때문입니다. 실제 트렌드 보기나 사람들에게 전체 스토리를 보여줄 수 있는 링크가 없었죠. Liftosaur는 또한 깔끔한 JSON 형태로 운동 데이터를 반환하지 않고, 압축된 Liftoscript-텍스트 블롭을 반환합니다. 그래서 작은 파서가 그 텍스트에서 날짜, 프로그램, 운동, 세트 수를 추출합니다. 개인 최고 기록(PR)과 트렌드 스파크라인은 별도의 호출로 가져오는 것이 아니라, 파싱된 데이터에서 렌더링 시점에 계산됩니다.

남은 과제들

현재 교차 게시 트리거는 제가 실행해야 하는 수동 CLI 명령어입니다. 제가 정말 원하는 수정 사항은 이렇습니다. dev.to 워크플로가 이미 완전히 PR 중심적이기 때문에(가져오기는 PR로 도착하고, id는 동일한 푸시에 대한 자동 커밋으로 다시 기록되며, 두 번째 PR은 생성되지 않음), 교차 게시 단계도 정확히 동일한 형태를 따라야 한다는 것입니다. pull_request에 대한 GitHub Action이 crosspost: true를 감지하고, /writing/ 항목을 생성한 다음, 동일한 PR에 추가 커밋으로 푸시하는 방식 말이죠. 수동 스크립트 실행이나 중복되는 두 번째 PR 대신에요. 이 글을 쓰는 동안 이슈를 발행했습니다. 한동안 이미 존재하는 줄 알고 있었는데 아니었더군요.

제가 기대하고 있는 다른 몇 가지 과제들도 있습니다. 발표 사이트에 제가 발표했던 모든 장소의 지도를 추가하는 것인데(이벤트 데이터에 이미 위도/경도가 있어서 즐거운 오후 작업이 될 것 같습니다), 그리고 /writing/과 레거시 아카이브를 위한 실제 히어로 및 썸네일 이미지 디자인입니다. 현재 교차 게시 스크립트는 커버 이미지를 게시물 본문에 인라인으로 삽입하여 이 간극을 메우고 있지만, 적절히 렌더링하는 작업이 필요합니다. 물론 mastermain으로 바꾸거나 Node 24로 업그레이드하는 것과 같은 일반적인 유지보수 작업들도 쌓여 있지만, 읽기에 그리 재미있는 내용은 아니죠.

어떤 것도 이미 작동하는 부분들의 출시를 지연시킬 만큼 시급한 것은 없었습니다. 어차피 좋은 할 일 목록은 보통 이렇게 만들어지는 법이죠.


원문: https://dev.to/mattstratton/i-contain-multitudes-and-also-three-git-repos-33pf 수집일: 2026-07-08 01:21:58