웹앱에서 같은 파일을 다시 선택해도 반응이 없을 때: change·초기화 점검

웹앱에서 파일을 한 번 고른 뒤 같은 파일을 다시 골랐는데 화면이 조용하다면, 서버 용량 제한부터 바꾸기 전에 선택 이벤트가 발생했는지, 파일 객체를 확보했는지, 읽기 결과가 현재 선택과 맞는지 확인하세요. 이 글은 파일 선택창과 로컬 읽기 단계에 집중합니다. 이미 전송한 요청의 413·multipart 문제는 파일 업로드 오류 가이드로 이어집니다.

예제는 사용자가 고른 UTF-8 .txt 메모 한 개를 최대 64 KiB까지만 브라우저에서 읽어 표시하는 방식입니다. 파일을 서버로 보내지 않으며, 아래 코드는 이 사이트에서 실행되는 기능이 아니라 복사해 검토할 설명용 코드입니다.

반응이 멈춘 단계를 먼저 나누세요

관찰한 증상 먼저 볼 증거 다음 조치
첫 선택은 되고 같은 파일 재선택만 조용함 input·change·cancel의 실제 발생 순서 변경 이벤트만으로 재실행을 기대했는지 확인
파일명이 보여도 파일을 읽지 못함 input.files.length와 확보한 File value의 경로 문자열을 읽기 주소로 사용하지 않기
초기화 뒤 files[0]이 없음 File 확보와 value 초기화의 순서 즉시 읽기 방식이면 참조를 먼저 확보
취소했더니 이전 첨부가 사라짐 선택창을 열기 전 click에서 지웠는지 이전 선택 유지 정책을 먼저 정하기
B를 골랐는데 늦게 A 내용으로 바뀜 선택 번호와 읽기 완료 번호 현재 선택의 결과만 반영
POST가 없는데 업로드 실패라고 표시됨 선택·검사·로컬 읽기·전송 중 어느 단계인가 서버 오류와 전송 전 실패를 분리

개발 중에는 이벤트 이름·선택 파일 수·안전한 오류 분류만으로 순서를 확인할 수 있습니다. 실제 사용자 파일 내용이나 전체 로컬 경로를 진단 로그에 남길 필요는 없습니다.

같은 파일 선택을 언제나 change로 해석하지 마세요

MDN cancel 문서는 선택창 취소와 기존 파일 재선택을 설명합니다. 동시에 HTML 표준의 선택창 동작은 재선택을 취소로 볼지 변경으로 볼지 사용자 에이전트가 판단할 수 있다고 명시합니다. 따라서 모든 브라우저에서 같은 파일이면 change가 반드시 한 번 더 온다거나, 반드시 cancel만 온다고 단정하지 않습니다.

change는 선택 목록이 바뀌었을 때 처리할 작업에 연결하고, cancel은 새 선택 없이 닫은 상황의 안내로 다룹니다. cancel 지원 여부에 앱의 핵심 정리를 의존하지 마세요. 브라우저가 취소를 빈 선택으로 바꿔 change를 발생시키는 경우도 고려해 files가 비어 있는 분기가 필요합니다.

input type=file 문서의 C:\fakepath\는 실제 파일 주소가 아닙니다. 선택 파일은 FileList 안의 File 객체로 가져오세요. 파일명·크기·수정 시각이 같다는 사실만으로 두 파일의 내용까지 같다고 판단해서도 안 됩니다.

초기화 시점은 첨부 방식에 따라 정하세요

화면의 계약 선택값 처리 주의할 점
선택 즉시 읽거나 가져오기 File 참조를 확보한 뒤 input.value를 빈 문자열로 다음 선택의 기준을 비우며 별도의 결과 영역이 상태를 소유
나중에 일반 form 제출로 첨부 제출에 사용할 선택값 유지 미리 비우면 파일 필드와 required 검증에 영향
File을 앱 상태에 보관해 나중에 직접 FormData 작성 별도 File 참조·명시적 제거 동작 설계 입력창과 앱 상태의 소유권·화면 종료 시 정리 필요

