하나 또는 여러 파일을 여는 방법

토마스 슈타이너
토마스 슈타이너

파일 처리는 웹에서 앱의 가장 일반적인 작업 중 하나입니다. 이전에는 사용자가 파일을 업로드하고 변경한 후 다시 다운로드하여 복사본이 다운로드 폴더에 있어야 했습니다. File System Access API를 사용하면 이제 사용자가 직접 파일을 열고 수정하고 변경사항을 원래 파일에 다시 저장할 수 있습니다.

현대적인 방식

File System Access API의 showOpenFilePicker() 메서드 사용

파일을 열려면 showOpenFilePicker()를 호출합니다. 이 메서드는 선택한 파일의 배열과 함께 프로미스를 반환합니다. 파일이 여러 개 필요한 경우 메서드에 { multiple: true, }를 전달할 수 있습니다.

브라우저 지원

  • 86
  • 86
  • x
  • x

소스

기존의 방식

<input type="file"> 요소 사용

페이지의 <input type="file"> 요소를 사용하면 사용자가 요소를 클릭하여 하나 이상의 파일을 열 수 있습니다. 이 방법은 자바스크립트를 사용하여 페이지에 보이지 않게 요소를 삽입하고 프로그래매틱 방식으로 클릭하는 것입니다.

브라우저 지원

  • 1
  • 12
  • 1
  • 1

소스

점진적 개선

아래 방법은 지원되는 경우 File System Access API를 사용하고 그 외의 경우에는 기존 접근 방식으로 대체합니다. 두 경우 모두 함수는 파일 배열을 반환하지만 File System Access API가 지원되는 경우 각 파일 객체에는 handle 속성에 FileSystemFileHandle도 저장되어 있으므로 선택적으로 핸들을 디스크에 직렬화할 수 있습니다.

const openFileOrFiles = async (multiple = false) => {
  // Feature detection. The API needs to be supported
  // and the app not run in an iframe.
  const supportsFileSystemAccess =
    "showOpenFilePicker" in window &&
    (() => {
      try {
        return window.self === window.top;
      } catch {
        return false;
      }
    })();
  // If the File System Access API is supported…
  if (supportsFileSystemAccess) {
    let fileOrFiles = undefined;
    try {
      // Show the file picker, optionally allowing multiple files.
      const handles = await showOpenFilePicker({ multiple });
      // Only one file is requested.
      if (!multiple) {
        // Add the `FileSystemFileHandle` as `.handle`.
        fileOrFiles = await handles[0].getFile();
        fileOrFiles.handle = handles[0];
      } else {
        fileOrFiles = await Promise.all(
          handles.map(async (handle) => {
            const file = await handle.getFile();
            // Add the `FileSystemFileHandle` as `.handle`.
            file.handle = handle;
            return file;
          })
        );
      }
    } catch (err) {
      // Fail silently if the user has simply canceled the dialog.
      if (err.name !== 'AbortError') {
        console.error(err.name, err.message);
      }
    }
    return fileOrFiles;
  }
  // Fallback if the File System Access API is not supported.
  return new Promise((resolve) => {
    // Append a new `<input type="file" multiple? />` and hide it.
    const input = document.createElement('input');
    input.style.display = 'none';
    input.type = 'file';
    document.body.append(input);
    if (multiple) {
      input.multiple = true;
    }
    // The `change` event fires when the user interacts with the dialog.
    input.addEventListener('change', () => {
      // Remove the `<input type="file" multiple? />` again from the DOM.
      input.remove();
      // If no files were selected, return.
      if (!input.files) {
        return;
      }
      // Return all files or just one file.
      resolve(multiple ? Array.from(input.files) : input.files[0]);
    });
    // Show the picker.
    if ('showPicker' in HTMLInputElement.prototype) {
      input.showPicker();
    } else {
      input.click();
    }
  });
};

추가 자료

데모

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="icon"
      href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📂</text></svg>"
    />
    <title>How to open one or multiple files</title>
  </head>
  <body>
    <h1>How to open one or multiple files</h1>
    <button type="button">Open file</button>
    <button class="multiple" type="button">Open files</button>
    <pre></pre>
  </body>
</html>

CSS


        :root {
  color-scheme: dark light;
}

html {
  box-sizing: border-box;
}

*,
*:before,
*:after {
  box-sizing: inherit;
}

body {
  margin: 1rem;
  font-family: system-ui, sans-serif;
}

button {
  margin: 1rem;
}
        

JS


        const button = document.querySelector('button');
const buttonMultiple = document.querySelector('button.multiple');
const pre = document.querySelector('pre');

const openFileOrFiles = async (multiple = false) => {
  // Feature detection. The API needs to be supported
  // and the app not run in an iframe.
  const supportsFileSystemAccess =
    "showOpenFilePicker" in window &&
    (() => {
      try {
        return window.self === window.top;
      } catch {
        return false;
      }
    })();

  // If the File System Access API is supported…
  if (supportsFileSystemAccess) {
    let fileOrFiles = undefined;
    try {
      // Show the file picker, optionally allowing multiple files.
      fileOrFiles = await showOpenFilePicker({ multiple });
      if (!multiple) {
        // Only one file is requested.
        fileOrFiles = fileOrFiles[0];
      }
    } catch (err) {
      // Fail silently if the user has simply canceled the dialog.
      if (err.name !== 'AbortError') {
        console.error(err.name, err.message);
      }
    }
    return fileOrFiles;
  }
  // Fallback if the File System Access API is not supported.
  return new Promise((resolve) => {
    // Append a new `` and hide it.
    const input = document.createElement('input');
    input.style.display = 'none';
    input.type = 'file';
    document.body.append(input);
    if (multiple) {
      input.multiple = true;
    }
    // The `change` event fires when the user interacts with the dialog.
    input.addEventListener('change', () => {
      // Remove the `` again from the DOM.
      input.remove();
      // If no files were selected, return.
      if (!input.files) {
        return;
      }
      // Return all files or just one file.
      resolve(multiple ? input.files : input.files[0]);
    });
    // Show the picker.
    if ('showPicker' in HTMLInputElement.prototype) {
      input.showPicker();
    } else {
      input.click();
    }
  });
};

button.addEventListener('click', async () => {
  const file = await openFileOrFiles();
  if (!file) {
    return;
  }
  pre.textContent += `${file.name}\n`;
});

buttonMultiple.addEventListener('click', async () => {
  const files = await openFileOrFiles(true);
  if (!files) {
    return;
  }
  Array.from(files).forEach((file) => (pre.textContent += `${file.name}\n`));
});