1탄 2탄에 나누어 리액트에서 첨부파일을 가져와 화면에 보여주고, 가져온 첨부파일을 API로 요청하는 방법에 대한 글을 써보려 한다.
이번 글에서는 버튼
클릭으로 input
파일 선택창을 실행하는 방법과, fileChangeHandler
에서 FileLeader를 사용해서 이미지, 오디오 데이터 정보를 가져오는 방법에 대한 내용을 먼저 다루고,
그 다음글에서 react query의 useMutaion
으로 이미지/오디오 파일 업로드 API 구현 방법과 실무에서 formData로 API 요청 시의 다양한 경우(json Data 함께 넣기, 여러개 파일 formData에 넣기)대처 방법에 대해 다뤄볼 예정이다.
사용자가 버튼을 클릭하면 파일 선택 창을 열어 이미지 파일을 선택할 수 있게 하는 기능은 다음과 같이 구현할 수 있다.
const ref = useRef<HTMLInputElement>(null);
const fileHandler = () => {
if (ref.current) {
ref.current!.click();
}
};
const handleFileChange = () =>{
//...
}
<button onClick={fileHandler}>
<input
name={name}
type="file"
accept="image/*"
ref={ref}
onChange={handleFileChange}
className="a11y-hidden"
/>
<button>
💡 <
!
> 단언 연산자를 사용한 이유?체이닝에서 ref.current!.click()과 같이
!
를 사용해 주면 ref.current가 존재한다, 즉 Nullish(null이나 undefined)한 값이 아니라는 확신을 심어줄 수 있다.
그렇지 않을 시 타입 에러가 발생할 수 있다
const ref = useRef<HTMLInputElement>(null);
useRef
는 React의 Hook 중 하나로, DOM 요소에 직접적으로 접근할 수 있게 해주는 친구로, 여기서는 <input type="file">
엘리먼트에 접근하기 위해 사용된다.
타입 스크립트에서는 useRef<HTMLInputElement>(null)
과 같이 초기값은 null
로 지정해줄 것.
버튼 클릭 이벤트 함수 안에서 ref.current
가 존재할 경우, 즉 input 엘리먼트가 마운트된 상태라면 해당 엘리먼트의 click() 메서드를 호출해 준다.
그래서 사용자가 직접 input을 클릭한 것처럼 버튼 클릭 시 파일 선택창을 열게 한다.
const fileHandler = () => {
if (ref.current) {
ref.current.click();
}
};
<button onClick={fileHandler}>
<input
name={name}
type="file"
accept="image/*"
ref={ref}
onChange={handleImageChange}
className="a11y-hidden"
multiple
/>
</ button>
onClick={fileHandler}
: 버튼 클릭 시 fileHandler
함수를 실행하여 파일 선택 창을 연다.ref
: ref={ref}
를 통해 이 input에 대한 참조를 저장 onChange={handleImageChange}
: 파일이 선택되었을 때 실행될 콜백 함수className="a11y-hidden"
: input을 화면상에서 숨기기 위해 미리 css에서 설정한 클래스 가져오기multiple
: 한 번에 여러 개의 파일을 가져올 때 사용, 그렇지 않을 때에는 생략이미지 데이터를 미리 화면에 띄워 보여주거나, 오디오 데이터의 이름, 실행 시간 등의 값을 가져올 때 FileReader
를 활용할 수 있다.
먼저 input 파일 선택창 실행 후 받아온 파일을 다루기 위해 위 코드에서 handleFileChange
에 해당하는 onChange 이벤트 함수를 세팅해 주자.
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
console.log("선택된 파일:", file);
}
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files) {
for(const file of files){
console.log("선택된 파일:", file);
}
}
잠깐 💡 트러블 슈팅! - 동일 파일 연속으로 안가져와지는 오류
파일 선택창에서 한 번 가져온 파일이 연속적으로 불러와지지 않는 오류가 있다.
구글링하다 발견한 아주 간단한 방법은,const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { const file = event.target.files?.[0]; event.target.value = ""; // 동일 파일 업로드 오류 방지 if (file) { console.log("선택된 파일:", file); }
onChange 시
e.target.value
에 담긴 값이 유지되어서 같은 파일 재업로드가 막히는 것 같다.
event.target.value
값을 초기화해 주면 해결!
이미지를 미리 보여줄 수 있는 로직은 아래와 같이 구현해 준다.
const [selectedImage, setSelectedImage] = useState("");
const [imageFile, setImageFile] = useState<File>()
const handleImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
// FileReader를 사용해 파일 읽기(JS의 내장기능)
const fileReader = new FileReader();
// blob 타입의 file을 url 형태로 만들기
fileReader.readAsDataURL(file);
// 읽기 작업이 완료되면, 결과물이 data로 들어오고 아래 함수가 실행된다
fileReader.onload = (e) => {
if (typeof data.target?.result === "string") {
// useState 초기값으로 string("")을 넣었기 때문에 꼭 타입을 지정해서 변경해 줄 것
setSelectedImage(e.target?.result as string) // 미리보기를 위한 임시 url
setImageFile(file) // API로 보내기 위한 File 저장
}
}
}
}
//...
return(
<button onClick={fileHandler}>
<input
name={name}
type="file"
accept="image/*"// image 파일만 받아올 수 있도록 설정
ref={ref}
onChange={handleImageChange}
className="a11y-hidden"
/>
<img src={selectedImage} alt="이미지" />
</ button>
)
핵심은 딱 이 세 단계를 기억할 것!
1. const fileReader = new FileReader();
2. fileReader.readAsDataURL(file);
3. fileReader.onload = (e) => {}
오디오 파일 이름과 재생 시간을 가져오는 로직이다.
파일 이름은 그냥 file.name으로 가져올 수 있지만, 재생 시간과 같은 오디오 정보를 가져올 때에는 오디오 객체가 필요하다.
const [fileName, setFileName] = useState("");
const [duration, setDuration] = useState(0)
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
console.log("선택된 파일:", file);
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
// 읽기 작업이 완료되면, 오디오 객체를 생성하고 메타데이터를 로드
const audio = new Audio(e.target?.result as string);
audio.onloadedmetadata = () => {
// 메타데이터 로드가 완료되면, 파일 이름과 재생 시간을 가져오기
// file 이름
setFileName(file.name);
// 추출된 오디오 재생 시간
setDuration(audio.duration) // (2분 30초=)150와 같이 초 단위로 가져와 짐
};
};
}
//...
return(
<>
<button onClick={fileHandler}>
<input
name={name}
type="file"
accept="audio/*" // audio 파일만 받아올 수 있도록 설정
ref={ref}
onChange={handleImageChange}
className="a11y-hidden"
/>
</ button>
<p>{fileName}</p>
<p>{duration}s</p>
</>
)
여기서의 포인트는
1) const audio = new Audio(e.target?.result as string);
- 오디오 객체 생성
2) audio.onloadedmetadata = () => {}
안에서 오디오 데이터 가져오기!