input type=file onChange 미작동 문제 해결
2025-08-10모바일/웹 환경에서 파일 업로드 UI를 커스터마이징할 때 흔히 쓰는 패턴이 있어요. <input type="file">은 숨겨두고 클릭 가능한 label이나 div로 파일 선택을 제어하면서, 선택된 이미지를 미리보기로 보여주다가 삭제 버튼으로 지우는 구조요. UI적으로는 깔끔한데, 이 방식에는 특정 상황에서 onChange 이벤트가 동작하지 않는 문제가 숨어 있어요.
문제 상황
아래 같은 코드에서 이미지를 업로드한 뒤 UI상에서 삭제하고 동일한 파일을 다시 업로드하면 onChange 이벤트가 아예 발생하지 않아요.
<input
type="file"
id="fileInput"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<label htmlFor="fileInput">이미지 선택</label>
{preview && (
<div>
<img src={preview} alt="미리보기" />
<button onClick={handleRemove}>삭제</button>
</div>
)}
왜 같은 파일을 다시 선택하면 이벤트가 발생하지 않을까?
브라우저는 <input type="file">의 현재 값을 value 속성에 파일 경로 형태로 저장해요. 사용자가 파일을 선택하면 이 값이 설정되고, 다음에 파일을 선택할 때 이전 값과 비교하거든요. 동일한 경로라면 값이 변하지 않았다고 판단해서 change 이벤트 자체를 발생시키지 않아요. 불필요한 이벤트 중복 호출을 막기 위한 설계인 거죠.
여기서 중요한 점은 미리보기를 삭제하는 것과 input의 value를 초기화하는 것은 완전히 별개라는 사실이에요. React 상태에서 preview를 null로 설정해도 input 엘리먼트 자체의 파일 참조는 그대로 남아 있거든요. 브라우저 입장에서는 이전에 선택한 파일과 동일한 파일을 다시 선택하는 상황이 되는 거예요.
이 현상은 플랫폼마다 구체적인 기준이 조금씩 달라요.
| 플랫폼/브라우저 | 동일 파일 재선택 시 동작 | 비고 |
|---|---|---|
| 안드로이드 Chrome/WebView | 거의 항상 onChange 미발생 | 문제 가장 자주 발생 |
| iOS Safari | 메타데이터 변동 시 발생 가능 | 카메라 촬영/갤러리 선택 시 파일이 달라질 수 있음 |
| 데스크톱 Chrome/Edge | 미발생 | 표준 동작 |
| 데스크톱 Firefox | 미발생 | 표준 동작 |
해결 방법
UI에서 이미지를 삭제할 때 input.value를 함께 초기화하면 돼요. value를 빈 문자열로 설정하면 브라우저가 파일 참조를 내부적으로 지우고 files 속성도 비워져요. 이후 같은 파일을 선택하더라도 브라우저는 이전 값이 없는 상태에서 새로운 선택이 이루어진 것으로 인식해서 change 이벤트를 정상적으로 발생시키죠.
import { useRef, useState } from "react";
function ImageUploader() {
const fileInputRef = useRef(null);
const [preview, setPreview] = useState(null);
function handleFileChange(e) {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
setPreview(url);
}
}
function handleRemove() {
setPreview(null);
if (fileInputRef.current) {
fileInputRef.current.value = ""; // 핵심: 파일 값 초기화
}
}
return (
<>
<input
type="file"
ref={fileInputRef}
style={{ display: "none" }}
onChange={handleFileChange}
/>
<label htmlFor="fileInput">이미지 선택</label>
{preview && (
<div>
<img src={preview} alt="미리보기" />
<button onClick={handleRemove}>삭제</button>
</div>
)}
</>
);
}
위 코드처럼 새로운 파일을 업로드하기 전, 기존 input을 초기화하면 문제가 해결돼요.
대안: key prop으로 input 자체를 교체하기
ref.current.value = ""가 직관적이지 않다고 느낀다면 다른 방법도 있어요. React의 key prop을 변경하면 해당 컴포넌트를 아예 언마운트 후 새로 마운트해요.
function ImageUploader() {
const [preview, setPreview] = useState(null);
const [inputKey, setInputKey] = useState(0);
function handleFileChange(e) {
const file = e.target.files?.[0];
if (file) {
setPreview(URL.createObjectURL(file));
}
}
function handleRemove() {
setPreview(null);
setInputKey(prev => prev + 1); // key 변경으로 input 재생성
}
return (
<>
<input
key={inputKey}
type="file"
style={{ display: "none" }}
onChange={handleFileChange}
/>
{/* ... */}
</>
);
}
key가 바뀌면 React는 이전 <input>을 DOM에서 제거하고 새 <input>을 삽입해요. 완전히 새로운 엘리먼트라서 value를 직접 건드리지 않아도 내부 상태가 초기화되는 거예요.
두 방법을 정리하면 이래요.
| 방법 | 동작 방식 | 장점 | 단점 |
|---|---|---|---|
ref.current.value = "" | input의 value를 직접 초기화 | DOM 조작 최소화 | 명령형 코드, ref 필요 |
key 변경 | input 컴포넌트를 언마운트 후 재마운트 | React스러운 방식 | 매번 DOM 노드 재생성 |
커스텀 훅으로 추상화하기
파일 업로드 로직이 여러 곳에서 반복된다면 훅으로 분리하면 재사용하기 좋아요.
import { useRef, useState } from 'react';
function useFileUpload() {
const fileInputRef = useRef<HTMLInputElement>(null);
const [preview, setPreview] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const selected = e.target.files?.[0];
if (selected) {
setFile(selected);
setPreview(URL.createObjectURL(selected));
}
}
function handleRemove() {
setPreview(null);
setFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
function openFilePicker() {
fileInputRef.current?.click();
}
return { fileInputRef, preview, file, handleFileChange, handleRemove, openFilePicker };
}
function ImageUploader() {
const { fileInputRef, preview, handleFileChange, handleRemove, openFilePicker } = useFileUpload();
return (
<>
<input ref={fileInputRef} type="file" style={{ display: 'none' }} onChange={handleFileChange} />
<button onClick={openFilePicker}>이미지 선택</button>
{preview && (
<div>
<img src={preview} alt="미리보기" />
<button onClick={handleRemove}>삭제</button>
</div>
)}
</>
);
}
마치며
input[type=file]의 onChange가 특정 상황에서만 동작하지 않는 이유는 브라우저의 의도된 설계 때문이에요. "값이 변하지 않으면 이벤트를 발생시키지 않는다"는 원칙은 일반 input에도 동일하게 적용되는데, 파일 업로드에서는 "UI상 삭제"와 "실제 input 초기화"가 별개라는 점이 이를 더 헷갈리게 만들어요.
결국 핵심은 간단해요. 사용자가 "삭제"를 눌렀다면, React 상태와 input의 실제 값 모두를 함께 초기화해야 한다는 거예요.