구현할 기능 및 문제 상황
다음과 같이 갤러리에서 이미지를 업로드하는 기능을 구현해야 했다.
각자 다른 페이지에서 두 번 씩이나 쓰이기 때문에 이미지를 등록하는 기능을 hook으로 구현해서 재사용을 할 예정이다.
구현에 앞서 이미지 등록에 필요한 개념들을 정리해보고자 한다!
이미지 등록 관련 개념 정리
1. Blob이란?
Blob(Binary Large Object)은 큰 파일(이미지, 비디오 등)을 다루기 위한 데이터 타입이다. 읽기 전용의 원시 데이터를 나타내며 주로 대용량 바이너리 데이터를 다루기 위해 사용된다. 파일 업로드/다운로드, 바이너리 데이터를 이미지로 생성, 미디어 파일 작업 등에 유용하다.
주요 기능은 다음과 같다.
1. Blob 생성
Blob() 생성자를 사용하여 배열 또는 문자열로부터 Blob을 생성할 수 있다.
const blob = new Blob(["안녕하세요"], { type: 'text/plain' });
2. Blob 메서드
- slice(start, end, contentType): 원본 Blob의 지정된 범위 데이터를 포함하는 새로운 Blob을 생성한다.
- size: Blob의 크기를 바이트 단위로 반환한다.
- type: Blob의 MIME 타입을 반환한다.
3. URL.createObjectURL()
Blob 콘텐츠에 접근할 수 있는 임시 URL을 생성한다.
파일을 업로드하기 전에 브라우저에서 파일을 미리보기 위해 URL을 생성한다. 생성된 URL은 클라이언트 측에서만 사용된다.
=> 파일 미리보기 기능을 구현할 때 사용된다!
const url = URL.createObjectURL(blob);
코드로 훅 구현하기
import { useState, useEffect } from 'react'
type UploadImage = {
file: File
thumbnail: string
type: string
}
export default function useImageUploader() {
const [selectedImage, setSelectedImage] = useState<UploadImage | null>(null)
useEffect(() => {
return () => {
if (selectedImage) {
URL.revokeObjectURL(selectedImage.thumbnail)
}
}
}, [selectedImage])
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const fileList = e.target.files
if (fileList && fileList[0]) {
const url = URL.createObjectURL(fileList[0])
setSelectedImage({
file: fileList[0],
thumbnail: url,
type: fileList[0].type.split('/')[0],
})
}
}
return {
selectedImage,
handleUpload,
}
}
타입 설명
type UploadImage = {
file: File;
thumbnail: string;
type: string;
};
- file: 사용자가 업로드한 파일 자체를 나타낸다.
- thumbnail: 파일의 썸네일 URL! 미리보기에 사용된다. URL.createObjectURL을 통해 생성된 URL!
- type: 파일의 MIME 타입으로, image/jpeg, image/png 등 파일의 형식을 나타낸다.
코드 설명
const [selectedImage, setSelectedImage] = useState<UploadImage | null>(null)
useState를 사용해 업로드된 이미지 파일을 저장한다.
useEffect(() => {
return () => {
if (selectedImage) {
URL.revokeObjectURL(selectedImage.thumbnail)
}
}
}, [selectedImage])
useEffect 사용 이유
React 컴포넌트에서 URL.createObjectURL를 사용하여 파일 미리보기를 구현할 때 useEffect를 사용했다.
URL.createObjectURL를 호출할 때마다 고유한 객체 URL이 생성되기 때문에 이런 방식은 메모리를 소비하게 된다. 따라서 메모리 누수를 방지하기 위해서 생성된 URL을 해제해주는 부분을 추가했다. selectedImage가 변경될 때만 객체 URL을 생성하도록!
관련 문서는 다음과 같다.
MDN 문서에서도 이렇게 설명하고 있다.
Memory managementEach time you call createObjectURL(), a new object URL is created, even if you've already created one for the same object. Each of these must be released by calling URL.revokeObjectURL() when you no longer need them.
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const fileList = e.target.files
if (fileList && fileList[0]) {
const url = URL.createObjectURL(fileList[0])
setSelectedImage({
file: fileList[0],
thumbnail: url,
type: fileList[0].type.split('/')[0],
})
}
}
이 코드를 자세히 설명하면 다음과 같다.
const fileList = e.target.files;
파일 입력 요소에서 선택된 파일 목록을 가져온다.
fileList는 FileList 객체로, 선택된 파일들을 포함한다.
if (fileList && fileList[0]) {
파일 목록이 존재하고 첫 번째 파일이 있는지 확인한다.
const url = URL.createObjectURL(fileList[0]);
URL.createObjectURL 메서드를 사용하여 파일의 임시 URL을 생성한다.
파일의 미리보기를 위해 사용한다!
setSelectedImage({
file: fileList[0],
thumbnail: url,
type: fileList[0].type.split('/')[0],
});
setSelectedImage를 사용하여 selectedImage 상태를 업데이트한다.
type: fileList[0].type.split('/')[0],
파일 형식들이 대부분 image/jpeg, image/png와 같이 슬래시(/)를 기준으로 구분이 되기 때문에
MIME 타입의 첫 번째 부분인 주요 파일 형식을 명확하게 추출해주는 부분이다!
페이지에서 훅 사용하기
import { useRef } from 'react'
import useImageUploader from '../../hooks/useImageUpload'
export default function FirstRegisterStoreInfo() {
const navigate = useNavigate()
const fileInputRef = useRef<HTMLInputElement>(null)
const { selectedImage, handleUpload } = useImageUploader()
}
return (
<StyledContainer>
...
<StyledSection>
<StyledLabel>가게 배너 사진 등록</StyledLabel>
<StyledUploadBox onClick={() => fileInputRef.current?.click()}>
{selectedImage ? (
<img
src={selectedImage.thumbnail}
alt="Preview"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
<>
<StyledUploadImg src={UploadImg} alt="업로드 아이콘" />
<StyledUploadText>
갤러리에서 이미지 등록하기
</StyledUploadText>
</>
)}
</StyledUploadBox>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
onChange={handleUpload}
/>
</StyledSection>
...
</StyledContainer>
)
}
import useImageUploader from '../../hooks/useImageUpload';
const { selectedImage, handleUpload } = useImageUploader();
useImageUploader 훅을 불러와서 selectedImage와 handleUpload 함수를 사용한다.
const fileInputRef = useRef<HTMLInputElement>(null);
useRef를 사용하여 파일 입력 요소에 접근한다. 왜 useRef를 사용했는지는
https://5ffthewall.tistory.com/111
[React/리액트]React에서 요소에 접근과 제어하는 방법 / useRef와 getElementByIdref 비교
위 글에서 파일 업로드를 구현하다가 궁금증이 생겼다. 대부분의 파일 업로드 코드들이 useRef를 사용해서 나도 useRef로 값을 참조했는데 useState를 써도 될 것 같다는 생각이 들었다. 그래서 직접
5ffthewall.tistory.com
이 글에서 설명한다.
<StyledUploadBox onClick={() => fileInputRef.current?.click()}>
{selectedImage ? (
<img
src={selectedImage.thumbnail}
alt="Preview"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
<>
<StyledUploadImg src={UploadImg} alt="업로드 아이콘" />
<StyledUploadText>갤러리에서 이미지 등록하기</StyledUploadText>
</>
)}
</StyledUploadBox>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
onChange={handleUpload}
/>
StyledUploadBox를 클릭하면 fileInputRef.current?.click()가 호출되어 파일 선택 창이 열린다.
파일이 선택되면 handleUpload 함수가 호출되어 selectedImage 상태가 업데이트된다.
selectedImage가 있으면 미리보기 이미지를 표시하고 없으면 기본 업로드 아이콘과 텍스트를 표시하도록 조건부 렌더링을 하였다.
<img
src={selectedImage.thumbnail}
alt="Preview"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
URL.createObjectURL로 만들어두었던 미리보기 (클라이언트에서만 쓰는 url)를 img의 src에 넣어주면 된다!
참고로 서버로 url을 보낼 때는 저 url을 보내는게 아니라 바로 fileList[0]를 보내면 된다 ㅎㅎ
다른 페이지에서 같은 훅을 재사용하는 방법은 다음과 같다.
...
import useImageUploader from '../../hooks/useImageUpload'
import { useRef } from 'react'
interface RegisterMenuProps {
index: number
menu: {
name: string
price: string
}
onChange: (e: React.ChangeEvent<HTMLInputElement>, index: number) => void
}
export const RegisterMenu = ({ index, menu, onChange }: RegisterMenuProps) => {
const { selectedImage, handleUpload } = useImageUploader()
const fileInputRef = useRef<HTMLInputElement>(null)
return (
<StyledMenuRow>
<StyledUploadBox onClick={() => fileInputRef.current?.click()}>
<StyledUploadImg
src={selectedImage ? selectedImage.thumbnail : Camera}
alt={selectedImage ? '미리보기' : '업로드 아이콘'}
isThumbnail={!!selectedImage}
/>
</StyledUploadBox>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
onChange={handleUpload}
/>
<StyledMenuInputContainer>
<StyledMenuInput
type="text"
name="name"
placeholder="메뉴 이름"
value={menu.name}
onChange={(e) => onChange(e, index)}
/>
<StyledMenuInput
type="text"
name="price"
placeholder="가격"
value={menu.price}
onChange={(e) => onChange(e, index)}
/>
</StyledMenuInputContainer>
</StyledMenuRow>
)
}
이런 식으로 재사용이 가능하도록 만들 수 있다!
완성된 화면
'React' 카테고리의 다른 글
[React/리액트] 상태 관리 추천 / Zustand 사용법 (0) | 2024.08.03 |
---|---|
[React/TS] 주소 검색 기능 구현하기 / 주소를 위도 경도로 변환하기 / 카카오맵 API 타입 정의 (0) | 2024.07.30 |
[React/리액트]React에서 요소에 접근과 제어하는 방법 / useRef와 getElementByIdref 비교 / input에 useRef를 쓰는 이유 (1) | 2024.07.28 |
[React/CSS-in-JS] Styled-Components를 사용하여 고정 푸터(버튼)와 스크롤 가능한 콘텐츠 구현하기 (0) | 2024.07.27 |
[React/리액트] ThemeProvider로 전역으로 디자인 시스템 설정하기 (0) | 2024.07.20 |