SW 게스트 학습 동기화에서 HTTP 409 충돌을 성공으로 반환하는 오류 해결

이 글의 목차 9개 항목
  • 분야: DB
  • 기술 스택: TypeScript · Cloudflare Workers · Cloudflare D1(SQLite)

문제와 원인

로그인 뒤 POST /api/sw-study의 sw-import는 게스트의 이론 진도를 먼저 저장하고 활성 세션을 저장했다. 세션 저장 함수가 HTTP 409를 반환해도 상위 분기의 status !== 409 조건이 실패 반환에서 제외해 마지막 HTTP 200 성공 응답으로 넘어갔다.

누락·변경 문항, 오래된 revision, 다른 활성 세션, 제출된 세션의 재활성화가 충돌 조건이었다. 클라이언트도 .catch(() => undefined)로 가져오기 오류를 소거하고 계정 조회로 넘어갔다.

수정 전 코드

sw-import

세션 저장의 HTTP 409를 실패 반환에서 제외했다.

const importedTheoryProgress = await repository.importTheoryProgress(
  userKey,
  completedTheoryIds,
  new Date().toISOString(),
);

if (payload.activeSession && typeof payload.activeSession === "object") {
  const session = payload.activeSession as Record<string, unknown>;
  const imported = await saveLearningSession(request, repository, userKey, {
    ...session,
    sessionId: session.id,
    action: "sw-session-save",
  }, "active");
  if (!imported.ok && imported.status !== 409) return imported;
}
return response(request, { ok: true, importedTheoryProgress }, "private-session");

계정 동기화

가져오기 오류를 소거한 뒤 계정 조회를 이어갔다.

void requestSwStudyMutation("sw-import", {
  completedTheoryIds: local.completedTheoryIds,
  activeSession: local.activeSession,
}, controller.signal).catch(() => undefined).then(async () => {
  if (controller.signal.aborted) return;
  const account = await requestSwStudyData<SwAccountState>({
    view: "state",
    cacheMode: "none",
    signal: controller.signal,
  });
  // 계정 상태 반영 부분 생략
}).catch(() => {
  if (!cancelled && !controller.signal.aborted) {
    setContentError("계정의 SW 학습 기록을 동기화하지 못했습니다. 이 브라우저의 기록은 유지됩니다.");
  }
});

수정 후 코드

sw-import

모든 세션 저장 실패를 즉시 반환하고 원래 상태·헤더·오류 코드와 이론 진도 반영 수를 함께 보낸다. sessionImport.status는 saved, failed, not-requested로 세션 처리 결과를 구분한다.

if (payload.activeSession && typeof payload.activeSession === "object") {
  const session = payload.activeSession as Record<string, unknown>;
  const imported = await saveLearningSession(request, repository, userKey, {
    ...session,
    sessionId: session.id,
    action: "sw-session-save",
  }, "active");
  if (!imported.ok) {
    const failure = await imported.json() as Record<string, unknown>;
    return Response.json({
      ...failure,
      ok: false,
      importedTheoryProgress,
      sessionImport: { status: "failed", code: failure.code },
    }, { status: imported.status, headers: imported.headers });
  }
}
return response(request, {
  ok: true, importedTheoryProgress,
  sessionImport: { status: payload.activeSession ? "saved" : "not-requested" },
}, "private-session");

이론 1개가 먼저 저장된 뒤 누락 문항으로 세션 저장이 거절되면 HTTP 409와 다음 본문을 반환한다.

{
  "ok": false,
  "code": "SW_SESSION_CONTENT_CHANGED",
  "importedTheoryProgress": 1,
  "sessionImport": {
    "status": "failed",
    "code": "SW_SESSION_CONTENT_CHANGED"
  }
}

createSwAccountSync.run

React 계정 동기화는 가져오기 성공 후 계정 조회와 로컬 적용을 진행하도록 연결했다. 가져오기 실패에는 오류를 표시하고 브라우저 기록을 보존한다. 가져오기 성공 뒤 계정 조회만 실패한 경우에는 조회부터 재시도한다.

pending = Promise.resolve().then(async () => {
  if (!isCurrent()) return false;
  const snapshot = options.snapshot();
  if (snapshot !== importedSnapshot) {
    await options.importSnapshot(snapshot, current.signal);
    if (!isCurrent()) return false;
    importedSnapshot = snapshot;
  }
  const account = await options.readAccount(current.signal);
  if (!isCurrent()) return false;
  options.applyAccount(account);
  complete = true;
  options.onError(null);
  return true;
}).catch((error: unknown) => {
  if (isCurrent()) options.onError(error);
  return false;
}).finally(() => {
  if (controller !== current) return;
  pending = undefined;
  controller = undefined;
  options.onBusy(false);
});

검증 결과

같은 학습 데이터의 API 비교

Swagger UI에서 수정 전후 원본 서비스를 다시 실행했다. 같은 초기 학습 데이터와 로컬 SQLite·D1 호환 어댑터를 사용한 합성 계정으로, 네 충돌 조건은 각 1회, 중복 요청 조건은 2회 실행했다.

입력수정 전수정 후
누락 문항을 포함한 활성 세션HTTP 200, ok: trueHTTP 409, ok: false, SW_SESSION_CONTENT_CHANGED
오래된 revisionHTTP 200, ok: trueHTTP 409, ok: false, SW_SESSION_CONFLICT
다른 활성 세션HTTP 200, ok: trueHTTP 409, ok: false, SW_SESSION_CONFLICT
제출된 세션을 활성 세션으로 가져오기HTTP 200, ok: trueHTTP 409, ok: false, SW_SESSION_CONFLICT
네 충돌을 전체 성공으로 응답한 건수4/4건0/4건
이론 1개 + 누락 문항 세션을 동일하게 2회 요청이론 1행, 세션 0행, 성공 응답이론 1행, 세션 0행, 단계별 실패 응답
문항을 유효한 1개로 교체 후 재요청HTTP 200, 세션 1행HTTP 200, saved, 세션 1행
SW 원본 서비스 재실행: 충돌 4건 HTTP 409, 중복 저장 방지와 복구 후 세션 1행원본 보기 (새 창)

SW 원본 서비스 재실행: 충돌 4건 HTTP 409, 중복 저장 방지와 복구 후 세션 1행

100%원본 보기 (새 창)

확대한 이미지는 상하좌우로 이동할 수 있습니다. 키보드는 이미지 영역에 초점을 두고 방향키를 사용하세요. Esc로 닫습니다.

revision·활성 세션·제출 세션 충돌에서는 기존 서버 행을 보존했다.