다중 파일의 드랍에 대한 안전한 처리

웹 API가 계속 발전하고 그에 맞춰 Javascript도 개선되는 과정에서 괴리가 발생하는데요. 그중 한가지 사례로 다중 파일에 대한 드랍처리입니다.

아래의 드랍 처리는 2개 이상의 파일을 드랍했을때 처리되어야할 파일이 누락됩니다.

dropArea.addEventListener('drop', async (e) => {
  e.preventDefault();
  dropArea.classList.remove('highlight');
  fileListElement.innerHTML = '';

  const items = e.dataTransfer.items;
  if (items) {
    for (let i = 0; i < items.length; i++) {
      try {
        const handle = await items[i].getAsFileSystemHandle();

        if (handle.kind === 'file') {
          const file = await handle.getFile();
          displayFile(file);
        } else if (handle.kind === 'directory') {
          await traverseFileSystemHandle(handle);
        }
      } catch (error) {
        console.error(error);
      }
    }
  }
});

위 코드의 문제점에 대한 원인은 비동기 처리를 위해 js에서는 for await .. of 문이 도입되었으나 아직 drop 이벤트 객체의 dataTransfer.items 컬렉션은 이를 지원하지 않기 때문입니다. 차선책으로 이를 개선한 코드가 다음과 같습니다.

dropArea.addEventListener('drop', async (e) => {
  e.preventDefault();
  dropArea.classList.remove('highlight');

  fileListElement.innerHTML = '';

  const items = e.dataTransfer.items;
  if (items) {
    const processingPromises = [];

    for (let i = 0; i < items.length; i++) {
      processingPromises.push((async () => {
        if (items[i].getAsFileSystemHandle) {
          try {
            const handle = await items[i].getAsFileSystemHandle();
            if (handle.kind === 'file') {
              const file = await handle.getFile();
              displayFile(file);
            } else if (handle.kind === 'directory') {
              await traverseFileSystemHandle(handle);
            }
          } catch (error) {
            console.warn(error);
          }
        }
      })()); // 즉시 실행
    }
    await Promise.allSettled(processingPromises);
  }
});

Compression Streams API를 이용한 텍스트 데이터에 대한 압축 및 해제

Compression Streams API는 웹 표준 API로 데이터에 대한 압축 및 해제를 위한 API이다. 파일 단위가 아닌 데이터에 대한 압축으로 그 목적이 zip과는 다르지만 데이터 압축에 대한 알고리즘은 유사하다. 즉, zip은 여러개의 파일을 압축하면서 압축 해제시에는 파일 명과 디렉토리 구조까지 복원해주지만 Compression Sterams API는 데이터에 대한 압축으로 압축을 해제하면 해제된 데이터가 나오며 그 데이터를 하나의 파일로 저장한다.

Compression Streams API로 압축할 수 있는 데이터는 텍스트 든 바이너리든 모두 가능한데, 아래의 코드는 텍스트에 대한 압축과 압축된 데이터를 다시 원래의 텍스트 데이터로 복원(압축 해제)하는 코드이다.

async function compressAndDecompressString(text) {
  // 1. 텍스트를 Uint8Array로 변환
  const textEncoder = new TextEncoder();
  const encodedData = textEncoder.encode(text);

  console.log("원본 데이터:", text);
  console.log("원본 데이터 크기 (바이트):", encodedData.byteLength);

  // 2. 데이터를 압축 (gzip 형식)
  // ReadableStream.from()은 Node.js에서 사용 가능하며, 
  // 브라우저에서는 ArrayBuffer를 직접 ReadableStream으로 변환해야 함.
  // 여기서는 편의를 위해 Blob을 사용하여 ReadableStream을 생성합니다.
  const originalStream = new Blob([encodedData]).stream();
  const compressedStream = originalStream.pipeThrough(new CompressionStream("gzip"));

  // 3. 압축된 데이터를 ArrayBuffer로 읽기
  const compressedResponse = new Response(compressedStream);
  const compressedBuffer = await compressedResponse.arrayBuffer();

  console.log("압축된 데이터 크기 (바이트):", compressedBuffer.byteLength);
  console.log("압축률:", 
    ((1 - compressedBuffer.byteLength / encodedData.byteLength) * 100).toFixed(2) + "%");

  // 4. 압축된 데이터를 압축 해제
  const decompressedStream = new Blob([compressedBuffer]).stream()
    .pipeThrough(new DecompressionStream("gzip"));

  // 5. 압축 해제된 데이터를 ArrayBuffer로 읽기
  const decompressedResponse = new Response(decompressedStream);
  const decompressedBuffer = await decompressedResponse.arrayBuffer();

  // 6. 압축 해제된 Uint8Array를 다시 텍스트로 변환
  const textDecoder = new TextDecoder();
  const decompressedText = textDecoder.decode(decompressedBuffer);

  console.log("압축 해제된 데이터:", decompressedText);
  console.log("압축 해제된 데이터 크기 (바이트):", decompressedBuffer.byteLength);

  // 원본 데이터와 압축 해제된 데이터가 동일한지 확인
  console.log("원본과 압축 해제된 데이터 일치 여부:", text === decompressedText);
}

// 예제 실행
const longText = `The quick brown fox jumps over the lazy dog.`;

compressAndDecompressString(longText);