쓰레드·인스타 자동 발행 두 개를 동시에 돌렸다가 기록이 사라졌다

·

블로그에 새 글 3편을 쓰고, 쓰레드용 발행 스크립트와 인스타용 발행 스크립트를 각각 실행했다. 둘 다 처리 시간이 길어서 백그라운드로 넘어갔고, 거의 동시에 돌았다. 둘 다 성공 로그를 찍었는데, 나중에 확인해보니 상태 파일에는 쓰레드 발행 기록이 없었다.

증상 — 게시는 됐는데 기록이 없다

Instagram Graph API로 재조회해서 3편이 실제로 게시된 것까지 직접 확인했다. 쓰레드도 실행 로그에 미디어 ID가 남아 있었다. 그런데 .social-queue/state.json을 열어보니 이랬다.

["writesonic-geo-pivot-2026", "elevenlabs-free-plan-commercial-use", "digitalocean-vs-cloudflare-static-site-cost"]
  .forEach(slug => {
    console.log(slug, "| threads:", state.published.threads.includes(slug),
                       "| instagram:", state.published.instagram.includes(slug));
  });

// writesonic-geo-pivot-2026 | threads: false | instagram: true
// elevenlabs-free-plan-commercial-use | threads: false | instagram: true
// digitalocean-vs-cloudflare-static-site-cost | threads: false | instagram: true

세 글 다 인스타는 기록이 있는데 쓰레드는 전부 false였다. 분명 쓰레드 실행 로그에 “게시 완료. media id: …”가 세 번 다 찍혀 있었는데도 그랬다.

원인 — 상태 저장 코드를 보니 답이 나왔다

발행 스크립트의 상태 관리 코드는 이렇게 생겼다.

function loadState(): State {
  if (!existsSync(STATE_FILE)) return { processed: [] };
  try {
    return JSON.parse(readFileSync(STATE_FILE, "utf-8")) as State;
  } catch {
    return { processed: [] };
  }
}
function saveState(state: State): void {
  mkdirSync(OUT_DIR, { recursive: true });
  writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}

async function main(): Promise<void> {
  const state = loadState();   // 시작할 때 딱 한 번 읽는다
  // ... 이 사이에서 발행 처리하며 state 객체를 메모리에서 수정 ...
  saveState(state);             // 끝날 때 딱 한 번, 메모리 내용을 파일 전체에 덮어쓴다
}

loadState()는 프로세스 시작 시 딱 한 번 파일을 읽고, saveState()는 끝날 때 딱 한 번 메모리에 있는 객체 전체를 파일에 그대로 덮어쓴다. 이게 바로 read-modify-write 패턴이다.

두 프로세스를 동시에 실행하면 순서가 이렇게 된다.

시각 0:  쓰레드 프로세스 시작 → state.json 읽음 (threads: [], instagram: [])
시각 0:  인스타 프로세스 시작 → state.json 읽음 (threads: [], instagram: [])   ← 같은 시점의 같은 내용
시각 5:  쓰레드 프로세스 끝 → 메모리의 state에 threads: [글1,글2,글3] 추가 후 파일에 씀
시각 8:  인스타 프로세스 끝 → 자기 메모리의 state(시각 0에 읽은 것)에 instagram: [글1,글2,글3]만
         추가한 뒤, 그 메모리 내용 전체로 파일을 다시 씀
결과:    파일에는 instagram만 있고 threads는 사라짐 (인스타 프로세스의 메모리에는
         쓰레드가 시각 5에 써놓은 내용이 반영돼 있지 않았으므로)

나중에 끝난 프로세스가 항상 이긴다. 자기가 시작할 때 읽은 옛 내용을 기준으로 파일 전체를 다시 쓰기 때문에, 그 사이에 다른 프로세스가 써놓은 내용은 통째로 지워진다. 파일 잠금(lock)이 없는 read-modify-write 코드에서는 전형적으로 나는 문제다.

왜 지금까지 안 걸렸나

