---
title: "드롭한 폴더는 0바이트 파일로 온다 — webkitGetAsEntry 로 가려내기"
description: "드래그 앤 드롭 업로드 시 폴더를 파일로 인식해 발생하는 오류를 webkitGetAsEntry API로 해결하는 방법과 사용자 경험을 개선하는 UX 전략을 공유합니다"
date: 2026-08-14
updated: 2026-08-14T06:42:30.236Z
tags: [frontend, drag-and-drop, browser-api, ux]
canonical: https://blog.wooncloud.com/posts/folder-drop-zero-byte-file
---

![드롭한 항목에서 파일과 폴더를 가려낸다](/images/posts/folder-drop-zero-byte-file/e53a2b19-8bea-42ee-9ec9-7c91be72b6d6.webp)

파일 업로드에 드래그 앤 드롭을 붙였다. 사용자가 탐색기에서 **폴더를** 끌어놓으면 "업로드 실패, 다시 시도해주세요"가 떴다. 폴더 업로드는 아직 만들지 않은 기능인데, 문구는 고장난 것처럼 말하고 있었다.

문의는 이렇게 들어왔다.

> 당연히 될 거라고 생각했는데 "실패"라고 뜨니까, 되어야 할 게 안 되는 느낌이에요.

## 폴더도 File 객체로 온다

`drop` 이벤트에서 흔히 이렇게 받는다.

```ts
element.addEventListener("drop", (e) => {
  if (e.dataTransfer.files.length > 0) upload(e.dataTransfer.files);
});
```

문제는 **브라우저가 드롭한 폴더도 `dataTransfer.files` 에 넣어 준다**는 것이다. `size: 0`, `type: ""` 인 `File` 객체로.

```js
// 폴더 하나를 끌어놓았을 때
e.dataTransfer.files[0];
// File { name: "사진첩", size: 0, type: "" }
```

파일과 구분할 필드가 없다. 그래서 폴더도 그대로 전송되고, 서버가 거절하고, 화면에는 실패 문구만 남는다.

크기로 거르는 휴리스틱을 먼저 떠올리게 되는데 쓸 수 없다. `size === 0` 은 진짜 빈 파일도 함께 막고, `type === ""` 은 확장자 없는 파일에서도 그렇다. 추측이 아니라 확정 판정이 필요하다.

## webkitGetAsEntry 로 가려낸다

[`DataTransferItem.webkitGetAsEntry()`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry) 가 그 판정을 준다. `webkit` 접두어가 붙어 있지만 File and Directory Entries API 로 표준화됐고 주요 브라우저가 모두 지원한다.

```ts
interface DroppedItems {
  files: File[];
  folderCount: number;
}

function splitDroppedItems(dt: DataTransfer): DroppedItems {
  const items = dt.items ? Array.from(dt.items) : [];

  // 미지원 환경 — 폴더를 가려낼 수단이 없으므로 기존 동작으로 폴백
  if (!items.some((i) => typeof i.webkitGetAsEntry === "function")) {
    return { files: Array.from(dt.files), folderCount: 0 };
  }

  const files: File[] = [];
  let folderCount = 0;

  for (const item of items) {
    if (item.kind !== "file") continue;
    if (item.webkitGetAsEntry()?.isDirectory) {
      folderCount++;
      continue;
    }
    const file = item.getAsFile();
    if (file) files.push(file);
  }

  return { files, folderCount };
}
```

여기서 지켜야 할 게 세 가지 있다.

### files 가 아니라 items 를 순회한다

폴더 여부는 `DataTransferItem` 의 entry 에만 있다. `files` 를 아무리 들여다봐도 답이 없다.

### drop 핸들러 안에서 동기로 호출한다

가장 놓치기 쉬운 지점이다. `DataTransferItemList` 는 이벤트 처리가 끝나면 무효화된다. `await` 를 하나만 끼워도 `webkitGetAsEntry()` 가 `null` 을 돌려주기 시작한다.

```ts
// 이렇게 하면 안 된다
element.addEventListener("drop", async (e) => {
  const ok = await checkQuota();          // 여기서 items 가 죽는다
  const dropped = splitDroppedItems(e.dataTransfer);  // 폴더가 파일로 분류됨
});
```

