19일차 - 이미지 프로세스, 업로드

류연찬·2022년 11월 19일
0

Codecamp FE07

목록 보기
19/39

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

uploadFile 이라는 api 가 있을때, 파일을 선택하고 uploadFile 을 요청하게 될 경우, backend 에서 storage 로 파일을 전송하게 됩니다.

우리가 이미지를 보기 위해서는 주소를 사용해 접근하게 됩니다.

즉, storage 에서는 backend 로 이미지 주소를 넘겨주게 되며 이 주소를 다시 front에게 주게 됩니다. 우리는 이 주소를 가지고 api 요청 시, image에 대한 주소를 보낼 수 있게 되는 것 이며, 이 정보들을 DataBase 에 저장하게 됩니다.

❗️ 참고로 imgFile에 접근할 수 있는 주소만 저장하며 file은 없습니다

이미지 업로드

우리가 이미지 업로드에 사용할 api는 uploadFile 입니다.

이미지 업로드는 uploadFile 로 받아온 이미지 url을 createBoard 에 넣어주시면 됩니다.

url을 받아오기 위해선 아폴로 업로드 관련 라이브러리를 설치해줘야 합니다. 아래 순서를 따라 설치와 세팅을 해주시길 바랍니다.

아폴로 업로드 라이브러리 설치와 세팅

url을 가지고 오기 위한 라이브러리로 createUploadLink 를 설치해야 합니다.

라이브러리 설치

터미널에 yarn add apollo-upload-client 입력해 설치해 주세요.

사용을 위한 세팅

app.tsx 에서 세팅

// class 폴더의 app.tsx파일
//import 부분
import {ApolloLink} from "@apollo/client"
import {createUploadLink} from "apollo-upload-client"

//세팅 함수 부분
const uplodLink = createUploadLink({
  uri: "백엔드 주소"
})
const client = new ApolloClient({
  link: ApolloLink.from([uplodLink as inknown as ApolloLink]),
  cache: new inMemoryCache(),
})

아폴로 업로드는 타입스크립트 또한 지원해주기 때문에 타입을 설치해 주시기 바랍니다.
yarn add @types/apollo-upload-client --dev 를 설치해줍니다.

파일 업로드할 수 있도록 화면 그려주기

파일을 선택할 수 있도록 input 태그의 type에 file로 지정해 그려주겠습니다.

index.tsx

// 이미지 업로드 api 사용을 위한 쿼리 작성
const UPLOAD_FILE = gql`
  mutation uploadFile($file: Upload!) {
    uploadFile(file: $file){
    	url
    }
  }
`

const ImageUPloadPage = () => {
  const [uploadFile] = useMutation(UPLOAD_FILE)

  // 이미지 업로드 함수
  const onchangeFile = async(e: ChangeEvent<HTMLInputElement>) => {
    //files는 있을수도 있고,없을수도 있기 때문에 옵셔널 체이닝을 사용해주셔야 합니다.
    const Imagefile = e.target.files?.[0]
    
    try {
      // 우리가 선택한 사진을 Imagefile라는 변수에 담았으니 해당 변수를 variables에 넣어서 보내주면 됩니다.
      await uploadFile({ variables: { file: Imagefile} })
      console.log(result.data?.uploadFile.url)
	} catch(error){
        alert(error.message)
    }
  }
    
  return <input type="file" onChange={onChangeFile}/>
}

이미지를 보내주고, url을 받아오는 과정 을 자세히 보도록 하겠습니다.

  1. onChange 를 통해 이미지 파일을 가지고와 Imagefile 에 넣어줍니다.
  2. Imagefilevariables 에 넣어 uploadFile mutation을 날려줍니다.
  3. 이미지가 정해둔 스토리지에 들어가고, 해당 스토리지에서 url을 반환받아 가지고 옵니다.
  4. 콘솔에 받아온 이미지 URL이 찍힙니다.

❗️ 여기서 학습한 것은 이미지 업로드하는 과정이나 등록한 것이 아닙니다!

이미지 검증

이미지를 업로드할때 필요 이상으로 큰 사이즈의 이미지나, 초고화질 이미지를 보내게 되면 저장공간을 많이 차지하기 때문에 비용측면에 있어서 부담이 될 수 있습니다.

또한 이미지를 업로드하는데 html파일이나 한글파일을 잘못 업로드 하는 경우도 있습니다.

따라서 위와 같은 일들을 방지하기 위해 이미지를 업로드 할 때 이미지의 크기를 지정하고, 확장자를 검증하는 등 이미지 검증 단계를 거쳐 업로드 하는 것이 좋습니다.

이미지 유무와 사이즈 검증하기

이미지를 5MB 이하의 사이즈만 넣을 수 있도록 해보겠습니다.

// 이미지 사이즈 검증
const UPLOAD_FILE = gql`
  mutation uploadFile($file: Upload!) {
  	uploadFile(file: $file) {
  		url
  }
}
`