표준은 파일 input의 value에 빈 문자열을 넣어 목록을 비우는 동작을 정의합니다. 임의의 로컬 경로를 value에 써서 파일을 선택시키는 동작은 허용하지 않습니다. 모든 click마다 선택값을 먼저 지우는 요령을 붙이면 취소 시에도 기존 첨부가 사라질 수 있으므로 제품이 원하는 유지·교체 규칙부터 정하세요.

File을 먼저 확보하고 늦은 읽기를 무시하는 예제

아래 두 파일을 같은 폴더에 두고 JavaScript 파일명을 file-preview.js로 맞춘 예제입니다. 실제 파일을 시험할 때는 공개해도 되는 작은 테스트 메모를 사용하세요. input의 네이티브 파일명 표시는 초기화되지만, 처리한 파일명과 결과는 별도 영역에서 안내하도록 의도했습니다. 일반 form 제출용 첨부 UI에 그대로 붙이는 코드는 아닙니다.

<label for="memo-file">UTF-8 메모 파일 한 개 선택 (.txt, 최대 64 KiB)</label>
<input id="memo-file" type="file" accept=".txt,text/plain">
<button id="clear-memo" type="button">결과 지우기</button>
<p id="file-status" role="status" aria-live="polite">선택한 파일이 없습니다.</p>
<pre id="memo-preview" aria-label="메모 미리보기"></pre>
<script src="file-preview.js" defer></script>
const input = document.querySelector('#memo-file');
const clearButton = document.querySelector('#clear-memo');
const status = document.querySelector('#file-status');
const preview = document.querySelector('#memo-preview');
const maxBytes = 64 * 1024;
let revision = 0;

input.addEventListener('change', async () => {
  const current = ++revision;
  const file = input.files?.[0] ?? null; // 초기화 전에 File을 확보합니다.
  input.value = ''; // 즉시 읽기 방식입니다. 폼 첨부 방식에 그대로 쓰지 마세요.
  preview.textContent = '';

  if (!file) {
    status.textContent = '선택한 파일이 없습니다.';
    return;
  }
  if (!/\.txt$/i.test(file.name)) {
    status.textContent = '.txt 테스트 메모를 선택하세요.';
    return;
  }
  if (file.size > maxBytes) {
    status.textContent = '64 KiB 이하의 메모를 선택하세요.';
    return;
  }

  status.textContent = `${file.name}: 로컬에서 읽는 중입니다.`;
  try {
    const text = await file.text();
    if (current !== revision) return;
    preview.textContent = text; // HTML로 해석하지 않습니다.
    status.textContent = `${file.name}: 로컬 읽기 완료 (${file.size} bytes). 서버 전송 없음.`;
  } catch {
    if (current !== revision) return;
    status.textContent = '파일을 읽지 못했습니다. 다시 선택할 수 있습니다.';
  }
});

input.addEventListener('cancel', () => {
  status.textContent = '새 파일 선택 없이 닫았습니다. 진행 중이거나 표시된 결과를 유지합니다.';
});

clearButton.addEventListener('click', () => {
  revision += 1; // 이미 시작된 읽기의 늦은 결과도 무시합니다.
  input.value = '';
  preview.textContent = '';
  status.textContent = '선택한 파일이 없습니다.';
});

File 참조를 읽은 뒤 input.value를 비워도 이미 확보한 참조로 읽기를 시작할 수 있습니다. 반대로 비운 뒤 files[0]을 조회하면 선택 목록이 비어 있으므로 순서가 중요합니다. 빈 값 대입은 사용자에게 새 파일을 선택시킨 동작이 아니며, 실제 선택창의 이벤트를 자동으로 시험했다는 뜻도 아닙니다.

revision은 선택 또는 명시적인 지우기마다 증가합니다. A를 읽는 중 B를 골랐거나 결과를 지웠다면, 뒤늦게 끝난 A는 화면을 바꾸지 않습니다. 이 비교는 결과 반영을 막는 장치이고 이미 시작한 파일 읽기를 물리적으로 취소하는 API는 아닙니다. cancel은 기존 결과와 읽기 작업을 유지하는 별도 정책을 사용합니다.

확장자·MIME·크기는 서로 다른 검사입니다

