타입이 file인 input 요소에 value를 전달할 수 없어 발생하는 문제 상황을 이야기합니다.
file input에 기본 값을 지정하기 위해 이미지 인풋 컴포넌트에 file prop으로 이미지 file을 전달합니다.
이후 usePreviewImage 커스텀 훅을 활용해 미리보기 이미지를 보여줍니다.
function ImageInput({ onInput, file }: ImageInputProps) {
// 이미지 파일
const [imageFile, setImageFile] = useState<File | undefined>(file);
// 미리보기 이미지
const preview = usePreviewImage(imageFile);
const inputRef = useRef<HTMLInputElement | null>(null);
return (
<>
{preview ? (
<Preview image={preview} onModify={openInput} onDelete={removeImage} />
) : (
<Guide className={className.content} onSubmit={openInput} />
)}
<input
id={id}
ref={inputRef}
className="hidden"
type="file"
accept="image/*"
onInput={handleInput}
{...rest}
/>
</>
)
}
이렇게하면 마치 기본값을 가지는 것처럼 눈속임 할 수 있습니다. 하지만 해당 컴포넌트에는 문제가 있습니다.
2개의 페이지로 나누어진 form을 상태를 활용 조건부로 보여주는 페이지에서 발생한 문제입니다.
2번 form → 1번 form으로 이동해서 이미지를 삭제하는 경우 react hook form의 데이터가 변경되지 않는 문제가 발생했습니다.


이미지를 삭제해도 남아있는 데이터
문제의 원인을 분석해봤을 때 이유는 다음과 같습니다..
컴포넌트 내부에서 input 요소에 onInput 이벤트가 발생하면 외부의 콜백이 실행되도록 구현되어 있습니다.
이전 문제 상황에서는 react-hook-form의 데이터를 가져와 컴포넌트에 prop으로 넘겨 마치 파일이 있는 것처럼 이전에 업로드된 이미지를 보여줍니다. 이 경우 input 요소에 값은 없기 때문에 이벤트가 발생하지 않아 해당 문제가 발생한 것으로 보입니다.
브라우저의 보안 이슈로 인해 input 요소에 직접 값을 전달할 수는 없기 때문에 다른 방법을 모색합니다.
// 이런식으로 처리가 불가능
<input type='file' accept='image/*' value={어떤 이미지 파일} />
onInput 이벤트로 콜백함수가 실행되던 방식을 이벤트를 직접 컨트롤 하는 방식으로 변경합니다.
컴포넌트 내부에 이미지 파일 상태를 정의해 useEffect가 imageFile 상태에 의존하도록 해 상태가 변경될 때 이벤트가 실행되도록 했습니다.
function ImageInput({ onInput, file } : ImageInputProps) {
const [imageFile, setImageFile] = useState<File | undefined>(file);
const [event, setEvent] = useState<ChangeEvent<HTMLInputElement>>();
const handleInput = (e: ChangeEvent<HTMLInputElement>) => {
const targetFile = e.target.files ? e.target.files[0] : undefined;
setImageFile(targetFile);
setEvent(e);
};
// 이미지 파일이 변경될 떄 이벤트 발생
useDidMountEffect(() => {
if(event && onInput) onInput(event, imageFile);
}, [imageFile, event]);
return (
// ...커스터마이징 image input 코드
<input
id={id}
className="hidden"
type="file"
accept="image/*"
onInput={handleInput}
{...rest}
/>
)
}
여기까지 했을 때 해결했다고 생각했지만 조건문으로 event 상태와 imageFile 상태가 존재하는 경우 외부 이벤트 리스너가 실행되도록 되어있습니다.
event 상태는 input 요소 이벤트 발생시 할당되기 때문에 file prop으로 눈속임 한 경우 event 객체가 없는 상태라서 외부의 이벤트 리스너가 실행되지 않습니다.
우선 첫 렌더링 이후, imageFile의 상태가 처음 변경될 때 딱 1회 실행하는 커스텀 useEffect를 정의했습니다.
이후 해당 Effect 내부에서 이미지 인풋 요소에 dispatchEvent 함수를 활용한 input 이벤트를 발생시킵니다.
이렇게 하면 prop으로 파일을 전달받은 경우에도 이벤트가 발생하기 때문에 이벤트 핸들러가 실행될 수 있습니다.
function ImageInput({ onInput, file } : ImageInputProps) {
const [imageFile, setImageFile] = useState<File | undefined>(file);
const [event, setEvent] = useState<ChangeEvent<HTMLInputElement>>();
const handleInput = (e: ChangeEvent<HTMLInputElement>) => {
const targetFile = e.target.files ? e.target.files[0] : undefined;
setImageFile(targetFile);
setEvent(e);
};
// imageFile이 첫 번째로 변경된 경우 해당 Effect가 발생
useDidMountAfterOnceEffect(() => {
// prop으로 file을 전달받은 경우 input에 이벤트를 발생
if (file) {
const inputEvent = new Event('input', { bubbles: true });
inputRef.current?.dispatchEvent(inputEvent);
}
}, [imageFile]);
useDidMountEffect(() => {
// 이제는 이벤트가 존재!
if(event && onInput) onInput(event, imageFile);
}, [file]);
return (
// ...커스터마이징 image input 코드
<input
id={id}
className="hidden"
type="file"
accept="image/*"
onInput={handleInput}
{...rest}
/>
)
}
이제 이미지 상태가 변경되는 모든 상황에서 이벤트 핸들러가 실행됩니다.
form2에서 form1로 이동해 이미지 파일의 미리보기를 사용하는 경우에도 이미지 상태가 undefined로 잘 변경되는 것을 확인할 수 있습니다.