const ImageUPloadPage = () => {
  const onchangeFile = async(e: ChangeEvent<HTMLInputElement>) => {
    const [uploadFile] = useMutation(UPLOAD_FILE)

    // files는 있을수도 있고,없을수도 있기 때문에 옵셔널 체이닝을 사용해주셔야 합니다
    const Imagefile = e.target.files?.[0]

    // 이미지 파일의 사이즈가 없으면 경고띄워주기
    if(!file?.size) {
		alert("파일이 존재하지 않습니다.")
		return 
  	}

	// 이미지 파일의 사이즈가 있지만, 5MB보다 클경우 경고를 띄우고 함수를 종료합니다.
  	if(file?.size > 5 * 1024 * 1024) {
    	alert("파일 용량이 너무 큽니다.(제한: 5MB)")
    	return 
	}

	try {
  		await uploadFile({ variables : { file: Imagefil } })
  		console.log(result.data?.uploadFile.url)
	} catch(error) {
    	alert(error.message)
 	}
  }

  return <input type="file" onChange={onChangeFile}/>
}

이미지 확장자 검증하기

이미지 확장자가 png, jpeg가 아니면 업로드할 수 없도록 하겠습니다.

// 이미지 사이즈 검증
const UPLOAD_FILE = gql`
  mutation uploadFile($file: Upload!) {
    uploadFile(file: $file) {
      url
    }
  }
`

const ImageUPloadPage = () => {
  const onchangeFile = async(e:ChangeEvent<HTMLInputElement>) => {
    const [uploadFile] = useMutation(UPLOAD_FILE)

    // files는 있을수도 있고,없을수도 있기 때문에 옵셔널 체이닝을 사용해주셔야 합니다.
    const Imagefile = e.target.files?.[0]

    // 이미지 파일의 사이즈가 없으면 경고띄워주기
    if(!file?.size) {
		alert("파일이 존재하지 않습니다.")
		return 
  	}

  	// 이미지 파일의 사이즈가 있지만, 5MB보다 클경우 경고를 띄우고 함수를 종료합니다.
  	if(file?.size > 5 * 1024 * 1024) {
    	alert("파일 용량이 너무 큽니다.(제한: 5MB)")
    	return 
	}

	// 이미지 파일의 확장자 검증
	if(!file.type.includes("png") && !file.type.includes("jpeg")) {
  		alert("jpeg 파일 또는 png 파일만 업로드 가능합니다.")
	} 

	try {
 		await uploadFile({ variables : { file: Imagefil } })
  		console.log(result.data?.uploadFile.url)
	} catch(error) {
    	alert(error.message)
  	}
  }

  return <input type="file" onChange={onChangeFile}/>
}

🔔 사이즈보는 방법
MB(메가바이트) KB(키로바이트) B(바이트)
1024B = 1KB
1024KB = 1MB

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

src/commons/utils.ts

export const checkFileValidation = (file?: File) => {
  	// 이미지 파일의 사이즈가 없으면 경고띄워주기
  	if(!file?.size) {
		alert("파일이 존재하지 않습니다.")
    	return false;
	}

	// 이미지 파일의 사이즈가 있지만, 5MB보다 클경우 경고를 띄우고 함수를 종료합니다.
	if(file?.size > 5 * 1024 * 1024){
		alert("파일 용량이 너무 큽니다.(제한: 5MB)")
		return false; 
	}

    // 이미지 파일의 확장자 검증
    if(!file.type.includes("png") && !file.type.includes("jpeg")){
		alert("jpeg 파일 또는 png 파일만 업로드 가능합니다.")
		return false;
    } 

	return true
}

파일을 분리하지 않았을 때는 if문에 return 을 넣어주어 함수를 종료 시켜주었지만, 파일을 분리하게 되면 검증함수를 종료할 뿐 이미지 업로드 함수를 종료하지 못합니다.

따라서 검증함수를 따로 분리했을 때는 true와 false를 return 하여 결과를 변수에 담아 해당 변수의 Boolean값에 따라 return으로 업로드를 막거나 업로드를 진행 해주시면 됩니다.

// 이미지 검증을 실행 할 컴포넌트
import {checkFileValidation} from '파일경로'


const UPLOAD_FILE = gql`
  mutation uploadFile($file: Upload!){
  	uploadFile(file: $file){
  		url
  	}
  }
`

const ImageUPloadPage = () => {
  const onchangeFile = async(e:ChangeEvent<HTMLInputElement>) => {
    const [uploadFile] = useMutation(UPLOAD_FILE)

    //files는 있을수도 있고,없을수도 있기 때문에 옵셔널 체이닝을 사용해주셔야 합니다.
    const Imagefile = e.target.files?.[0]

    //이미지 검증 함수를 끌어오겠습니다.
    const isValid = checkFileValidation(Imagefile)
    //이미지 검증의 결과값에 따라 업로드를 진행하거나 함수를 종료합니다.
    if(!isValid) return 

    try {
		await uploadFile({ variables : { file: Imagefil } })
		console.log(result.data?.uploadFile.url)
	} catch(error) {
        alert(error.message)
    }
  }

  return <input type="file" onChange={onChangeFile}/>
}

