한 번 만든 카드뉴스를 다음 실행에서 이어 쓰면, 실패할 때마다 그림을 다시 만들지 않아도 되겠다 싶었어요. 그런데 카드 11장을 완성한 뒤에도 업로드에서 한 번 막혔고, 이어진 수동 재개 두 번은 이미지를 복원하다 글을 쓰기 전에 멈췄어요.

완성한 원고와 그림을 이어 쓰려 했어요

저는 이 블로그의 글을 Codex가 작성하고 Claude가 최종 검수한 뒤, 실행기가 검사·이미지 업로드·사이트 배포를 맡도록 운영하고 있어요. 실행기는 예약 시각이나 수동 요청에 맞춰 이 순서를 진행하는 프로그램이에요. 카드뉴스를 글 앞에 붙이는 새 기준이 생기면서, 글과 이미지가 함께 맞는지도 공개 조건에 들어갔어요. 앞선 실행이 멈추면 보관한 원고와 그림을 다음 작업 폴더로 가져와 이어 가도록 했고요. 완성된 산출물을 운반하는 그 단계에서, 명령 하나와 폴더 복사 하나가 각각 길을 막았어요.

처음 멈춘 곳은 새 카드뉴스 관문이었어요

2026년 10월 10일 18:00 예약 소식 실행은 이전 기준의 작업본에서 원고를 썼어요. 그 사이 실행기에는 실제 이미지 카드뉴스를 요구하는 새 관문이 적용됐고요. 관문은 다음 오류로 발행을 멈췄어요.

새 글·수정 글에는 본문과 대조한 실제 이미지 카드뉴스가 필요합니다.

원고를 준비한 기준과 공개를 판단한 기준이 달랐던 거예요. 그래서 카드 기준과 관문을 함께 반영한 최신 작업본에서 재개하는 쪽으로 갔어요. 여기서 글에 필요한 실제 카드를 준비했어요.

18:11:12에는 기존 글 카드 보강을 수동으로 재개했어요. cost-xray 글의 카드 7장을 공개하고, 공개 HTML과 이미지 정합을 확인한 실행이에요. HTML은 브라우저가 읽는 웹페이지 내용이에요. 이 확인은 해당 글 한 편의 완료였고, 18:00에 멈춘 소식 글의 완료와는 별개였어요.

18:29:20 수동 실행은 승인된 소식 수정 계획을 지정해 다시 시작했어요. 대상은 Codex 업데이트 목차 글이었어요. 원고와 카드 11장을 완성했고 로컬 검사도 통과했지만, 이번에는 카드 내용 검사보다 뒤인 미디어 업로드에서 실패했어요. 처음의 카드 누락을 채웠는데도 끝나지 않은 셈이에요.

그림을 올리기 전에 명령이 없다고 했어요

업로드 단계에서 보인 오류의 핵심은 Get-FileHash를 실행할 수 있는 명령 이름으로 인식하지 못했다는 것이었어요. Get-FileHash는 파일 내용을 읽어 해시를 계산하는 PowerShell 명령이에요. 해시는 파일 내용에서 계산한 긴 지문 같은 값이라, 올릴 파일이 예상한 파일과 같은지 대조할 때 써요.

에이전트는 실제 실행 경로인 Node 자식 프로세스의 Windows PowerShell 5.1에서 이 오류를 재현했어요. Node는 자동화 프로그램을 실행하는 런타임이고, 자식 프로세스는 그 프로그램이 별도로 띄운 프로그램이에요. 사람이 평소 여는 터미널이 아니라, 실행기가 PowerShell을 호출하는 조건에서 명령을 확인한 거예요.

18:45:51에 커밋 ca108ce가 남았어요. 커밋은 코드 변경 한 묶음을 기록하는 단위예요. 메시지는 다음과 같았어요.

Node PowerShell의 카드 해시 계산을 표준 .NET으로 고쳐 업로드 재개

해시 계산을 PowerShell 명령 대신 .NET의 SHA256으로 바꾼 변경이에요. .NET은 여기서 파일을 읽고 해시를 계산하는 라이브러리를 제공하고, SHA256은 그 계산 방식의 이름이에요. 계산에 연 파일 스트림도 닫도록 했어요. 파일명과 실제 파일의 해시를 대조하는 검증은 그대로 유지했어요.

