Minwook’s portfolio

로컬서버 file upload구현 본문

Today I Learned

로컬서버 file upload구현

yiminwook 2023. 3. 10. 00:14

 

사용스택

next.JS, typeScript, sass, axios, formidable

추가. multer, next-connect

 

 

1. 프론트에서 file input으로 data 받기

이미지 구현방식은 useRef로 DOM에 접근해서 이미지파일을 가져오고

onChange를 걸어서 이미지 미리보기가 자동으로 바뀌게 구현할 것 이다.

 

1) useRef로 DOM에 접근하여 file 데이터 받아오기

const imgRef = useRef<HTMLInputElement>(null);

const imgReset = () => {
  if (imgRef.current) {
    imgRef.current.value = "";
  }
};

return (
 ...
  <form className={img_upload.form}>
    <label>file</label>
    <input
     type="file"
      name="cardImg"
      id="card-img--input"
      ref={imgRef}>
    </input>
    <button
      type="button" //버튼을 누를시 onSubmit()이 실행되지 않도록 button으로 지정
      className={img_upload.form_reset}
      onClick={imgReset}
    >
      삭제하기
    </button>
  </form>

  ...
)

삭제하기를 누를시에 e.current.value를 삭제시켜서 파일 이름이 뜨지 않도록 한다.

 

 

2) onChange를 이용한 미리보기 구현 

const ImgUpload = () => {
  const imgRef = useRef<HTMLInputElement>(null);
  const [imgUrl, setImgUrl] = useState<string>("");
  
  const send = () => {}

  const imgReset = () => {
    if (imgRef.current) {
      imgRef.current.value = "";
      //객체 URL 메모리 누수방지
      URL.revokeObjectURL(imgUrl);
      setImgUrl((_pre) => "");
    }
  };

  return (
    <div className={img_upload.container}>
      <form className={img_upload.form}>
        <label>file</label>
        <input
          type="file"
          name="cardImg"
          ref={imgRef}
          id="card-img--input"
          onChange={(e: React.ChangeEvent<{ files: FileList | null }>) => {
            if (e.target.files && e.target.files.length > 0) {
              //객체 URL 메모리 누수방지
              const file = e.target.files[0];
              URL.revokeObjectURL(imgUrl);
              //URL생성
              setImgUrl((_pre) => URL.createObjectURL(file));
            }
          }}
        ></input>
        <button
          type="button"
          className={img_upload.form_reset}
          onClick={imgReset}
        >
          삭제하기
        </button>
      </form>
      //imgUrl이 있을때만 미리보기와 send버튼이 활성화
      {imgUrl && (
        <>
          <div className={img_upload.img_container}>
            <Image
              className={img_upload.preView}
              src={imgUrl}
              alt="preview"
              width={200}
              height={300}
            />
          </div>
          <button onClick={send} className={img_upload.button}>
            submit
          </button>
        </>
      )}
    </div>
  );
};

 

 

 

- 추가적으로 Blob을 base64로 변환시키는 코드도 구현해보았다 -

 const [img, setImg] = useState<Blob | null>(null);
 const [imgToBase64, setImgToBase64] = useState<string>("");
 
   const imgRendering = () => {
    //window FileReader 사용
    const reader = new window.FileReader();
    if (img) {     
      reader.readAsDataURL(img);     
      reader.onloadend = () => {
        const base64 = reader.result;
        if (base64) {
          //base64를 string으로 변환하여 state 변경
          setImgToBase64((_pre) => base64.toString()); 
        }
      };
      reader.onerror = () => {
        alert("upload error!!"); //실패시
      };
    }
  };
  
  useEffect(() => {
    //useEffect cleanup 언마운트시 실행
    return imgRendering(); 
  }, [img]); //img가 바뀔때만 실행
  
  //미리보기 리셋
  const imgReset = () => {
    setImg((_pre) => null);
  };
  
  
  return (
  ...
    <form className={img_upload.form}>
      <label>file</label>
      <input
        type="file"
        name="cardImg"
        id="card-img--input"
        onChange={(e: React.ChangeEvent<{ files: FileList | null }>) => {
          if (e.target.files && e.target.files.length > 0) {
            const file = e.target.files[0];
            setImg((_pre) => file);
          }
        }}
      ></input>
      <button className={img_upload.form_reset} onClick={imgReset}>
        삭제하기
      </button>
    </form>
    //imgToBase64가 있을때만 랜더링 
    {imgToBase64 && (
      <>
        <div className={img_upload.img_container}>
          <Image
            className={img_upload.preView}
            src={imgToBase64} //base64를 직접 img src로 쓸 수 있다.
            alt="preview"
            width={200}
            height={300}
          />
        </div>
        <button onClick={send} className={img_upload.button}>
          submit
        </button>
      </>
    )}
  ...
  )

 

 

