
uploadFile 이라는 API 가 있을 때 파일을 선택하고, 해당 API를 요청하게 되면 백엔드에서 Storage로 파일을 전송
Storage 는 백엔드에게서 파일을 받은 후, 이미지를 조회할 수 있는 이미지 주소를 백엔드에게 넘겨 줌
*Storage는 어디에 있는가 ? => AWS, GCP 같은 클라우드 안에 있음
백엔드는 받은 이 주소를 DB에 저장
*이 때 저장하는 것은 파일이 아닌 이미지 주소!
*ex. [https://image저장 클라우드 주소/이미지 주소]로 이후 이미지에 접근 가능
이미지 업로드 실습
1. apollo-upload-client 설치 및 세팅
yarn add apollo-upload-client
yarn add @types/apollo-upload-client --dev // 타입 설치
리액트에는 파일 업로드 처리에 대한 지원 기능이 내장되어 있지 않음
따라서 apollo-upload-client 같은 파일 업로드를 지원하는 라이브러리를 설치해야만 파일을 이미지로 인식하고 제대로 처리가 가능
// app.tsx파일
// import 추가하기
import {createUploadLink} from "apollo-upload-client"
// 세팅 함수 부분
const uplodLink = createUploadLink({
uri : "백엔드 주소"
})
const client = new ApolloClient({
link : ApolloLink.from([uplodLink]),
cache : new inMemoryCache(),
})
app.tsx 에 위와 같이 세팅2. 파일 업로드 가능하도록 기본 세팅
// 이미지 업로드 api 사용을 위한 쿼리 작성
const UPLOAD_FILE = gql`
mutation uploadFile($file: Upload!) {
uploadFile(file: $file) {
url
}
}
`;
export default function ImageUploadPage(): JSX.Element {
const [imageUrl, setImageUrl] = useState("");
const [uploadFile] = useMutation<
Pick<IMutation, "uploadFile">,
IMutationUploadFileArgs
>(UPLOAD_FILE);
// 이미지 업로드 함수
const onChangeFile = async (
event: ChangeEvent<HTMLInputElement>
): Promise<void> => {
const file = event.target.files?.[0]; // 배열로 들어오는 이유: <input type="file" multiple/>일때, 여러 개 업로드 가능하기 때문입니다.
const result = await uploadFile({ variables: { file } });
console.log(result.data?.uploadFile.url);
setImageUrl(result.data?.uploadFile.url ?? "");
};
return (
<>
<input onChange={onChangeFile} type="file" />
<img src={`https://storage.googleapis.com/${imageUrl}`} />
</>
);
return 문에 input type="file" 로 input 태그를 만들어 주고, 아래에 이미지 주소를 받아 올 수 있는 img 태그를 만들어 줌
*img 태그의 type이 file 인 경우 파일선택 가능한 기본 버튼이 생기고 이를 통해 파일을 선택 가능
onChange 함수를 input 태그에 바인딩해서 파일을 선택하면 해당 파일의 정보(이름, 크기..)가 event 객체에 담기고 이를event.target.files 를 통해 files 배열에서 확인 가능
조회한 파일 정보를 변수 file 에 담고, uploadFile Mutation 의 변수 값으로 담아서 API 요청을 날림
요청 후 받은 응답(result)에서 url정보(result.data?.uploadFile.url)를 빼내서 ImageUrl 변수에 담음
응답으로 받은 url 을 담은 변수 ImageUrl 을 img 태그의 src 경로에 넣어서 화면상에 이미지 표시
*이미지는 클라우드 Storage에 저장되어 있기 때문에 클라우드 주소 + DB에 저장된 이미지 URL 로 조회해야 함!
*결과적으로 위의 onChangeFile 함수가 실행되는 것은 input 태그(파일선택 버튼)를 클릭했다는 것이고, 파일이 선택되면 그 파일 정보를 담은 객체를 API 요청으로 보내서 응답으로 받은 url을 이용해 이미지를 화면에 표시하는 것!
+a) Input file 타입의 기본 태그 숨기기

기본 input file 태그는 css 속성의 부분적인 변형만 가능
따라서 원래 태그는 숨기고 새로 HTML 태그를 만들어서 해당 태그를 꾸미는 방식을 주로 사용 (2가지 방식)
Input 태그 숨기기 - 방법 1 (useRef 활용)
import { useRef } from 'react';
export default function ImageRefPage(): JSX.Element {
const fileRef = useRef();
}
useRef 사용 가능<input
style={{ display: "none" }}
onChange={onChangeFile}
type="file"
ref={fileRef}
/>
ref={fileref} 로 useRef를 담은 변수를 지정하면 fileRef 를 사용해서 ref가 지정된 태그를 불러올 수 있음const onClickImage = (): void => {
// 기존 방식: document.getElementById("파일태그ID")?.click();
fileRef.current?.click();
};
onClick 함수를 하나 만들고, 함수 안에 fileRef.current?.click() 넣어 줌<button onClick={onClickImage}>이미지 등록 버튼</button>
onClickImage 함수를 새로 만든 button 태그에 넣어 줌
onClickImage 함수가 실행되고, fileRef 속성이 들어있는 태그를 클릭(fileRef.current.click( ))하게 되어 input 태그를 클릭한 것과 같은 결과를 도출함Input 태그 숨기기 - 방법 2 (label 태그, htmlFor 활용)
<div>
<label htmlFor="fileTag">이거 눌러도 실행돼요!</label>
<img style={{ width: '500px' }} id="image" />
<input id="fileTag" type="file" onChange={readImage}></input>
</div>
label 태그에는 htmlFor 라는 속성이 있음
이 속성에 값을 넣으면, 그 값과 같은 id를 가진 태그를 찾아 해당 태그의 기능을 실행해 줌
이미지 업로드 시 필요 이상으로 큰 사이즈나, 초고화질의 이미지를 보내면 저장공간을 많이 차지하게 되고, 비용측면에 부담이 될 수 있음
또한 이미지 업로드에 html파일, 한글파일 등을 잘못 업로드 하는 경우도 발생가능
생길 수 있는 변수를 제어할 검증단계(이미지 크기 제한, 확장자 검증..)가 필요!!
이미지 유무와 사이즈 검증
const onChangeFile = async (
event: ChangeEvent<HTMLInputElement>
): Promise<void> => {
const file = event.target.files?.[0];
// 검증 로직
if (typeof file === "undefined") {
alert("파일이 없습니다.");
return;
}
if (file.size > 5 * 1024 * 1024) { // 5MB
alert("파일 용량이 너무 큽니다.(제한: 5MB)");
return;
}
// API 호출 로직
const result = await uploadFile({ variables: { file } });
setImageUrl(result.data?.uploadFile.url ?? "");
};
이미지 확장자 검증
const onChangeFile = async (
event: ChangeEvent<HTMLInputElement>
): Promise<void> => {
const file = event.target.files?.[0];
~~~ // 기타 검증 로직 생략
if (!file.type.includes("jpeg") && !file.type.includes("png")) {
alert("jpeg 또는 png 파일만 업로드 가능합니다.");
return;
}
const result = await uploadFile({ variables: { file } });
setImageUrl(result.data?.uploadFile.url ?? "");
};
};
<input
style={{ display: "none" }}
onChange={onChangeFile}
type="file"
ref={fileRef}
accept="image/jpeg,image/png" // 띄어쓰기 없이 콤마(,)를 기준으로 작성합니다.
// accept를 추가하면 지정되지 않은 확장자는 선택 자체가 불가합니다.
/>
accept 속성을 적용해서 해당 속성의 값에 해당되지 않을 경우 파일 선택 자체가 불가능하도록 하는 방법도 가능 컴포넌트 분리 시 조건에 해당 안 되면 return으로 업로드 중단 시키기
// 하위 컴포넌트 (자식)
export const checkValidationFile = (file?: File): boolean => {
if (typeof file === "undefined") {
alert("파일이 없습니다.");
return false;
}
if (file.size > 5 * 1024 * 1024) {
alert("파일 용량이 너무 큽니다.(제한: 5MB)");
return false;
}
if (!file.type.includes("jpeg") && !file.type.includes("png")) {
alert("jpeg 또는 png 파일만 업로드 가능합니다.");
return false;
}
return true;
};
checkValidationFile 함수만 종료될 뿐, 상위컴포넌트에 위치한 onChangeFile 함수는 종료되지 않기 때문에 파일 업로드를 중단시킬 수 없음!// 상위 컴포넌트 (부모)
const onChangeFile = async (
event: ChangeEvent<HTMLInputElement>
): Promise<void> => {
const file = event.target.files?.[0];
const isValid = checkValidationFile(file);
if (!isValid) return;
const result = await uploadFile({ variables: { file } });
setImageUrl(result.data?.uploadFile.url ?? "");
};
이 경우 상위 컴포넌트 안에 검증 함수 checkValidationFile 의 인자로 file 을 넣고, 이 함수 자체를 isValid 변수에 담아 줌
만약 인자로 들어간 file 이 조건에 걸린다면 false가 return 되고, isValid의 값도 false가 되면서 상위 컴포넌트의 onChangeFile 함수도 종료되게 됨