무서운 건 예외가 안 난다는 것이다. `null` 은 "디렉터리가 아님"으로 읽혀서 **폴더가 조용히 일반 파일로 분류되고**, 원래 증상으로 그대로 돌아간다. 판정이 필요한 것은 먼저 동기로 뽑아 두고, 비동기 작업은 그 뒤에 한다.

### 판정 불가를 차단으로 바꾸지 않는다

`webkitGetAsEntry` 가 없는 환경에서는 폴더를 가려낼 수단이 없다. 그럴 때는 기존 동작(`files` 그대로 사용)으로 폴백한다. 못 가려낸다고 업로드를 막으면 멀쩡한 파일까지 못 올린다. 판정 실패는 "모른다"이지 "위험하다"가 아니다.

## 문구를 두 갈래로 나눈 이유

기술 판정이 끝나고 나면 UX 결정이 하나 남는다. 폴더가 섞였을 때 어떻게 할 것인가.

| 드롭한 것 | 동작 | 강도 |
|---|---|---|
| 폴더만 | 전송하지 않고 압축 안내 | error |
| 폴더 + 파일 | 파일은 그대로 올리고 폴더만 안내 | warning |
| 파일만 | 기존 경로 그대로 | — |

**섞였다고 통째로 막지 않는다.** 여러 개를 한 번에 끌어놓는 건 흔한 동작이라, 폴더 하나 섞였다고 전부 거부하면 함께 놓은 파일까지 사라져 더 혼란스럽다.

그리고 문구에서 **"다시 시도해주세요"를 뺐다.**

```
❌ 업로드 실패. 다시 시도해주세요
✅ 폴더는 업로드할 수 없습니다. 폴더를 압축(zip)한 뒤 올려주세요
```

미지원 기능을 실패로 안내하면 사용자는 재시도를 반복한다. 같은 제약이라도 "지원하지 않으니 이렇게 하세요"라고 말하면 정책으로 읽힌다. 기능을 만들지 않고도 문의가 사라지는 지점이 여기다.

## 폴더 업로드를 진짜 구현하려면

같은 API 로 트리를 순회할 수 있다. `FileSystemDirectoryEntry.createReader().readEntries()` 를 재귀 호출하면 된다.

```ts
async function readAll(reader: FileSystemDirectoryReader) {
  const out: FileSystemEntry[] = [];
  // readEntries 는 한 번에 최대 100개만 준다 — 빈 배열이 올 때까지 반복
  while (true) {
    const batch = await new Promise<FileSystemEntry[]>((res, rej) =>
      reader.readEntries(res, rej),
    );
    if (batch.length === 0) break;
    out.push(...batch);
  }
  return out;
}
```

[`readEntries()`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryReader/readEntries) 가 한 번에 최대 100개만 돌려준다는 게 함정이다. 한 번만 부르고 끝내면 101번째부터 조용히 사라진다.

버튼 경로는 훨씬 쉽다. [`webkitdirectory`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory) 속성 하나면 되고, `File.webkitRelativePath` 에 상대 경로가 담겨 온다.

```html
<input type="file" webkitdirectory multiple />
```

정작 비싼 쪽은 프론트가 아니다.

- **API 계약** — 경로를 받아 폴더를 만들어 가며 올리는 엔드포인트가 필요하다. 보통은 부모 ID 하나만 받게 되어 있다
- **충돌 정책** — 같은 이름의 폴더가 이미 있으면 병합인가 새로 만들기인가. 파일 단위 충돌과는 다른 질문이다
- **부분 실패** — 300개 중 50개가 실패하면 폴더는 이미 만들어져 있다. 재시도가 폴더를 다시 만들지 않도록 경로→ID 매핑을 들고 있어야 한다
- **요청 수** — 폴더 업로드는 수백~수천 개가 기본값이라 rate limit 을 정면으로 맞는다

그래서 "폴더를 가려내 안내하기"와 "폴더 업로드 구현하기"는 규모가 다른 일이다. 전자는 하루면 되고 후자는 몇 주가 걸린다. 문의가 온 시점에 필요한 건 대개 전자다.