2. 프론트에서 서버로 이미지 전송하기 (axios)

...
const imgRef = useRef<HTMLInputElement>(null);
  const [imgUrl, setImgUrl] = useState<string>("");

  const send = async () => {
    if (
      imgRef.current &&
      imgRef.current.files &&
      imgRef.current.files.length > 0
    ) {
      const formData = new FormData();
      formData.append("img", imgRef.current.files[0]);
      formData.append("title", "title");
      const result: AxiosResponse<{ message: string }> = await axios.post(
        "/api/upload",
        formData,
        {
          headers: {
            "Contest-Type": "multipart/form-data",
          },
        }
      );
      console.log(result);
      //보내고 나면 리셋
      imgReset();
    }
  };
  
  const imgReset = () => {
    if (imgRef.current) {
      imgRef.current.value = "";
      URL.revokeObjectURL(imgUrl);
      setImgUrl((_pre) => "");
    }
  };
 ...

 

 

3. 서버에서 이미지 받아서 저장하기 

node.js에서는 multer를 사용했는데 next에서는 multer를 사용하기 어려웠다.

따라서 formidable를 사용하여 formData를 parse했다. 

yarn add formidable @types/formidable
import type { NextApiRequest, NextApiResponse } from "next";
import formidable from "formidable";
import path from "path";
import fs from "fs/promises"; 

