ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 탭 두 개가 같은 작업을 동시에 할 때: Web Locks의 범위
    Programming 2026. 10. 4. 00:49
    728x90
    반응형

    탭 A가 처리하는 동안 탭 B가 대기한 뒤 현재 목록을 읽는 시간 흐름.
    Web Locks가 조정하는 구간입니다. 다른 기기와 서버의 중복 처리는 별도 설계가 필요합니다.

    같은 출처의 협력하는 작업은 Web Locks로 조정하되, 서버·다른 기기까지 잠겼다고 가정하지 마세요.

    메모 앱을 두 탭에서 열면 두 탭 모두 미전송 메모를 발견할 수 있습니다. 각 탭에 isRunning 변수를 두어도 상대 탭의 상태는 알 수 없습니다. 이럴 때 같은 이름의 Web Lock을 요청하면 한쪽이 작업하는 동안 다른 쪽을 기다리게 할 수 있습니다.

    다만 기다린 탭이 나중에 오래된 목록을 그대로 보내면 중복 전송은 남습니다. 이 글에서는 잠금을 얻는 코드보다 그 안에서 언제 목록을 읽고, 언제 완료로 처리하는지까지 연결해 보겠습니다. 2026년 9월 28일 확인한 명세를 바탕으로 하며, 코드는 실제 서버에 연결하지 않은 설계 예시입니다.

    탭 A가 먼저 끝나도 탭 B의 목록은 낡아 있습니다

    미전송 메모가 하나 있다고 해보겠습니다. 다음 순서에는 문제가 있습니다.

    1. A와 B가 각자 메모를 읽습니다.
    2. A가 잠금을 얻어 전송하고 완료 표시를 남깁니다.
    3. B가 잠금을 얻어 자신이 앞서 읽은 메모를 전송합니다.

    동시에 실행된 구간은 없는데 같은 메모가 두 번 전송됐습니다. 목록을 읽는 시점이 잠금 밖에 있었기 때문입니다. B는 잠금을 얻은 뒤 현재 미완료 목록을 읽어야 A의 처리 결과를 반영할 수 있습니다.

    따라서 보호할 작업은 전송 함수 호출 하나보다 넓습니다. 현재 상태 읽기, 처리할 대상 고르기, 전송 결과 반영이 하나의 순서로 이어져야 합니다. 저장소와 네트워크 전체가 원자적 트랜잭션이 되는 것은 아니지만, 적어도 두 탭이 낡은 목록을 동시에 준비하는 문제는 줄일 수 있습니다.

    같은 이름을 쓰는 코드끼리 순서를 맞춥니다

    앱은 notes:outbox처럼 자원을 나타낼 이름을 정합니다. 기본 배타 모드에서는 그 이름의 잠금을 한 작업이 가지고 있는 동안 다른 요청이 기다립니다. 브라우저는 이름의 업무 의미를 해석하지 않으므로 다른 코드가 outbox:notes를 쓰면 별도 잠금입니다. Web Locks의 자원 이름과 모드

    이 방식은 참여 코드의 협력이 필요합니다. 잠금 없이 같은 메모를 수정하는 경로까지 브라우저가 금지하지는 않습니다. 앱의 자동 동기화, 수동 재전송, 정리 작업 중 같은 목록을 건드리는 경로가 어디인지 먼저 찾고 이름을 맞춥니다.

    범위도 브라우저 안으로 한정됩니다. 명세상 같은 사용자 에이전트에서 storage bucket을 공유하는 실행 맥락끼리 조정합니다. 같은 출처(origin)이며 같은 저장소 범위를 공유하는 탭과 워커가 대상이고, 다른 프로필·비공개 세션은 분리됩니다. 휴대전화와 노트북의 서버 요청을 한 줄로 세우는 잠금은 아닙니다. 명세의 조정 범위

    콜백이 실제 작업의 끝을 기다려야 합니다

    아래 함수는 앱이 구현한 세 함수를 받아 동기화 순서를 묶습니다.

    async function syncOutbox({
      readPending, send, markSent
    }) {
      if (!navigator.locks) {
        throw new Error(
          "Web Locks 사용 불가"
        );
      }
    
      return navigator.locks.request(
        "notes:outbox", async () => {
        const pending =
          await readPending();
        for (const item of pending) {
          await send(item);
          await markSent(item.id);
        }
      });
    }
    

    readPending은 두 탭이 공유하는 저장소에서 현재 목록을 읽어야 합니다. 탭 내부 메모리에 남은 배열을 반환한다면 잠금 안으로 옮겨도 오래된 상태일 수 있습니다. send는 전송 결과를 확인한 뒤 완료돼야 하고, markSent도 공유 상태에 기록이 끝나야 합니다.

    잠금은 콜백이 반환한 Promise가 완료될 때 해제됩니다. 그래서 위 코드는 각 비동기 작업을 기다립니다. 다음처럼 전송을 시작만 하면 수명이 끊어집니다.

    navigator.locks.request(
      "notes:outbox", () => {
        // Promise를 반환하지 않은 예
        sendPending();
      }
    );
    

    return sendPending()으로 Promise를 반환하거나 async 콜백에서 기다려야 합니다. 내부 함수도 자신이 시작한 비동기 작업을 제대로 기다려야 합니다. 바깥 함수의 async 한 단어가 내부의 모든 작업을 자동으로 추적하지는 않습니다. request의 반환값과 잠금 해제

    다른 탭이 처리 중이면 이번 시도를 건너뛸 수도 있습니다

    정기 동기화처럼 곧 다시 기회가 생기는 작업은 무조건 줄을 설 필요가 없을 수 있습니다. ifAvailable: true를 주면 즉시 획득할 수 없을 때 콜백에 null이 전달됩니다.

    const result =
      await navigator.locks.request(
      "notes:outbox",
      { ifAvailable: true },
      async (lock) => {
        if (!lock) return "skipped";
        await syncCurrentPending();
        return "completed";
      }
    );
    

    이 코드의 syncCurrentPending은 잠금을 다시 요청하지 않고 현재 목록을 처리하는 앱 함수라고 가정합니다. 같은 잠금을 안에서 또 기다리면 자신의 해제를 기다리는 구조가 될 수 있습니다.

    결과가 skipped면 이번 탭은 처리하지 않았다는 뜻입니다. 다른 탭의 성공을 확인한 것이 아니므로 UI에 동기화 완료로 표시하지 않습니다. 반드시 수행해야 하는 저장 버튼이라면 건너뛰기보다 대기 상태와 최종 결과를 보여 주는 편이 요구에 맞습니다.

    서버에 저장됐는데 응답을 못 받으면

    A의 요청이 서버에 반영된 직후 탭이 닫히면 로컬 완료 표시가 남지 않을 수 있습니다. 이후 B가 잠금을 얻어 목록을 읽으면 그 메모는 여전히 미전송입니다. B가 재전송할 때 같은 작업이라는 것을 서버가 구분할 수 있어야 합니다.

    이 예시의 서버 설계에서는 메모 변경마다 안정적인 작업 ID를 부여하고, 같은 ID가 다시 들어오면 중복 반영을 막는 정책을 둘 수 있습니다. ID의 보관 기간, 실패 재시도와 충돌 처리는 앱 요구로 정합니다. Web Locks만 넣고 정확히 한 번 전송을 약속할 수 없는 이유가 여기에 있습니다.

    대기 요청의 취소와 실제 전송 취소도 따로 봅니다. request()의 AbortSignal은 아직 얻지 못한 잠금을 기다리는 요청을 취소하는 데 씁니다. 이미 실행 중인 네트워크 작업이나 서버 반영을 되돌리는 기능은 아닙니다.

    두 탭에서 확인할 것은 실행 순서와 남은 상태입니다

    HTTPS 등 보안 컨텍스트와 기능 지원을 확인한 뒤, 비민감 샘플로 두 탭이 같은 잠금 이름을 쓰게 합니다. A의 작업을 길게 유지했을 때 B가 기다리는지, A가 완료한 메모를 B가 다시 읽어 전송하지 않는지 봅니다. 전송 실패와 탭 종료 뒤에는 남은 목록을 따로 확인합니다.

    기다림이 끝나지 않으면 navigator.locks.query()로 보유·대기 중인 잠금을 조사할 수 있습니다. 조회 결과는 그 순간의 상태이므로 완료되지 않은 Promise나 중첩 잠금의 획득 순서와 함께 읽습니다. 잠금 상태 조회

    이 검사가 끝나면 다른 기기에서 같은 요청이 들어오는 경우를 서버 정책으로 확인하세요. 두 탭의 순서는 Web Locks로 맞추고, 이미 반영된 작업을 다시 받을 때의 처리는 서버에서 확인해야 메모 동기화의 빈틈을 좁힐 수 있습니다.

    728x90
    반응형
Designed by Tistory.