2023 . 4 . 6

Junghan Lee·2023년 4월 6일
0

TIL Diary

목록 보기
32/52

Index

이미지 프로세스 이해(서버, 스토리지 포함)
이미지 업로드(아폴로 업로드 라이브러리, 업로드용 화면 그리기)
HTML기본 FILE태그 숨기기(useRef)
이미지 검증


intro

사진은 픽셀 단위에서 모든 칸을 숫자로 변경해(RGB) binary data가 되고 그렇게 변환한 데이터를 백엔드 서버로 보내고 클라우드로 보내고 하면서 전송한다.

이미지 프로세스 이해

이미지 프로세스 이해를 위해서는 storage 사용을 알아야 한다.
storage 또한 컴퓨터이며 여러 컴퓨터들을 연결시켜 놓은 큰 용량을 담을 수 있는 DB

이미지 업로드

uploadFile이라는 api가 있을 때, 파일을 선택하고 UploadFile을 요청하게 될 경우, backend에서 storage로 파일을 전송하게 됩니다.
실제 등록 버튼 부분에는 https://강아지.png 같은 다운로드 url이 들어간다.

이미지 조회

storage에서는 backend로 이미지를 조회할 수 있는 이미지 주소를 넘겨주게 되고, database 에 이미지 주소를 저장한다.(파일 저장이 아닌 주소를 저장)
frontend에서도 이미지 주소를 통해 이미지를 조회
(!! imgFile에 접근할수있는 주소를 저장, file 없음)

storage는 어디에 있나? -> AWS, GCP, AZURE와 같은 클라우드 안에 있다.

try 이미지 업로드

이미지 업로드에 사용할 api는 uploadFile, uploadFile로 받아온 이미지 url을 createBoard에 넣으면 된다. url을 받아오기 위해선 아폴로 업로드 관련 라이브러리 설치가 필요하다.
1) yarn add apollo-upload-client
2) (타입스크립트) yarn add @types/apollo-upload-client --dev

세팅
app.tsx )

// app.tsx파일
// import 추가하기
import {createUploadLink} from "apollo-upload-client"

// 세팅 함수 부분
const uplodLink = createUploadLink({
		uri : "백엔드 주소"
	})

const client = new ApolloClient({
		link : ApolloLink.from([uplodLink]),
		cache : new inMemoryCache(),
	})

파일 업로드 위해 화면 그려주기
파일을 선택할 수 있도록 Input태그의 type에 file로 지정해 그려주자

// 이미지 업로드 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}`} />
    </>
  );

이미지를 보내주고 url을 받아오는 과정
1) OnChange를 통해 이미지 파일을 가져와 file에 넣어줌
2) file을 variables에 넣어 uploadFile Mutation 날려줌
3) 이미지가 정해둔 스토리지에 들어가고 해당 스토리지에서 url을 반환받아 가져옴
4) 콘솔에 받아온 이미지 URL이 찍힘

주의
여기까지의 과정은 이미지를 Storage에 업로드하는 과정일 뿐, 게시글 등록하기 API요청해야 함
!! 해당 Url의 이미지를 보고 싶다면, 저장해둔 스토리지 주소의 뒤에 이미지 url을 적으면 됨
(예시) http://storage.googleapis.com/이미지url

HTML 기본 FILE 태그 숨기기


이 못생긴 기본 input 태그를 숨기고 꾸며주는 방법은 크게 2가지가 있다.
useRef
HTML태그를 선택할 때 보통 getElementId 사용, react에서는 html태그에 접근을 도와주는 역할을 useRef가 하고 있다. 간단한 사용 방법은?

import { useRef } from 'react';

export default function ImageRefPage(): JSX.Element {
	const fileRef = useRef<HTMLInputElement>(null);

}

Import로 useRef를 가져오고 constfileRef = useRef() 작성하는 것이 가장 처음 Ref를 불러오고 사용하는 기본 설정이다. useState나 useEffect처럼 react에서 가져와야 사용할 수 있다. 이렇게 하고 fileRef를 태그에 연결해주면 해당 태그는 fileRef로 불러서 사용할 수 있다.

<input
  style={{ display: "none" }}
  onChange={onChangeFile}
  type="file"
  ref={fileRef}
/> 

이렇게 input태그에 ref={fileRef}를 작성하면 이제 input 태그를 fileRef를 이용해 사용할 수 있다. 그리고 나서, useRef에 있는 기능을 이용하면 된다. 참고로 useRef에는 다양한 기능이 있다.

const onClickImage = (): void => {
    // 기존 방식: document.getElementById("파일태그ID")?.click();
    fileRef.current?.click();
  };

onClick함수의 Ref에는 current안에 click이라는 기능이 있는데 이는 이름 그대로 current는 fileRef에 들어온 태그를 뜻하고 그 태그를 click하겠다는 기능이다.

<button onClick={onClickImage}>이미지 등록 버튼</button>

그리고 새로운 버튼을 하나 만들어 해당 기능을 넣어준다.
이러면 버튼을 클릭했을 때 fileRef.current.click()이 실행될 것이고 그건 useRef에 넣어두었던 input 태그를 클릭한 것과 같은 결과가 나올 것이다.