에이전트는 Node에서 PowerShell을 부르는 회귀 검사도 추가했어요. 회귀 검사는 고친 문제가 다시 생기는지 보는 시험이에요. 올바른 해시는 업로드 경로를 진행하고, 불일치하면 업로드 전에 막는 두 경우를 확인했어요. 어느 모듈 로딩 조건 때문에 명령을 찾지 못했는지까지 원인을 확대하기보다, 실패한 호출 조건과 대체 구현의 동작을 확인한 수정이었어요.

다음 두 번은 작성 화면까지 가지 못했어요

명령을 고친 뒤 18:46:14와 18:51:52에 같은 소식 계획을 수동으로 재개했어요. 둘 다 원고 작성 전에 중단됐어요. 앞서 보관한 이미지 폴더를 작업본으로 복원하는 단계였어요.

작성 전 이미지 복원 중 Node fs.cpSync 네이티브 충돌(-1073741819 재현)

fs.cpSync는 Node에서 폴더를 동기 방식으로 복사하는 함수예요. 동기는 호출한 자리에서 복사가 끝날 때까지 기다리는 방식이에요. 네이티브 충돌은 JavaScript의 평범한 오류 처리로 끝난 것이 아니라, 런타임 내부 실행에서 프로세스가 중단됐다는 뜻으로 기록됐어요. -1073741819는 그때 재현된 프로세스 종료 코드예요.

같은 파일을 단일 파일 복사 함수인 copyFileSync로 복사했을 때는 정상이고, 비동기 폴더 복사인 fs.promises.cp도 정상이라는 결과가 남았어요. 파일 자체를 다시 만드는 대신 복사 방식과 실행 환경을 대조할 근거가 생겼어요. 재현 환경은 Node 22.17이었어요. 이 비교는 해당 환경의 복사 경로를 좁힌 것이고, 한글 경로나 모든 Windows 환경을 충돌 원인으로 확정한 결과는 아니에요.

별도로 18:45:01에 시작한 예약 카드 보강에서도 같은 종류의 충돌이 확인됐어요. GA4 글의 카드 5장을 작성·검수·배포한 뒤 이미지를 보존하다 중단된 경우예요. 소식 재개는 작성 전 복원, GA4 보강은 배포 후 보존에서 멈췄어요. 같은 복사 함수가 작업의 앞과 뒤에 놓여 있었던 거예요.

18:56:18에 커밋 48343e8이 남았어요.

이미지 폴더를 비동기로 복사해 Windows Node 충돌과 종료 누락을 수정

핵심 변경은 폴더 복사를 다음 형태로 바꾼 것이었어요. 아래 source와 destination은 각각 가져올 폴더와 복사할 폴더를 가리켜요.

import { cp } from 'node:fs/promises';

await cp(source, destination, { recursive: true });

recursive: true는 폴더 안의 하위 폴더까지 복사한다는 옵션이에요. await는 비동기 복사가 완료될 때까지 다음 단계로 넘어가지 않게 기다려요. 함수 이름만 바꾼 것이 아니라, 복원을 요청한 실행기와 검사도 그 함수의 완료를 기다리도록 연결했어요. 카드 복원·원본 보존·로컬 미디어 복사·종료 시 보존에 이 흐름을 적용했어요.

시작 시각 순으로 보면, 완료는 서로 겹쳐요

관련 실행을 모두 시작 시각 순서로 놓으면 아래와 같아요. 시각은 10월 10일 한국 시각이고, 예약·수동은 각 실행 기록의 시작 방식이에요. 소식 수동 재개는 같은 승인 계획을 지정해 돌린 실행이에요.

