[사이드 프로젝트] multer를 이용한 이미지 업로드 feat.React x Node js

June hyoung Park·2020년 9월 12일
10

React

목록 보기
7/19
post-thumbnail

요즘 코드스테이츠 부트캠프에서의 빡센 일정과 학습량 덕에 정신없는 나날들을 보내고 있지만, 하루의 남는 시간들을 최대한 활용하여, 만들었던 사이드 프로젝트의 기능들을 보완하고 개선시키는 중이다.

위 사이드 프로젝트는 React, MongoDB, NodeJS(Express)기반으로 제작중이며, jwt토큰 발급, bcrypt 암호화 기반의 로그인, 회원가입 기능을 중점으로 제작 중인 보일러 플레이트이다. 누구나 .env 파일에 Mongo Db Atlas 계정만 넣어준다면, 손쉽게 로그인, 회원가입 기능이 미리 구현되어있는 환경에서 빠르게 개발을 시작할 수 있게끔 리팩토링과 변수명, 기타 등등 여러가지를 신경쓰고 있지만, 아직 개선해야 할점이 너무 많다.

최근엔 회원가입 시 이미지 파일을 업로드 받아 유저의 고유한 프로필 이미지로 쓸 수 있게끔 파일 업로드에 대한 기능을 추가 하기위해 Node js의 미들웨어 'multer'에 대해 공부했는데, 생각보다 데이터 흐름이 복잡하기도하고, 업로드 후 미리보기 기능 구현 및 기존의 리덕스 관련 로직들 수정 등등 손볼곳이 많아서 꽤 애를 먹었다.

전체적인 데이터 흐름에 관한 다이어그램을 만들어보았다. 너무 디테일하게 서술해놓으면 폰트 사이즈가 너무 작아질까봐 대략적인 설명만 서술해 놓았다.

multer

Node js는 기본적으로 사용자가 업로드한 파일을 서버컴퓨터의 원하는 디렉토리에 저장하는 기능을 제공하고 있지는 않기 때문에, multer라는 모듈을 설치해서 이를 처리해야한다. multer는 파일 업로드를 위해 사용되는 multipart/form-data 를 다루기 위한 node.js 의 미들웨어 이며, 사용 시 주의해야할 사항은 multer는 (multipart/form-data)가 아닌 폼에서는 동작하지 않는다는 것이다.

설치

npm i multer --save

라우터 미들웨어 생성

//=================================
//   	user_uploadImage.js
//=================================

import express from "express";
const router = express.Router();
const multer = require("multer");

// multer-optional
var storage = multer.diskStorage({
 destination: (req, file, cb) => {
   cb(null, "uploads/");
 },
 filename: (req, file, cb) => {
   cb(null, `${Date.now()}_${file.originalname}`);
 },
});
var upload = multer({ storage: storage }).single("profile_img");

// Router
router.post("/", (req, res) => {
 upload(req, res, (err) => {
   if (err) {
     return res.json({ success: false, err });
   }
   return res.json({
     success: true,
     image: res.req.file.path,
     fileName: res.req.file.filename,
   });
 });
});

export default router;

먼저 클라이언트 측에서 /api/users/upload으로 post 요청을 보내면 파일 업로드 기능을 처리할 라우터 미들웨어를 등록해주었다. 또한 multer의 DiskStorage엔진을 이용해서 uploads라는 static 폴더에 파일들이 저장되도록 경로를 지정해 줬으며, 파일 이름의 중복을 막기위해Date.now() 메서드를 이용하여, 저장될 파일마다 고유한 이름을 지정해줬다.

React

회원가입 컴포넌트인 RegisterPage.js 에 파일 업로드 관련 코드들을 추가하면 하나의 컴포넌트가 너무 방대 해질거같아 FileUpload라는 자식 컴포넌트를 하나 만들어주었다. FileUpload의 역활은 유저가 파일을 업로드 시 Axios를 통해 /api/users/upload로 post 요청을 한뒤, /api/users/upload에서 반환해준 response값을 부모 컴포넌트인 RegisterPage에서 props로 내려준 fileToParents라는 함수에 인자로 넘겨서 부모 컴포넌트인 RegisterPage에게 전달한다.

// FileUpload.js

 const handleFileOnChange = (event) => {

   {...}
   
    // 서버 api에 Post 요청
    const formData = new FormData();
    formData.append('profile_img', event.target.files[0]);
    Axios.post('/api/users/upload', formData, {
      header: { 'content-type': 'multipart/form-data' },
    }).then((response) => {
      console.log({ response });
      props.fileToParents(response.data.image);
    });
  };


  return (
    <>
      <form encType="multipart/form-data" style={{ display: 'flex' }}>
        <FakeUploadBtn>{profile_preview}</FakeUploadBtn>
        <UploadButton
          type="file"
          accept="image/jpg,impge/png,image/jpeg,image/gif"
          name="profile_img"
          placeholder="업로드"
          onChange={handleFileOnChange}
        ></UploadButton>
      </form>
    </>
  );
};
{...}

multipart/form-data 타입의 폼을 만들어준뒤 onChange시 handleFileOnChange함수를 실행하여 post 요청을 하게끔 만들어주었다. 물론 submit타입의 버튼을 만들어주어서 폼의 onSubmit 이벤트 발생시 handleFileOnChange함수를 실행 시키게끔 하는것이 정석일 수 있지만, 회원가입 창의 이미지 업로드 영역에 별도의 업로드 이미지 버튼을 만들고 싶지않아서 이미지 업로드시 바로 업로드 되게끔 만들어주었다. (이부분은 나중에 수정이 좀 필요할것같다.)

이미지 업로드시 서버단에서 파일 이름과 경로를 리턴해준다 :) 그후 부모컴포넌트에서 props로 내려준 함수에 파일 경로가 담긴 response.data.image를 인자로 넣어 부모에게 전달한다.

//=================================
//       Register-Page
//=================================

{...}

const RegisterPage = (props) => {
  // State
  const [images, setImages] = useState('');
  const [email, setEmail] = useState('');
  const [name, setName] = useState('');
  const [password, setPassword] = useState('');
  const [confilmPassword, setConfilmPassword] = useState('');

  {...}

  //=================================
  // Redux-Dispatch
  const dispatch = useDispatch();
  const onSubmitHandler = (event) => {
    event.preventDefault(); // 새로고침 방지
    if (password !== confilmPassword) {
      alert('패스워드가 일치하지 않습니다.');
    } else {
      let requestBody = {
        email,
        name,
        password,
        profileImage: images,
      };
      console.log(requestBody);
      dispatch(registerUser(requestBody)).then(props.history.push('/login'));
    }
  };
  //=================================

  const updataImages = (newImages) => {
    setImages(newImages);
  };

  return (
    <LoginPageContent>
      <HeaderContainer />
      <FormContent>
        <LoginText>회원가입</LoginText>
        <EmailText>프로필 사진</EmailText>
        <FileUpload fileToParents={updataImages}></FileUpload>

  {...}

RegisterPage.js는 자식 컴포넌트에서 올려준 image path를 전달받은 후 updataImages라는 함수를 실행시켜 state에 이를 저장한다. 그후 다른 폼들의 정보들과 함께 requestBody라는 객체에 실려서 액션(리덕스)의 인자로 전달된다.

- mongoDB Atlas

- Redux

정상적으로 작동된다 :)

profile
Take me home~~~~

0개의 댓글