HTML 기본 FILE 태그 숨기기

위의 이미지는 input 태그입니다. 기본은 예쁘지 않으니까 좀 더 예쁘게 꾸며주는 방법을 알아보겠습니다.

Label 태그와 htmlFor 사용하기

Label 태그에는 htmlFor 이라는 속성이 있습니다. 이 속성에 값을 넣으면 값과 똑같은 id를 찾아 그 태그의 기능과 연결해줍니다.

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

노란색 부분이 label 태그 입니다. htmlFor="fileTag"input의 id="fileTag" 의 값이 똑같습니다.

그럼 어떤 결과가 있는지 한 번 보겠습니다.

위의 결과를 보면 label 태그를 클릭해도 똑같이 파일을 올릴 수 있습니다.
그러면 label 태그를 우리가 원하는 디자인으로 꾸미고, 기존 input 태그는 안 보이도록 CSS를 작업하면 됩니다.

useRef 이용하기

우리가 HTML 태그를 선택할 때는 보통 getElementId 를 사용했습니다.
react에서는 HTML 태그에 접근을 도와주는 역할을 useRef 가 하고 있습니다.

우선 간단하게 사용 방법을 보도록 하겠습니다. 위의 예제에서 htmlFor 부분을 없애고 useRef 로 똑같이 한 번 만들어 보겠습니다.

import { useRef } from 'react';

export default function Web() {
	const inputEl = useRef();
}

우선 바뀐 부분을 먼저 보기 위해 이미지 미리보기 코드는 잠시 지웠습니다.

보시면 제일 위에서 import로 useRef 를 가져오고 const inputEl = useReft(); 를 작성했습니다.
이게 가장 처음 Ref를 불러오고 사용하는 기본 설정입니다. useStateuseEffect 처럼 react에서 가져와야 사용할 수 있습니다.
이렇게 하고 inputEl 을 태그에 넣어주면 그 태그는 inputEl 로 대신 사용할 수 있습니다.

태그에 넣어주는 것도 한 번 코드로 보곘습니다.

<input
  ref={inputEl}
  id="fileTag"
  type="file"
  onChange={readImage}
/>

이렇게 input 태그에 ref={inputEl} 을 작성하면 이제 input 태그를 inputEl 을 이용해 사용할 수 있습니다.
그리고 나서, useRef 에 있는 기능을 이용해주면 됩니다.
useRef 에는 다양한 기능이 있지만, 지금은 하나만 보도록하겠습니다.

const handleFileBtn = () => {
  inputEl.current.click();
};

함수를 하나 만들어주었습니다.
onClick 에 넣을 함수인데, Ref에는 current 안에 click 이라는 기능이 있습니다.
이름 그대로 currentinputEl 에 들어온 태그를 뜻하고, 그 태그를 click 하겠다는 기능입니다.

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

그리고 새로운 버튼을 하나 만들어서 해당 기능을 넣어주었습니다.

이렇게 하면 button 을 클릭했을 때 inputEl.current.click(); 이 실행될 것이고, 그것은 우리가 useRef 에 넣어두었던 input 태그를 클릭한 것과 같은 결과가 나올 것입니다.

보시면 이미지등록버튼 을 눌렀을 때 input 태그를 클릭한 것과 같은 결과가 발생합니다.

여기까지 하면 useRefinput 태그의 기능을 대신 하는 것은 끝났습니다.

조금 더 응용을 해볼수도 있습니다.

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

위의 코드를 보시면 미리보기 이미지에도 handleFileBtn 을 넣어주었습니다. handleFileBtn 이 어떤 기능인지는 위에서 설명을 했습니다.

그리고 input 태그에는 hidden 기능을 이용해 태그를 숨겨주었습니다.

이러면 우리는 미리보기 이미지를 클릭할 때, 이미지 등록 버튼을 클릭할 때 모두 <input type="file"/> 태그를 클릭한 것과 동일한 결과를 볼 수 있습니다.

아래는 전체코드입니다.

import { useRef } from 'react';

export default function Web() {
  const inputEl = useRef();

  const readImage = (input) => {
    // input 태그에 파일이 있는 경우
    if (input.target.files && input.target.files[0]) {
      // FileReader 인스턴스 생성
      const reader = new FileReader();
      // reader가 이미지 읽도록 하기
      reader.readAsDataURL(input.target.files[0]);
      // 이미지가 로드가 된 경우
      reader.onload = (e) => {
        const previewImage = document.getElementById('image');
        previewImage.src = e.target.result;
      }
    }
  };

  const handleFileBtn = () => {
    inputEl.current.click();
  };

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

0개의 댓글