응용

return (
		<>
			<div>
				<img onClick={onClickImage} style={{ width: '500px' }} id="image" />
				<input
					hidden={true}
					ref={fileRef}
					type="file"
					onChange={readImage}
				></input>
				<button onClick={onClickImage}>이미지 등록 버튼</button>
			</div>
		</>
	);

위 코드에선 미리보기 이미지에도 onClickimage를 넣어주었는데 이 기능이 어떤 기능인지는 위에서 이야기했고 input태그에는 hidden기능을 이용해 태그를 숨겨주었다. 이러면 미리보기 이미지를 클릭할 때, 이미지 등록 버튼을 클릭할 때 모두 input type=file태그를 클릭한 것과 동일한 결과를 볼 수있다.

import { gql, useMutation } from "@apollo/client";
import { ChangeEvent, useRef, useState } from "react";
import {
  IMutation,
  IMutationUploadFileArgs,
} from "../../../src/commons/types/generated/types";

const UPLOAD_FILE = gql`
  mutation uploadFile($file: Upload!) {
    uploadFile(file: $file) {
      url
    }
  }
`;
export default function ImageRefPage(): JSX.Element {
  const [imageUrl, setImageUrl] = useState("");
  const fileRef = useRef<HTMLInputElement>(null);

  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 } });
    setImageUrl(result.data?.uploadFile.url ?? "");
  };

  const onClickImage = (): void => {
    // 기존 방식 : document.getElementById("파일태그ID")?.click();
    fileRef.current?.click();
  };
  return (
    <>
      <div
        style={{ width: "50px", height: "50px", backgroundColor: "gray" }}
        onClick={onClickImage}
      >
        이미지 선택
      </div>
      <input
        style={{ display: "none" }}
        onChange={onChangeFile}
        type="file"
        ref={fileRef}
      />
      <img src={`https://storage.googleapis.com/${imageUrl}`} />
    </>
  );
}

위는 전체 코드이다.

(참고)label태그와 htmlFor 이용하기
Label태그에는 htmlFor이라는 속성이 있는데 이속성에 값을 넣으면 갑소가 똑같은 id를 찾아 그 태그의 기능과 연결해준다.

<div>
				<label htmlFor="fileTag">이거 눌러도 실행돼요!</label>
				<img style={{ width: '500px' }} id="image" />
				<input id="fileTag" type="file" onChange={readImage}></input>
</div>

htmlFor='fileTag'와 input id="filetag'의 값이 똑같다.

결과적으로 label태그를 클릭해도 똑같이 파일을 올릴 수 있다. 그러면 label태그를 원하는 디자인으로 꾸미고 기존 input태그는 안보이도록 css작업하면 된다.

이미지 검증

이미지 업로드시에 필요 이상으로 큰 사이즈의 이미지나 고화질 이미지는 저장공간을 많이 차지해 비용 측면의 부담이 될 수 있다. 또한 업로드하는데 html파일이나 한글파일 등을 잘못 업로드 하는 경우도 있기 때문에 이를 방지하기 위해 이미지의 크기를 지정하고 확장자를 검증하는 등의 이미지 검증 단계를 거치는 것이 좋다.

참고) 확장자 검증에는 input type="file" accept="image/jpeg,image/png" 같은 검증 방법도 있다.(이 방법은 애초에 jpeg, png파일 이외에는 선택이 되지 않는다.

참고) 용량 팁, 아스키 코드, binary

이미지 유무와 사이즈 검증

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

5메가 이하 이미지만 넣을 수 있다.

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;
    }

		~~~ // API 호출 로직 생략

  };

이미지 확장자가 png, jpeg가 아니면 업로드할 수 없다.
사진 선택 시 호출되는 함수에서 확장자를 검증한다.
If문의 모습은 early-exit패턴

<input
  style={{ display: "none" }}
  onChange={onChangeFile}
  type="file"
  ref={fileRef}
  accept="image/jpeg,image/png" // 띄어쓰기 없이 콤마(,)를 기준으로 작성합니다.
  // accept를 추가하면 지정되지 않은 확장자는 선택 자체가 불가합니다.
/>

지정하지 않은 확장자의 파일은 선택자체가 불가능

이미지 검증과정 컴포넌트 분리

// src/commons/libraries/validationFile.ts

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;
};

파일을 분리하지 않았을 때는 if문에 리턴을 넣어 함수를 종료시켰으나 파일 분리하게 되면 검증함수를 종료할 뿐 이미지 업로드 함수를 종료하지 못하므로 따로 분리했을 때는 true와 false를 리턴해 결과를 변수에 담아 해당 변수의 boolean값에 따라 리턴으로 업로드를 막거나 업로드를 진행하면 된다.

// 이미지 검증을 실행 할 컴포넌트

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

이미지 검증 컴포넌트

이런 검증은 확장자 이외에도 추가적인 다른 검증 사항이 있을 때 쓴다. 확장자 검증만 할 때는 accept 면 된다.

profile
Strive for greatness

0개의 댓글