예제의 accept와 .txt 확장자 검사는 사용자가 테스트 메모를 고르기 위한 안내입니다. 파일 내용의 안전성·진짜 유형을 증명하지 않습니다. Blob.type 문서도 파일의 MIME 값이 확장자와 환경에 영향을 받고 빈 문자열일 수 있음을 설명합니다. type이 비었다고 고장으로 단정하거나 MIME 하나만 보안 검사로 사용하지 마세요.

64 KiB는 이 로컬 예제의 읽기 상한이고 정확히 65,536 bytes까지 허용합니다. 업로드 서버의 최대 크기나 일반 서비스 권장값이 아닙니다. Blob.text()는 UTF-8로 해석하므로 다른 문자 인코딩 파일의 변환·정확한 인코딩 판별까지 하지 않습니다. 내용을 textContent로 넣어 HTML로 실행하지 않지만, 이 사실이 파일 형식 검증을 대신하지도 않습니다.

모의 실행에서 실제로 확인한 범위

2026년 10월 27일 Node.js v24.19.0에서 공개한 JavaScript를 그대로 불러 모의 DOM·File·이벤트 핸들러로 14개 시나리오를 실행했고 모두 통과했습니다. 네이티브 파일 선택창·브라우저의 실제 이벤트 발생·실제 디스크 읽기·모바일 기기 테스트는 수행하지 않았습니다.

검사 묶음 관찰 결과
빈 선택, 참조 확보 뒤 초기화, 같은 참조로 핸들러 재실행 빈 선택은 읽지 않고, 확보한 File은 읽으며 재호출 가능
.txt 외 확장자, 대문자 .TXT·빈 MIME, 65,536·65,537 bytes 예제의 확장자·크기 경계에 맞춰 읽기 전 분기
0 bytes 파일, 읽기 거부 빈 성공과 읽기 실패를 구분하고 거짓 성공 없음
느린 A 뒤 B, 읽는 중 지우기, 뒤의 잘못된 선택 이전 읽기 완료가 최신 결과·안내를 덮지 않음
완료 뒤 cancel, 진행 중 cancel 기존 내용과 진행 중 읽기를 유지하는 정책 확인
정적 HTML과 API 사용 비제출 지우기 버튼·상태 영역 확인, 네트워크·HTML 삽입 없음

같은 File 참조로 핸들러를 두 번 실행한 검사는 처리 함수의 재실행을 확인한 것입니다. 실제 브라우저가 같은 파일 재선택에서 change를 발생시켰다는 관찰로 바꾸어 쓰지 않습니다. 결과 순서를 시험한 테스트도 File 읽기 성능을 측정한 자료가 아닙니다.

제품 브라우저에서 마칠 확인 순서

  1. 작은 UTF-8 메모 A를 선택하고 로컬 읽기 완료·표시 내용을 확인합니다. Network에 파일 전송 요청이 생기지 않는지도 봅니다.
  2. 동일한 A를 다시 고르고 실제 발생한 input·change·cancel 순서와 결과 영역을 기록합니다. 브라우저 이름·버전과 OS를 함께 남깁니다.
  3. 다른 메모 B, 선택창 취소, 빈 선택으로 바뀌는 경로를 각각 확인합니다. 사용자가 취소했을 때 유지할 결과가 무엇인지 점검합니다.
  4. 느린 읽기 중 다음 선택과 지우기를 실행해 이전 결과가 뒤늦게 나타나지 않는지 확인합니다.
  5. 빈 파일·허용 상한·상한 초과·잘못된 확장자와 작은 화면·키보드 조작을 확인합니다. 앱의 접근성 안내와 상태 문구를 결과와 대조합니다.
  6. 일반 form 또는 직접 FormData로 전송하는 제품이라면 실제 제출 시 File이 남아 있는지 따로 확인합니다. 로컬 미리보기 완료를 서버 저장 완료로 표시하지 않습니다.

아직 POST가 없는 선택 단계와 이미 요청을 보낸 업로드 단계를 나누면, 서버 제한을 바꿀 상황과 입력 상태를 고칠 상황을 빠르게 구분할 수 있습니다.

공식 원문과 관련 가이드

2026년 10월 27일 아래 원문을 직접 열어 선택·초기화·파일 읽기 조건을 대조했습니다. 코드와 판단표는 이를 바탕으로 작성한 설명 자료이며, 위 14건의 실행 결과는 모의 환경으로 제한합니다.