시작 시작 방식 실행 ID 확인된 단계와 결과
18:00:00 예약 20261010180000-news 이전 기준 원고가 새 카드 관문에서 중단
18:11:12 수동 20261010181112-cardnews cost-xray 카드 7장 공개 정합 확인
18:29:20 수동 20261010182920-news 소식 카드 11장 완성 뒤 업로드 명령 실패
18:45:01 예약 20261010184501-cardnews GA4 카드 5장 배포 후 이미지 보존 중 충돌
18:46:14 수동 20261010184614-news 작성 전 이미지 복원 중 충돌
18:51:52 수동 20261010185152-news 작성 전 이미지 복원 중 충돌 재발
18:56:22 수동 20261010185622-news 기존 11장 복원·검수·업로드·배포·공개 확인
19:01:34 수동 20261010190134-cardnews GA4 기존 5장의 배포 확인만 재개

18:56:22에 시작한 소식 재개는 19:06:49에 끝났어요. 19:01:34에 시작한 GA4 확인은 19:03:42에 끝났고요. 두 작업이 겹쳐 진행된 거예요.

소식은 카드 관문 중단 → 카드 11장 준비 → 업로드 명령 실패 → 명령 수정 → 복원 충돌 두 번 → 복사 방식 수정 → 기존 카드 재사용 → 공개 정합 확인으로 이어졌어요. 11장은 마지막 재개에서 새로 그린 그림 수가 아니라, 앞서 만들어 보존한 카드 수예요. 새 이미지 생성은 0건이었어요.

마지막 재개에서도 실행기가 처음 건넨 준비본은 카드가 10장이던 이전 묶음이었어요. 작성 에이전트는 18:29 실행 작업본에 남은 최신 11장을 Python 파일 복사로 가져왔고, 이전 검수본과 파일 바이트가 같은지 대조했어요. 원고는 현재 공개본과 중단 때 보존한 사본을 줄 단위로 비교해 변경 14곳을 병합했고, 카드 전체를 그 본문과 다시 대조했어요.

최종 소식 배포의 SHA는 6485bfa였어요. SHA는 여기서 배포에 사용한 Git 커밋 식별자를 줄여 쓴 값이에요. 공개 주소는 HTTP 200으로 응답했고 카드 11장의 정합도 확인했어요. 페이지 응답뿐 아니라 카드와 본문도 함께 확인했어요.

GA4 확인 실행은 기존 5장을 다시 만들지 않고 공개 상태만 확인했어요. 배포 SHA는 3c348e2, 응답은 HTTP 200이었고 종료와 잠금 해제까지 남았어요. 잠금은 같은 작업이 동시에 겹쳐 실행되는 것을 막는 표시예요. 사이트가 열리는 것과 다음 예약을 막던 잠금이 풀리는 것은 서로 다른 완료 조건이었어요.

다음 재개에서는 실행 환경부터 비교해요

같은 파일을 다른 복사 방식으로 옮겼을 때는 정상이었어요. 그래서 그림을 다시 만들기 전에 실행기가 호출한 PowerShell과 Node의 조건을 비교했어요. PowerShell 환경은 다음처럼 확인할 수 있어요. sample.webp는 자기 시스템에 있는 시험용 파일로 바꿔요.

$PSVersionTable.PSVersion
Get-Command Get-FileHash -ErrorAction Stop
Get-FileHash -LiteralPath .\sample.webp -Algorithm SHA256

자동화가 별도 프로세스를 띄운다면 그 경로에도 같은 점검을 넣어 결과를 비교해요. 설치된 프로그램 이름만 확인하는 것보다, 실패한 실행 조건을 같이 적으면 대체 구현을 검증하기 쉬워져요.

비동기 함수는 완료를 나타내는 Promise를 돌려줘요. 호출자가 기다리지 않으면 복사 중인 폴더를 검사하거나, 보존이 끝나기 전에 작업본을 정리할 수 있어요. 이번에는 복사 함수와 호출자의 완료 대기를 함께 고쳤고, 복원·보존 경로를 다음 회귀 검사로 확인했어요.

node --test ops/runner/cardnews.test.mjs ops/runner/post-artifacts.test.mjs

카드 그림은 다시 만들지 않았어요. 운반하는 명령과 복사 완료를 기다리는 순서를 고친 뒤 기존 카드 11장이 공개됐어요. 재개가 끝났다는 확인은 그림 생성이 아니라 공개 페이지·카드와 본문의 일치·정상 종료까지 이어졌어요.