이 자동화는 원래 한 플랫폼씩 순서대로 실행하는 게 기본 사용 패턴이었다. 순차 실행이면 프로세스가 겹칠 일이 없어서, 코드에 이 결함이 있어도 드러나지 않는다. 이번엔 우연히 “쓰레드용 명령”과 “인스타용 명령”을 같은 시점에 각각 실행했고, 둘 다 처리 시간이 길어 백그라운드로 넘어가면서 실제로 겹쳐 돌았다. 잠재된 버그가 실행 패턴이 바뀌자 드러난 경우다.

어떻게 대응했나

이 스크립트는 사용 빈도가 낮은 개인 운영 자동화라, 파일 락 라이브러리를 새로 넣는 대신 두 가지로 대응했다.

  1. 즉시 대응: Instagram API 재조회로 실제 발행 여부를 다시 확인하고, 사라진 쓰레드 기록 3건을 수동으로 상태 파일에 복구했다.
  2. 재발 방지: 코드에 원인을 주석으로 남기고, 앞으로는 --only 플래그로 플랫폼을 나눠 실행할 때 동시에 실행하지 않고 순차로 실행하는 것을 운영 규칙으로 정했다.
// 파일 락이 없다 — read-modify-write라 --only threads와 --only instagram을 동시에
// 백그라운드로 돌리면 나중에 끝난 프로세스가 먼저 끝난 쪽의 기록을 통째로 덮어쓴다.
// --only 여러 개를 쓸 땐 순차로 실행할 것.

정리

같은 파일을 여러 프로세스가 “읽고 고치고 전체를 다시 쓰는” 방식으로 다루면, 동시에 실행되는 순간 나중에 끝난 쪽이 먼저 쓴 내용을 지운다. 이건 특별히 복잡한 동시성 버그가 아니라 read-modify-write의 가장 기본적인 함정이다. 자동화 스크립트를 병렬로 돌릴 때는 “이 스크립트가 상태를 파일에 어떻게 저장하는가”부터 확인하는 게 먼저다 — 순차 실행에서 멀쩡하던 코드가 병렬 실행에서 조용히 데이터를 지울 수 있다.

자주 묻는 질문

왜 이 문제가 지금까지 안 걸렸나요?

이 자동화는 보통 한 번에 한 플랫폼씩 순서대로 실행했습니다. 이번엔 쓰레드용과 인스타용을 각각 별도 명령으로 동시에 실행했는데, 두 프로세스가 모두 실행 시간이 길어 백그라운드로 넘어갔고, 그 상태에서 겹쳐 돌았습니다. 순차 실행에서는 애초에 동시에 파일을 건드릴 일이 없어서 드러나지 않았던 구조적 결함입니다.

실제 게시물도 잘못됐나요?

아닙니다. 쓰레드와 인스타 양쪽 다 API로 실제 게시 여부를 재조회해서 확인했고, 게시물 자체는 정상이었습니다. 문제는 '어떤 글이 어느 플랫폼에 이미 발행됐는지' 추적하는 로컬 상태 파일에서만 발생했습니다.

이 문제를 방치하면 어떤 위험이 있나요?

이 자동화는 상태 파일을 보고 '아직 발행 안 된 글'을 찾아 다음 실행 때 자동으로 올립니다. 발행 기록이 사라지면 이미 올라간 글을 다시 발행 대상으로 오인해서, 다음 실행 때 같은 글이 중복으로 게시될 위험이 있습니다.

완전히 고치려면 뭐가 필요한가요?

정식으로 고치려면 파일 락(예: proper-lockfile 같은 라이브러리)이나 원자적 쓰기, 혹은 SQLite 같은 실제 동시성을 지원하는 저장소로 옮기는 방법이 있습니다. 이번에는 사용 빈도가 낮은 개인 자동화 스크립트라 코드에 원인을 주석으로 남기고 '동시에 실행하지 않는다'는 운영 규칙으로 대응했습니다.

#race condition#Node.js#자동화#버그#동시성