export const config = {
  api: {
   //next에서는 기본으로 bodyParser가 작동되므로 false로 해준다.
    bodyParser: false,
  },
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {

  //local에 저장할 path
  const imgStoragePath = path.join(
    process.cwd() + "/src" + "/public" + "/images"
  );
  
  //fs모듈을 사용하여path에 폴더가 없을때엔 생성하도록 할 수 있다.
  try {
    await fs.readdir(imgStoragePath);
  } catch {
    await fs.mkdir(imgStoragePath);
  }
  
  //추후 s3 버켓으로 보내려고 default는 false로 하였다.
  /** true일시 로컬에 저장 */
  const readFile = (req: NextApiRequest, saveLocally: boolean = false) => {
    const options: formidable.Options = {};

    if (saveLocally) {
      //true일때 option객체에 path와 filename을 저장
      options.uploadDir = imgStoragePath;
      options.filename = (name, ext, path, form) => {
        return Date.now().toString() + "_" + path.originalFilename;
      };
    }

    return new Promise<{
      fields: formidable.Fields;
      files: formidable.Files;
    }>((resolve, rejects) => {
      const form = formidable(options);

      form.parse(req, (err, fields, files) => {
        if (err) {
          rejects(err);
        }
        console.log(fields);
        resolve({ fields, files });
      });
    });
  };
  
  const data = await readFile(req, true);
  
  //files
  console.log(data.files.img); //img Blob
  
  //fields
  console.log(data.fields.imgToBase64); //img base64
  console.log(data.fields.text); //작성한 text를 확인가능
  
  return res.status(201).json({ message: "OK" });
}

 

 

프로젝트 구조

이미지를 서버로 보내면 지정된 path에 이미지 파일이 생긴것을 볼 수 있다.

단, 텍스트는 바로 파일로 저장되지않는다.

 

 

-- 추가 multer 와 next-connect 를 사용한 middle ware구현 --

 

nextJS에서 express처럼 미들웨어를 구현하여 file parse를 하려고 공식문서를 터졌지만 

nextJS의 내장 미들웨어는 res를 NextRequest 타입으로 받는데 서버 res는 타입을 NextApiRequest로 받아서

서로 타입이 안맞았다. nextJS 내장 미들웨어가 서버 api 미들웨어로는 부적합하다고 생각되어 다른 방식을 찾던중 

 

 

next-connect라는 서드파티 미들웨어 라이브러리를 찾았다

next-connect를 쉽게 말하자면 nextJS를 express처럼 사용 할 수 있게 해준다

 

이를 적용한 코드이다.

 

client

//client axios code
 const send = async () => {
    if (img) {
      const formData = new FormData();
      formData.append("imgToBase64", imgToBase64);
      formData.append("img", img);
      formData.append("title", "title");
      const result: AxiosResponse<{ message: string }> = await axios.post(
        "/api/hello",
        formData,
        {
          headers: {
            "Contest-Type": "multipart/form-data",
          },
        }
      );

      console.log(result);
    }
  };

 

server

파일위치는 src/api/upload.ts 이다

//api => localhost:3000/api/upload
//upload.ts
import { NextApiRequest, NextApiResponse } from "next";
import nextConnect from "next-connect";
import multer from "multer";

//버퍼 형식으로 메모리에 저장
const upload = multer({ storage: multer.memoryStorage() });

const handler = nextConnect();

interface parsedNextApiRequest extends NextApiRequest {
  file?: Express.Multer.File;
  files?: Express.Multer.File[];
  //이미지 파일은 files에서 확인할 수 있고
  //나머지는 body에서 확인가능 
  body: {
    title: string;
    imgToBase64: string;
  };
}

interface answer {
  message: string;
}

//미들웨어사용
handler.use(upload.single("img"));

handler.post((req: parsedNextApiRequest, res: NextApiResponse<answer>) => {
  if (req.files) {
    //file
    console.log(req.file);
    //body
    console.log(req.body.title);
    return res.status(200).json({ message: "OK" });
  } else {
    return res.status(404).json({ message: "not found" });
  }
});

export const config = {
  api: {
    bodyParser: false, //bodyParser 비활성화
  },
};

export default handler;

next-connect를 잘 사용한다면 미들웨어를 통하여 코드 재사용이 편할 것 같다. 공식 문서를 더 읽어볼 필요성을 느꼇다.

 

 

 

 

 

next-connect

The method routing and middleware layer for Next.js (and many others). Latest version: 0.13.0, last published: 7 months ago. Start using next-connect in your project by running `npm i next-connect`. There are 35 other projects in the npm registry using nex

www.npmjs.com

 


 

 

참고

 

with-typescript - CodeSandbox

with-typescript using react-image-lightbox, @rehooks/component-size, react-textfit, formik, buffer-to-data-url, next, react-sticky-mouse-tooltip, pngjs, ethers

codesandbox.io

 

axios 사용시 폼 데이터 전송하기 (+파일 업로드) | 두글 블로그

axios 의 post 기능은 기본적으로 폼 데이터 전송방식을 사용하지 않기 때문에 서버쪽에서 파라메터를 받는 부분을 수정할 수 없는 상황이라면 문제가 됩니다. 보통 외부 API 서비스를 사용할 때 많

doogle.link

 

[JS] 📚 Base64 / Blob / ArrayBuffer / File 다루기 총정리

웹 개발을 진행하다 보면 이진 데이터를 다루어야 할 때를 간혹 마주칠 수 있다. 브라우저에선 주로 파일 생성, 업로드, 다운로드 또는 이미지 처리와 관련이 깊고, 서버 사이드인 node.js 에선 파

inpa.tistory.com

 

[JS] 📚 FormData 사용법 & 응용 총정리 (+ fetch 전송하기)

FormData API 보통 서버에 데이터를 전송하기 위해서는 HTML5 의 폼 태그를 사용해 다음과 같이 메뉴를 구성하여 제출 해본 기억들이 있을 것이다. 아이디 비밀번호 성별 남자 여자 응시분야 영어 수

inpa.tistory.com

 

image Blob 객체를 url로 바꾸어 img 띄우기, javascript, JavaScript, blob, createObjectUrl, revokeObjectUrl, react, vue,

image Blob 객체를 url로 바꾸어 img 띄우기, javascript, JavaScript, blob, createObjectUrl, revokeObjectUrl, react, vue, window, document

kyounghwan01.github.io

 

[React/JavaScript] 이미지 파일 업로드 전 미리 보는 방법

이미지 file을 서버에 업로드하기 전, 등록한 파일을 화면에서 미리 보여주고 싶을 때가 있다. URL.createObjectURL() 이용하기 부제: 이미지 미리 보기 url 생성 거두절미하고 리액트 코드부터 보시죠...

xively.tistory.com

 

Comments