ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 브라우저 OPFS의 파일은 다운로드 폴더 어디에 있을까
    Programming 2026. 9. 27. 01:07
    728x90
    반응형

    작은 서랍이 들어 있는 열린 나무 수납장과 그 옆의 문서철
    웹사이트 내부 저장 공간과 사용자가 꺼내 받는 파일을 구분한 생성 이미지입니다.

    OPFS는 사이트 내부 저장소로 이해하고, 사용자가 가져갈 파일은 별도 내보내기 기능으로 제공하세요.

    웹앱에서 파일을 저장했다는 메시지를 봤는데 다운로드 폴더에는 없습니다. 저장에 실패한 것일 수도 있지만, 앱이 OPFS에 기록한 것이라면 사용자 폴더를 찾는 질문부터 달라져야 합니다. 이 저장소는 웹앱이 내부 작업 파일을 다루기 위한 공간입니다.

    이 글은 2026년 9월 13일 MDN 문서 기준입니다. 예시는 기능을 지원하는 보안 컨텍스트의 별도 실습 페이지를 가정합니다. 실제 브라우저 저장소에 파일을 만들거나 다운로드·삭제하지 않았습니다.

    OPFS의 private는 사이트별 내부 공간이라는 뜻입니다

    Origin Private File System은 페이지의 출처에 속한 저장소입니다. 일반 파일 선택기로 고른 사용자 문서와 달리 파일 탐색기에서 직접 찾아 다루도록 설계되지 않았습니다. 브라우저가 디스크에 보관하더라도 앱에서 만든 이름과 운영체제 파일이 일대일로 대응한다고 기대하지 않습니다. OPFS의 저장 모델

    따라서 앱이 notes.txt를 저장했다고 화면에 표시하더라도 다운로드 폴더에 notes.txt가 생겼다는 의미는 아닙니다. “이 브라우저에 저장”과 “파일로 내보내기”를 같은 버튼 문구로 쓰면 사용자가 보관 위치를 오해할 수 있습니다.

    출처는 주소의 스킴·호스트·포트와 관련된 경계입니다. 개발 주소와 운영 주소, 서로 다른 포트에서 같은 앱을 열었다고 이전 작업 파일을 그대로 읽는다고 가정하지 않습니다. 브라우저 프로필이나 기기가 바뀌는 경우도 별도 저장 환경입니다.

    또한 private라는 단어는 파일 내용의 암호화나 비밀 보관을 자동으로 보장한다는 뜻이 아닙니다. 이 출처의 앱 코드가 읽을 수 있는 데이터이므로, 같은 출처에서 실행되는 코드의 신뢰성과 민감 자료 처리 정책은 따로 필요합니다.

    내부 저장과 사용자 파일 수정은 다른 흐름입니다

    사용자가 파일 선택기를 통해 문서를 고르고 해당 파일에 대한 접근을 허용하는 흐름은 사용자에게 보이는 파일 시스템과 연결됩니다. OPFS의 루트 디렉터리를 얻는 흐름은 앱 내부 저장소에 연결됩니다.

    둘 다 FileSystemFileHandle을 다룰 수 있다는 이유만으로 같은 위치라고 판단하지 않습니다. 핸들을 어디에서 얻었는지가 저장 대상의 의미를 결정합니다. OPFS와 사용자 파일 접근의 차이

    가령 텍스트 편집기의 자동 저장은 내부 작업 복구를 위해 OPFS를 사용할 수 있습니다. 반면 사용자가 다른 앱에 보낼 문서는 별도 내보내기로 만들어야 합니다. 자동 저장이 성공했다는 이유로 외부 전달 파일까지 최신이라고 표시하면 안 됩니다.

    이 구분은 화면 문구에도 반영할 수 있습니다. “이 브라우저에 저장됨”, “내보낼 파일 준비됨”, “사용자가 내려받은 파일 확인 필요”처럼 실제로 확인한 단계만 안내합니다. 웹페이지가 알 수 없는 최종 다운로드 위치나 디스크 기록을 성공으로 지어내지 않습니다.

    쓰기 요청을 보낸 것과 닫기 완료를 구분합니다

    메인 스레드에서는 비동기 API로 루트와 파일 핸들을 얻고 writable stream에 기록할 수 있습니다. createWritable로 만든 스트림의 변경은 닫기가 완료돼야 파일에 반영됩니다. write 호출만 기다리고 저장 완료를 표시하지 않습니다. createWritable의 반영 시점

    아래는 작은 가상 메모를 새 이름으로 저장하고 다시 읽는 함수입니다. 기존 사용자 파일을 덮어쓰지 않도록 실습용 이름에 임의 식별자를 붙였습니다.

    async function saveDemoNote() {
      if (!globalThis.isSecureContext ||
          typeof navigator.storage?.getDirectory !== "function") {
        throw new Error("이 환경에서는 OPFS 실습을 사용할 수 없습니다.");
      }
    
      const root = await navigator.storage.getDirectory();
      const name = `opfs-demo-${crypto.randomUUID()}.txt`;
      const handle = await root.getFileHandle(name, { create: true });
      const writer = await handle.createWritable();
      const text = "내보내기 동작을 설명하는 가상 메모입니다.\n";
    
      try {
        await writer.write(text);
        await writer.close();
      } catch (error) {
        await writer.abort().catch(() => {});
        throw error;
      }
    
      const saved = await handle.getFile();
      if (await saved.text() !== text) {
        throw new Error("저장 후 다시 읽은 내용이 예상과 다릅니다.");
      }
      return { handle, name };
    }
    

    호출하는 화면은 함수가 성공했을 때만 내부 저장 완료를 표시하고, 실패하면 입력 내용을 유지한 채 오류를 안내해야 합니다. 함수를 정의한 코드와 실제로 버튼에 연결해 예외를 처리한 앱은 다르므로 여기에 실행 결과를 붙이지 않았습니다.

    이 예시는 close 이후 같은 핸들에서 읽은 내용을 비교합니다. 브라우저를 재시작한 뒤의 복구나 다른 기기에서의 접근까지 검증한 것은 아닙니다. 실패 시 파일 항목이 비어 있는 상태로 남을 수도 있으며, abort를 시도했다고 모든 흔적이 제거됐다고 말하지 않습니다.

    이름마다 새 파일을 만드는 방식도 실습을 위한 선택입니다. 운영 자동 저장에서 무한히 새 파일을 쌓는 정책으로 사용하지 않습니다. 기존 문서의 버전 관리와 동시에 열린 탭의 충돌 처리는 별도로 설계해야 합니다.

    사용자가 가져갈 복사본은 별도로 준비합니다

    내부 파일에서 getFile을 호출하면 File 객체를 얻습니다. File은 Blob의 한 종류이므로 임시 blob URL을 만들어 다운로드 링크에 연결할 수 있습니다. 이때 생기는 것은 OPFS 경로를 공개한 주소가 아니라 해당 객체를 참조하는 임시 URL입니다. blob URL의 용도

    다음 함수는 앞에서 받은 handle과 링크를 넣을 화면 요소를 인자로 받습니다. 실제 다운로드를 자동 실행하지 않고 사용자가 누를 수 있는 링크를 만듭니다.

    async function offerExport(handle, container) {
      if (!(container instanceof HTMLElement)) {
        throw new TypeError("다운로드 링크를 표시할 요소가 필요합니다.");
      }
      const file = await handle.getFile();
      const url = URL.createObjectURL(file);
      const link = document.createElement("a");
      link.href = url;
      link.download = "draft-note.txt";
      link.textContent = "메모 파일 내려받기";
      container.append(link);
    
      return function disposeExport() {
        link.remove();
        URL.revokeObjectURL(url);
      };
    }
    

    호출자는 돌려받은 정리 함수를 보관했다가 내보내기 UI를 없애거나 새 링크로 교체할 때 호출합니다. 링크를 보여 준 직후 URL을 해제하면 사용자가 누르기 전에 사용할 수 없게 될 수 있습니다. 반대로 필요가 끝난 URL을 계속 유지하면 관련 객체가 메모리에 남습니다.

    사용자가 링크를 누른 직후 정리해야 한다는 고정 타이머도 여기에는 넣지 않았습니다. 링크를 더 이상 제공하지 않는 UI 수명과 연결하는 것이 이 예시의 전제입니다. 내보내기를 다시 준비할 때는 이전 링크를 정리하는 호출부가 필요합니다.

    다운로드 속성의 이름은 제안값입니다. 최종 파일명이나 저장 위치, 다운로드 허용 여부는 브라우저와 사용자 선택의 영향을 받습니다. 링크 생성 성공을 파일 저장 완료로 보고하지 않습니다.

    또한 내보내기 링크는 준비한 시점의 파일 객체를 대상으로 합니다. 그 뒤 메모가 바뀌었다면 최신 내용을 내보내려는 의도에 맞춰 다시 준비해야 합니다. 내부 저장 버전과 내보낸 버전이 무엇인지 화면에서 구분해 주는 편이 좋습니다.

    저장 공간은 무제한이 아니며 사이트 데이터 삭제의 영향을 받습니다

    OPFS는 브라우저 저장 용량 정책의 영향을 받습니다. 기본 저장은 best-effort이며 저장 압박이나 사용자 선택 등에 따라 데이터가 제거될 수 있습니다. 사이트 데이터를 지우면 OPFS도 삭제될 수 있습니다. 저장 용량과 제거 기준

    navigator.storage.estimate로 얻은 추정치를 앞으로 반드시 쓸 수 있는 공간의 예약으로 해석하지 않습니다. 저장 시점의 예외를 처리해야 하며, 용량이 부족한데 성공 표시만 남지 않도록 해야 합니다.

    지속 저장을 요청하는 navigator.storage.persist도 별도 정책입니다. 허용 여부를 확인해야 하고, 허용되더라도 사용자의 명시적 삭제나 기기 손상까지 막는 백업은 아닙니다.

    다른 브라우저나 새 프로필에서 작업이 보이지 않을 때 곧바로 파일 손상이라고 판단하지 않습니다. 먼저 같은 출처·프로필·기기의 저장소인지 확인합니다. 계정으로 로그인했다고 로컬 저장 데이터가 자동 동기화되는 것도 아닙니다.

    사용자가 잃으면 안 되는 문서라면 내보내기나 별도 동기화·복구 설계를 함께 제공합니다. OPFS는 앱이 사용할 저장 기능이지 보관 책임을 끝내 주는 서비스가 아닙니다.

    동기 파일 접근이 필요해도 메인 스레드에 옮기지 않습니다

    OPFS에는 워커에서 사용할 수 있는 동기 접근 경로도 있습니다. 큰 데이터나 데이터베이스 파일을 처리할 때 검토할 수 있지만, 동기 API를 메인 스레드에서 사용할 수 있다고 가정해서는 안 됩니다. 워커의 OPFS 접근

    작업용 핸들의 잠금과 수명, flush·close의 의미, 워커 종료 시점을 함께 설계해야 합니다. 비동기 메모 예시를 동기 데이터베이스의 안전한 저장 절차로 확대하지 않습니다.

    여러 탭이 같은 내부 파일을 갱신하는 문제도 저장소가 자동으로 문서 병합을 해 준다는 뜻은 아닙니다. 앱에 맞는 단일 작성자 정책이나 충돌 표시가 필요합니다. 여기서는 한 번 저장하고 내보내는 작은 흐름만 다뤘습니다.

    저장 위치와 복구 경로를 설명할 수 있어야 합니다

    OPFS 파일이 다운로드 폴더에 없다는 사실만으로 저장 실패라고 단정하지 마세요. 먼저 앱 내부 저장인지, 사용자 파일에 직접 쓴 것인지, 내보내기 복사본을 만든 것인지 구분합니다.

    앱을 만든다면 close와 다시 읽기 결과로 내부 저장 상태를 확인하고, 외부로 가져갈 파일은 별도 기능으로 제공합니다. 오류가 났을 때는 사용자의 입력을 유지하고 확인되지 않은 성공을 표시하지 않습니다.

    마지막으로 사용자가 브라우저 데이터를 지우거나 기기를 바꿀 때 무엇이 남는지 안내해야 합니다. 파일 이름 하나보다 저장한 범위, 내보낸 버전, 다시 가져올 수 있는 경로가 명확할 때 사용자가 안심하고 작업을 이어갈 수 있습니다.

    728x90
    반응형
Designed by Tistory.