리액트에서 파일 첨부하기 - File input 숨기고, 버튼으로 대체하는 방법과 프리뷰까지!
by 담담이담mock data로 메신저를 구현하는 토이 프로젝트에서
협업하는 디자이너분이 파일 전송 기능을 구현해주셨다.
시연해보고 싶다면
https://react-messenger-19th-beta.vercel.app/chats
Telegram
유담이와 연우의 텔레그램
react-messenger-19th-beta.vercel.app
먼저 왼쪽 사진에 보이는 클립 버튼을 누르면 파일 첨부 창이 뜨고
이미지를 전송하면 위의 사진처럼 전송이 되어야한다.
일단 html의 기본 속성으로 type=file을 주게 되면 이런 못난 창이 뜨게 되는데
이걸 감추고, 클립 버튼을 누르면 파일 첨부 창이 뜨게 만들어야한다.
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileButtonClick = () => {
if (!fileInputRef.current) return;
fileInputRef.current.click(); // 숨겨진 file input의 클릭 이벤트를 실행
};
// 사진 파일 첨부 후 전송
const fileInputSubmit = (e: ChangeEvent<HTMLInputElement>) => {
const targetFile = e.target.files?.[0];
if (!targetFile) return;
const { size, name } = targetFile;
const url = URL.createObjectURL(targetFile);
if (!selectedChats) return;
dispatch(
addNewChat({
id: String(+selectedChats[selectedChats.length - 1].id + 1),
// me가 보내는 사람임
senderId: me.id,
text: inputValue,
createdAt: Date().toString(),
photo: { name, size, url },
}),
);
dispatch(setReplyMessage(null));
};
return (
<>
<ChatInputWrapper onSubmit={handleInputSubmit}>
<div className="file_audio_button">
<button type="button" onClick={handleFileButtonClick}>
<FileInputIcon alt="파일 인풋 아이콘" />
<input
ref={fileInputRef}
id="fileInput"
// 파일 인풋을 숨기고, 파일 아이콘 클릭 시 파일 첨부창이 뜨게 함
style={{ display: 'none' }}
type="file"
accept=".jpg, .png"
onChange={fileInputSubmit}
/>
</button>
아래 리턴문의 코드를 보게 되면, input에 인라인 스타일링으로
display:none을 줌으로써 유저에게 보이지 않게 만들었고
대신 fileInputRef라는 useRef의 리턴값을 연결해주었다.
이후 파일 인풋 아이콘이 클릭되면 fileInputRef의
click 속성이 트리거 되도록 코드를 구성했다.
내가 필요했던 건 파일의 이름, 사이즈, url이었다.
그래서 먼저 onchange 함수를 input에 연결해주고,
e.target.files[0]을 해줌으로써
선택된 파일의 맨 첫 번째 값을 가져왔다.
지금은 Input에 multiple 속성을 주지 않았기에
하나의 파일만 선택 가능하다.
이후 이 값을 변수로 만들어, size와 name을 추출하고
url은 JavaScript에서 제공하는 URL.createObjectURL을 이용해
File 객체를 URL로 변환해주었다.
내가 서버를 사용하지 않았기 때문에
url을 저런 식으로 저장하는 게 적절하지 않았을 수도 있다는 생각이 든다.
하지만 프리뷰를 구현할 때 URL.createObjectURL를 자주 사용한다고 한다!
이후 혼자 공부를 하며
프리뷰 이미지를 구현해보았다.
그런데 강의에서 URL.createObjectURL이 아닌
FileReader를 사용하는 거 아닌가!
찾아보니, URL.createObjectURL은 가짜 Url을
FileReader는 다른 브라우저에서도 사용 가능한 url를 리턴해준다고 한다.
즉, 둘 다 파일의 내용을 바로 브라우저에서 사용할 수 있는 base64 인코딩된 URL로 변환하며,
이 URL은 <img> 태그의 src 속성 값으로 즉시 사용될 수 있어 이미지 미리보기 기능 구현에 사용된다!
"use client";
import { useRef, useState } from "react";
import styles from "./ImagePicker.module.css";
import Image from "next/image";
export default function ImagePicker({ label, name }) {
const [pickedImage, setPickedImage] = useState();
const fileInputRef = useRef();
const handleButtonClick = () => {
fileInputRef.current.click();
};
const handleChangePickedImage = (e) => {
const file = e.target.files[0];
if (!file) {
setPickedImage(null);
}
const fileReader = new FileReader(file);
fileReader.onload = () => {
setPickedImage(fileReader.result);
};
fileReader.readAsDataURL(file);
};
return (
<div className={styles.picker}>
<label htmlFor={name}>{label}</label>
<div className={styles.controls}>
<div className={styles.preview}>
{!pickedImage && <p>No image picked yet</p>}
<Image src={pickedImage} alt="The image selected by the user" fill />
</div>
<input
className={styles.input}
type="file"
id={name}
accept="image/png, image/jpeg"
name={name}
ref={fileInputRef}
onChange={handleChangePickedImage}
/>
<button
className={styles.button}
type="button"
onClick={handleButtonClick}
>
Pick an Image
</button>
</div>
</div>
);
}
readAsDataURL 함수를 이용해 파일을 읽어 url로 변환한다.
이 과정이 완료되면, fileReader.result에 업로드 된 파일이 저장되고
onload 함수가 자동으로 트리거된다.
gpt의 설명
- FileReader 인스턴스 생성: FileReader 객체를 생성합니다. 코드에서는 new FileReader()를 호출하여 객체를 초기화합니다.
- 파일 읽기 설정: fileReader.onload를 설정하여 파일 로드가 완료되었을 때 실행할 함수를 정의합니다. 이 함수 내에서 fileReader.result에 접근하여 읽은 데이터(URL)를 setPickedImage를 사용하여 pickedImage 상태에 저장합니다.
- 파일 읽기 시작: fileReader.readAsDataURL(file)를 호출하여 파일의 내용을 데이터 URL로 읽기 시작합니다. 이 호출은 비동기적으로 실행되므로, 파일 로딩이 완료될 때까지 기다리지 않고 즉시 다음 코드 라인으로 넘어갑니다.
- onload 이벤트 처리: 파일의 데이터가 모두 읽혀지면 onload 이벤트가 발생하고, 이전에 설정한 onload 핸들러가 실행됩니다. 이 핸들러 내에서 fileReader.result를 pickedImage 상태에 저장하여, 이 데이터 URL을 사용하여 이미지를 렌더링하거나 다른 처리를 할 수 있습니다.
주의할 점!!!!
https://legacy.reactjs.org/docs/uncontrolled-components.html
Uncontrolled Components – React
A JavaScript library for building user interfaces
legacy.reactjs.org
레거시긴 하지만 리액트 공식문서에 따르면
React에서 파일 인풋은 항상 비제어 컴포넌트(uncontrolled component)다.
인풋 값이 사용자에 의해서만 설정될 수 있고 프로그래밍적으로 설정될 수 없기 때문이다.
비제어 컴포넌트는 React에서 상태를 직접 관리하지 않는 요소를 말한다.
즉, 리액트에서 useState로 만든 상태값과 input의 value가 연결되지 않는다는 것이다!!
인풋은 거의 대부분 제어 컴포넌트이지만
파일 인풋만 비제어 컴포넌트여야한다.
참고 자료)
https://velog.io/@e_juhee/image-preload
이미지 미리보기 :: createObjectURL vs FileReader
임시 URL을 생성하는 두 가지 방법을 알아보자!createObjectURL로 생성한 이미지는 가짜 URL이다.👉🏻 다른 브라우저에서는 접근이 불가능하다.아래와 같은 형태의 url이 생성된다.FileReader로 생성한
velog.io
https://developer.mozilla.org/ko/docs/Web/API/FileReader
FileReader - Web API | MDN
FileReader 객체는 웹 애플리케이션이 비동기적으로 데이터를 읽기 위하여 읽을 파일을 가리키는File 혹은 Blob 객체를 이용해 파일의 내용을(혹은 raw data버퍼로) 읽고 사용자의 컴퓨터에 저장하는
developer.mozilla.org
https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static
URL: createObjectURL() static method - Web APIs | MDN
The URL.createObjectURL() static method creates a string containing a URL representing the object given in the parameter.
developer.mozilla.org
https://developer.mozilla.org/ko/docs/Web/API/FileReader/readAsDataURL
FileReader.readAsDataURL() - Web API | MDN
readAsDataURL 메서드는 컨텐츠를 특정 Blob 이나 File에서 읽어 오는 역할을 합니다. 읽어오는 read 행위가 종료되는 경우에, readyState 의 상태가 DONE이 되며, loadend 이벤트가 트리거 됩니다. 이와 함께, b
developer.mozilla.org
'프로젝트' 카테고리의 다른 글
eslint-plugin-prettier를 삭제한 이유(feat. delete enter, delete cr 에러) (1) | 2024.01.26 |
---|---|
CI를 위한 GitHub Action 설정하기 (0) | 2024.01.20 |
NextJS 프로젝트 초기 설정 (feat 리액트 쿼리, 리덕스, 타입스크립트) (0) | 2024.01.20 |
nvm과 npm 그리고 npx의 차이 (0) | 2024.01.19 |
Component definition is missing display name 에러 (0) | 2024.01.06 |
블로그의 정보
유명한 담벼락
담담이담