프론트엔드 - 14

송현섭 ·2023년 4월 11일

프론트엔드

목록 보기
14/24
post-thumbnail

이미지 업로드 (이미지 프로세스)

  • 이미지 프로세스 이해를 위해서는 Storage 사용을 알아야 함

    Storage = 여러 컴퓨터를 연결시킨 큰 용량을 담을 수 있는 DB (Storage 또한 컴퓨터임)







  • 위와 같이 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}
/> 
  • input 태그에 ref={fileref} 로 useRef를 담은 변수를 지정하면 fileRef 를 사용해서 ref가 지정된 태그를 불러올 수 있음





const onClickImage = (): void => {
    // 기존 방식: document.getElementById("파일태그ID")?.click();
    fileRef.current?.click();
  };
  • onClick 함수를 하나 만들고, 함수 안에 fileRef.current?.click() 넣어 줌
    *onClickImage 함수가 실행되면 ref ={fileRef}가 들어있는 태그를 click(클릭)하게 됨





<button onClick={onClickImage}>이미지 등록 버튼</button>
  • 해당 onClickImage 함수를 새로 만든 button 태그에 넣어 줌








  • 결과적으로 button을 클릭하면 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 ?? "");
  };
  • 업로드하는 파일이 없거나, 조건에 걸어둔 용량크기보다 클 경우 함수를 return 시켜서 아래의 uploadFile API가 실행되지 않도록 함








이미지 확장자 검증

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 ?? "");
  };

  };
  • 파일의 확장자가 jpeg, png 둘 다 아니면 함수를 return 해서 파일 업로드가 불가능하도록 함







<input
  style={{ display: "none" }}
  onChange={onChangeFile}
  type="file"
  ref={fileRef}
  accept="image/jpeg,image/png" // 띄어쓰기 없이 콤마(,)를 기준으로 작성합니다.
  // accept를 추가하면 지정되지 않은 확장자는 선택 자체가 불가합니다.
/>
  • input 태그의 속성 중 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;
};
  • 이미지 유효 여부를 검증하는 부분을 컴포넌트로 따로 분리할 경우, 각 조건에 맞지 않아서 return 되더라도 해당 조건을 담은 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 함수도 종료되게 됨

profile
막 발걸음을 뗀 신입

0개의 댓글