input file 컴포넌트에 기본값이 있는 것 처럼 속이기

BeomDev·2024년 6월 4일
post-thumbnail

타입이 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로 잘 변경되는 것을 확인할 수 있습니다.

0개의 댓글