[next.js] 이미지 업로드 & 미리보기

적자생존·2023년 2월 2일
0

next.js

목록 보기
3/6

1. 구현하고자 하는 내용

가. 이미지 파일을 올려서 프로필 사진을 변경할 것이다.
나. 이미지 용량이 매우 크기 때문에 압축을 해야 할 것이다.
다. 이미지가 업로드 되면 미리보기가 나와야 한다.

2. 라이브러리 설치

이미지 용량을 줄이기 위해서 라이브러리를 설치해야 한다.

browser-image-compression

설치 링크
라이브러리를 설치할 것이다.

터미널을 열고

npm i browser-image-compression를 이용해서 설치해준다.

3. 이미지 업로드 컴포넌트 생성

공통으로 사용할 기능이기 때문에 컴포넌트를 생성해주고 기본적인 CSS를 만들어 준다.

styled-components를 이용해서 만들어줄 것이다.

// imageUpload.tsx
import styled from "styled-components";


const ImageBox = styled.div`
  width: 12rem;
  height: 12rem;
  position: relative;
  border-radius: 50%;
  margin-bottom: 4rem;
`;
const DefaultImage = styled(Image)`
  cursor: pointer;
  border-radius: 50%;
`;


const ImageUpload = () => {
const filePickerRef = useRef()
  return (
  <div>
      {previewFile ? (
        <ImageBox>
          <DefaultImage
            src={previewFile}
            alt="프로필"
            onClick={pickImageHandler}
            fill={true}
          />
        </ImageBox>
      ) : (
        <ImageBox>
          <DefaultImage
            src={DefaultProfile}
            alt="기본"
            onClick={pickImageHandler}
            fill={true}
          />
        </ImageBox>
      )}
      <input
        ref={filePickerRef}
        type="file"
        accept=".jpg,.png,.jpeg"
        onChange={pickedHandler}
        style={{ display: "none" }}
      />
    </div>
  )
  
}

return 부분에 previewFile은 이후 업로드 된 이미지를 미리보기를 만들고 미리보기가 있으면 미리보기를 띄우고 없으면 기본 이미지를 띄우기 위해 만들어 뒀다.

input을 이용해서 file을 업로드 하지만 매우 이쁘지 않기 때문에 ref를 이용해서 input을 클릭하지 않아도 업로드 기능을 할 수 있게 구현한다.

이제 이미지 업로드를 위한 사전준비가 끝났으니 기능을 구현해 본다.

우선 api로 보낼 데이터와 미리보기 위한 데이터 state를 만들어 준다.

// api로 보낼 데이터
const [file, setFile] = useState()
// 미리보기 데이터
const [previewFile, setPreviewFile] = useState()

이미지 압축을 위한 함수를 구현해준다.

이때 처음에 설치한 라이브러리를 사용해 준다.

import imageCompression from "browser-image-compression";


const pickedHandler = async (event) => {
  // 타입스크립트를 위한 조건식
 if(!event.currentTarget.files){
 return
 }
  // 업로드된 파일 선택
  const imageFile = event.currentTarget.files[0];
  // file 형식의 데이터
  console.log(imageFile);
  // 이미지 압축 옵션들
  const options = {
  	  maxSizeMB: 0.5, // 용량 제한
      maxWidthOrHeight: 120, // 가로세로 크기 제한
      useWebWorker: true, // 멀티쓰레드 웹 워커 사용하고 메인 사용(뭔지 모름)
  }
  
  try{

    // blob 데이터로 변환 및 압축
      const compressedFile = await imageCompression(imageFile, options);

      // blob에서 file로 변환
      const convert = new File([compressedFile], imageFile.name, {
        type: `${imageFile.type}`,
      });
      // api로 요청할 데이터 file 형식
      console.log(convert, "convert");
      // api로 보낼 데이터
      setFile(convert);

      // preview 만들어줄 데이터, web api new FileReader 이용
      const reader = new FileReader();
      // file 데이터를 url로 변환
      reader.readAsDataURL(convert);
      // 변환이 완료되면 setPreviewFile에 결과 입력
      reader.onloadend = () => setPreviewFile(reader.result);
  } catch(error){console.log(error)}
}

다음과 같이 구현하였다.

보이지 않는 input클릭하기

const pickImageHandler = () => {
	if(!filePickerRef.current.click()){return}
  filePickerRef.current.click()
}

바인드는 처음 css를 구현했던 곳에서 참고해서 하면 된다.

4. 전체 코드 with typescript

잘 몰라서 any가 대부분

import { ChangeEvent, useRef, useState } from "react";
import DefaultProfile from "../../../../public/icon/profileForm.png";
import styled from "styled-components";
import Image from "next/image";
import imageCompression from "browser-image-compression";

interface IProps {
  profileImg?: any;
}

const ImageBox = styled.div`
  width: 12rem;
  height: 12rem;
  position: relative;
  border-radius: 50%;
  margin-bottom: 4rem;
`;
const DefaultImage = styled(Image)`
  cursor: pointer;
  border-radius: 50%;
`;
const ImageUpload = ({ profileImg }: IProps) => {
  const filePickerRef = useRef<any>();
  // api요청 데이터
  const [file, setFile] = useState<any>(profileImg || null);
  // 미리보기 데이터
  const [previewFile, setPreviewFile] = useState<any>(profileImg || null);

  const pickedHandler = async (event: ChangeEvent<HTMLInputElement>) => {
    if (!event.currentTarget.files) {
      return;
    }

    const imageFile = event.currentTarget.files[0];
    console.log(imageFile);
    const options = {
      maxSizeMB: 0.5,
      maxWidthOrHeight: 120,
      useWebWorker: true,
    };
    try {
      // blob 데이터로 변환 및 압축
      const compressedFile = await imageCompression(imageFile, options);

      // blob에서 file로 변환
      const convert = new File([compressedFile], imageFile.name, {
        type: `${imageFile.type}`,
      });
      // api로 요청할 데이터
      console.log(convert, "convert");
      setFile(convert);

      // preview 만들어줄 데이터
      const reader = new FileReader();
      reader.readAsDataURL(convert);
      reader.onloadend = () => setPreviewFile(reader.result);
    } catch (error) {
      console.log(error);
    }
  };
  //   console.log(previewFile);

  const pickImageHandler = () => {
    if (!filePickerRef.current.click()) {
      return;
    }
    filePickerRef.current.click();
  };

  return (
    <div>
      {previewFile ? (
        <ImageBox>
          <DefaultImage
            src={previewFile}
            alt="프로필"
            onClick={pickImageHandler}
            fill={true}
          />
        </ImageBox>
      ) : (
        <ImageBox>
          <DefaultImage
            src={DefaultProfile}
            alt="기본"
            onClick={pickImageHandler}
            fill={true}
          />
        </ImageBox>
      )}
      <input
        ref={filePickerRef}
        type="file"
        accept=".jpg,.png,.jpeg"
        onChange={pickedHandler}
        style={{ display: "none" }}
      />
    </div>
  );
};

export default ImageUpload;


profile
적는 자만이 생존한다.

0개의 댓글