본문 바로가기
Nodejs

[NodeJs] multer를 활용한 이미지 업로드

by 동복이 2023. 9. 6.

multer 란?

Nodejs와 Express의 인기가 높아짐에 따라 파일 업로드와 관련된 복잡한 작업을 쉽게 처리하기 위한 라이브러리나 미들웨어의 필요성이 증가했습니다.

multipart/form-data는 파일 업로드에 사용되는 복잡한 인코딩 유형 중 하나로 이를 처리하기 위해서는 별도의 라이브러리나 미들웨어가 필요한데 이러한 필요성 때문에 multer가 등장하게 됬습니다.

 

주요 기능

스토리지 엔진 : multer는 다양한 스토리지 엔진을 제공하여 파일을 어떻게 저장할지 정의할 수 있으며 일반적인 두 가지 스토리지 엔진은 DiskStorage, MemoryStorage가 있습니다. DiskStorage는 파일을 서버 디스크에 저장하며 MemoryStorage는 파일을 메모리에 저자한 후 다른 곳에 전송하기 위해 사용됩니다.

파일 필터링 : 업로드할 파일의 유형이나 크기 등을 기반으로 파일을 필터링 할 수 있습니다.

파일 이름 및 저장 경로 설정 : DiskStorage 엔진을 사용할 때 업로드된 파일의 이름과 저장 경로를 지정할 수 있습니다.

다중 파일 업로드 : multer는 한 번의 요청으로 여러 파일을 동시에 업로드하는 것을 지원합니다.

파일 크기 제한 : 업로드될 파일의 최대 크기를 설정할 수 있습니다. 이를 통해 큰 파일의 업로드를 제한 할 수 있습니다.

데이터 파싱 : multipart/form-data 요청의 데이터를 파싱하며 파일 외의 필드 데이터도 req.body에서 사용할 수 있게 해줍니다.

 

설치 방법

npm install multer

 

기본 사용 예제

const express = require('express')
const multer  = require('multer')
const upload = multer({ dest: 'uploads/' })

const app = express()

app.post('/profile', upload.single('avatar'), function (req, res, next) {
  // req.file 은 `avatar` 라는 필드의 파일 정보입니다.
  // 텍스트 필드가 있는 경우, req.body가 이를 포함할 것입니다.
})

app.post('/photos/upload', upload.array('photos', 12), function (req, res, next) {
  // req.files 는 `photos` 라는 파일정보를 배열로 가지고 있습니다.
  // 텍스트 필드가 있는 경우, req.body가 이를 포함할 것입니다.
})

const cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }])
app.post('/cool-profile', cpUpload, function (req, res, next) {
  // req.files는 (String -> Array) 형태의 객체 입니다.
  // 필드명은 객체의 key에, 파일 정보는 배열로 value에 저장됩니다.
  //
  // e.g.
  //  req.files['avatar'][0] -> File
  //  req.files['gallery'] -> Array
  //
  // 텍스트 필드가 있는 경우, req.body가 이를 포함할 것입니다.
})

 

multer를 활용한 이미지 업로드

기능들만 구현해놓은 웹페이지에 multer를 이용한 이미지 업로드 작업 진행

 

프로젝트 구조

 

사용한 모듈

npm init -y
npm install bcrypt cors dotenv express express-session jsonwebtoken multer mysql2 nodemon sequelize

 

Backend

config/index.js

데이터베이스 연결 설정

const config = {
  dev: {
    database: process.env.DATABASE_NAME,
    username: process.env.DATABASE_USERNAME,
    password: process.env.DATABASE_PASSWORD,
    host: process.env.DATABASE_HOST,
    dialect: "mysql",
  },
};

module.exports = config;

 

controllers/loginController.js

로그인 관련 유효성 검사 및 토큰 발급

const { User } = require("../models");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");

exports.Login = async (req, res) => {
  const { user_id, user_pw } = req.body;
  try {
    if (!user_id) {
      console.log("아이디를 입력하세요.");
      return;
    }
    if (!user_pw) {
      console.log("비밀번호를 입력하세요.");
      return;
    }

    const data = await User.findOne({ where: { user_id } });

    if (!data?.user_id) {
      console.log("없는 아이디입니다.");
      return;
    }
    const compare = bcrypt.compareSync(user_pw, data.user_pw);
    if (compare) {
      console.log("login success");
      const token = jwt.sign(
        {
          name: user_id,
        },
        process.env.ACCESS_TOKEN_KEY,
        {
          expiresIn: "5m",
        }
      );
      req.session.accessToken = token;
      res.redirect("http://127.0.0.1:5500/frontEnd/index.html");
    } else {
      console.log("비밀번호가 틀립니다.");
    }
  } catch (error) {
    console.error(error);
  }
};

 

controllers/mypageController.js

프로필 이미지 미리보기, 프로필 이미지 파일 업데이트 기능

const { User } = require("../models");
const multer = require("multer");
const path = require("path");

exports.Mypage = async (req, res) => {
  const { decoded } = req;
  try {
    const data = await User.findOne({ where: { user_id: decoded.name } });
    // console.log(data.dataValues);
    res.redirect("http://127.0.0.1:5500/frontEnd/mypage.html");
  } catch (error) {
    console.error(error);
  }
};

exports.userInfo = async (req, res) => {
  const { decoded } = req;
  try {
    const { id, user_id, img } = await User.findOne({
      where: { user_id: decoded.name },
    });
    const userInfo = {
      id,
      user_id,
      img,
    };
    res.json(userInfo);
  } catch (error) {
    console.error(error);
  }
};

exports.UpdateImg = multer({
  storage: multer.diskStorage({
    destination: (req, file, done) => {
      console.log("----------file -----------");
      console.log(file);
      console.log("----------file -----------");
      done(null, "uploads/");
    },
    filename: (req, file, done) => {
      const ext = path.extname(file.originalname);
      console.log("ext");
      console.log(ext);
      const filename =
        path.basename(file.originalname, ext) + "_" + Date.now() + ext;
      console.log("filename");
      console.log(filename);
      done(null, filename);
    },
  }),
  limits: { fileSize: 5 * 1024 * 1024 },
});

exports.UpdateUserImg = async (req, res) => {
  const { decoded } = req;
  const { file } = req;
  try {
    // console.log(file.filename);
    const img = "/img/" + file.filename;
    await User.update({ img }, { where: { user_id: decoded.name } });
  } catch (error) {
    console.error(error);
  }
};

 

controllers/signupController.js

회원가입 관련 기능

const { User } = require("../models");
const bcrypt = require("bcrypt");

exports.SignUp = async (req, res) => {
  const { user_id, user_pw } = req.body;
  const img = "/img/redposion.png";
  try {
    const data = await User.findOne({ where: { user_id } });

    if (!data?.user_id) {
      const hash = bcrypt.hashSync(user_pw, 10);

      await User.create({
        user_id,
        user_pw: hash,
        img,
      });

      res.redirect("http://127.0.0.1:5500/login.html");
    } else {
      console.log("이미 가입한 아이디입니다.");
    }
  } catch (error) {
    console.error(error);
  }
};

 

middleware/imgUpload.js

multer 기능

const multer = require("multer");
const path = require("path");

// mutler 함수 안에 매개변수로 객체 형태 인자 전달
// storage 속성을 통해서 업로드된 파일을 어디에 저장시킬지 지정
exports.Upload = multer({
  // diskStorage : 서버 컴퓨터 하드디스크에 파일을 업로드
  // 객체로 인자값을 전달
  storage: multer.diskStorage({
    // destination 속성 : 파일이 저장될 폴더를 설정
    destination: (req, file, done) => {
      // 콜백 함수 done : 두번쨰 인자값으로 폴더의 이름을 설정
      // 서버 컴퓨터 폴더명
      // 첫번째 매개변수 : 에러처리 부분
      // 두번째 매개변수 : 파일이 저장될 폴더 이름
      done(null, "uploads/");
    },
    // filename 속성 : 매개변수 file.originalname 은 클라이언트가 업로드한 파일의 이름을 나타냄
    // file.originalname : 사용자가 업로드한 파일 원본명
    filename: (req, file, done) => {
      // extname 메소드 : 파일의 경로를 매개변수로 받고 파일의 확장자를 추출
      const ext = path.extname(file.originalname);
      console.log("ext");
      console.log(ext);

      // 파일을 저장하는데 이름이 같으면 안되기 때문에 파일이름 뒤에 날짜를 붙여줌
      // basename 메소드 : 확장자를 추가, 제거 할 수 있음
      const filename =
        path.basename(file.originalname, ext) + "_" + Date.now() + ext;
      // done 첫번째 매개변수 : 에러 처리 부분
      // 두번째 매개변수 : 저장할 파일명
      done(null, filename);
    },
  }),
  // 파일의 최대 사이즈 설정
  // 5MB 설정
  limits: { fileSize: 5 * 1024 * 1024 },
});

 

middleware/loginChk.js

로그인 상태 체크

const { User } = require("../models");
const jwt = require("jsonwebtoken");

exports.isLogin = async (req, res, next) => {
  const { accessToken } = req.session;
  try {
    await jwt.verify(
      accessToken,
      process.env.ACCESS_TOKEN_KEY,
      (err, decoded) => {
        if (err) {
          console.log("다시 로그인하세요");
        } else {
          req.decoded = decoded;
          next();
        }
      }
    );
  } catch (error) {
    console.error(error);
  }
};

 

models/index.js

데이터베이스 초기 설정

const config = require("../config");
const Sequelize = require("sequelize");

const seq = new Sequelize(config.dev);

const User = require("./users");

User.init(seq);

const db = {};

db.sequelize = seq;
db.User = User;

module.exports = db;

 

models/users.js

user 테이블 설정

const { DataTypes, Model } = require("sequelize");

class User extends Model {
  static init(seq) {
    return super.init(
      {
        user_id: {
          type: DataTypes.STRING(20),
          allowNull: false,
        },
        user_pw: {
          type: DataTypes.STRING(64),
          allowNull: false,
        },
        img: {
          type: DataTypes.STRING(100),
        },
      },
      {
        sequelize: seq,
        underscored: false,
        modelName: "User",
        tableName: "upload_users",
        charset: "utf8",
        collate: "utf8_general_ci",
      }
    );
  }
}

module.exports = User;

 

routers/loginRouter.js

login 관련 라우팅 설정

const router = require("express").Router();
const { Login } = require("../controllers/loginController");

router.post("/", Login);

module.exports = router;

 

routers/mypageRouter.js

mypage 관련 라우팅 설정

const router = require("express").Router();
const { isLogin } = require("../middleware/loginChk");
const {
  Mypage,
  userInfo,
  UpdateImg,
  UpdateUserImg,
} = require("../controllers/mypageController");
router.get("/", isLogin, Mypage);

router.get("/userinfo", isLogin, userInfo);

router.post(
  "/updateimg",
  isLogin,
  UpdateImg.single("updateimg"),
  UpdateUserImg
);

module.exports = router;

 

routers/signupRouter.js

회원가입 관련 라우팅 설정

const router = require("express").Router();
const { SignUp } = require("../controllers/signupController");

router.post("/", SignUp);

module.exports = router;

 

routers/uploadRouter.js

파일 업로드 관련 라우팅 설정

 

const router = require("express").Router();
const { Upload } = require("../middleware/imgUpload");

// Upload.single 매개변수로 form에서 이미지 파일을 가지고 있는 input의 name을 작성
router.post("/", Upload.single("upload"), (req, res) => {
  const { file, body } = req;
  console.log(file, body);
  res.send("save file");
});

module.exports = router;

 

app.js

// 이미지 업로드
// 이미지 파일을 서버측 컴퓨터에 폴더 저장
// 파일의 경로를 설정하고 서버측에서 이미지 파일을 가져와서 보여줌

// 사용할 모듈
// express path multer
// multer : 모듈을 사용해서 이미지 업로드. 파일이 저장될 경로나 파일의 확장자 이름들을 설정해서 파일을 저장

const express = require("express");
const session = require("express-session");
const path = require("path");
const multer = require("multer");
const cors = require("cors");
const dot = require("dotenv").config();
const { sequelize } = require("./models");

const uploadRouter = require("./routers/uploadRouter");
const signupRotuer = require("./routers/signupRouter");
const loginRotuer = require("./routers/loginRouter");
const mypageRouter = require("./routers/mypageRouter");

const app = express();

app.use(
  cors({
    origin: "http://127.0.0.1:5500",
    credentials: true,
  })
);

sequelize
  .sync({
    force: false,
  })
  .then((e) => {
    console.log("database connect");
  })
  .catch((err) => {
    console.error(err);
  });

app.use(
  session({
    secret: process.env.SESSION_KEY,
    resave: false,
    saveUninitialized: false,
  })
);

app.use(express.urlencoded({ extended: false }));

app.use("/img", express.static(path.join(__dirname, "uploads")));
// json 형식의 데이터를 전달 받았을때 json 파싱을 해서 자바스크립트 객체로 변환 시켜주는 미들웨어
app.use(express.json());

app.use("/upload", uploadRouter);
app.use("/signup", signupRotuer);
app.use("/login", loginRotuer);
app.use("/mypage", mypageRouter);

app.listen(8080, () => {
  console.log("Server On");
});

 

Frontend

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <input type="text" id="imgs" />
    <input type="file" id="file" />
    <button id="uploadBtn">img upload</button>
    <a href="http://127.0.0.1:5500/frontEnd/login.html">login</a>
    <a href="http://127.0.0.1:5500/frontEnd/signup.html">signup</a>
    <a href="http://127.0.0.1:8080/mypage">mypage</a>
  </body>
  <script>
    uploadBtn.onclick = () => {
      // new Formdata() : formData를 동적으로 생성
      const form = new FormData();
      // html 상에서 name으로 키값을 전달하던 부분을 append 메소드로 설정
      // 첫번째 매개변수 : 키
      // 두번째 매개변수 : 값
      form.append("imgs", imgs.value);
      // file은 버퍼 데이터로 들어 오기 때문에 files로 생성됨
      form.append("upload", file.files[0]);

      // 파일을 보낼때 파일의 데이터를 폼데이터로 보낸다고 설정
      // 헤더의 내용으로 인코딩된 폼 데이터로 전송한다고 설정
      axios
        .post("http://127.0.0.1:8080/upload", form, {
          "Content-Type": "multipart/form-data",
        })
        .then((e) => {
          console.log("upload success");
          console.log(e.data);
        })
        .catch((err) => {
          console.error(err);
        });
    };
  </script>
</html>

 

login.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h2>login page</h2>
    <form action="http://127.0.0.1:8080/login" method="post">
      <label for="">id</label>
      <input type="text" name="user_id" />
      <label for="">pw</label>
      <input type="text" name="user_pw" />
      <button>login</button>
    </form>
  </body>
</html>

 

mypage.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <style>
      .profile img {
        width: 100px;
        height: 100px;
        border: 1px solid;
      }
    </style>
  </head>
  <body>
    <h2>mypage</h2>
    <div class="profile">
      <img id="profileImg" />
    </div>

    <div>
      <span>user_id : </span>
      <span id="userId">user</span>
    </div>

    <label for="">프로필 이미지 변경</label>
    <input type="file" id="file" />
    <button id="changeImg">img change</button>
  </body>
  <script>
    // 유저 프로필 사진 업로드
    changeImg.onclick = async () => {
      const form = new FormData();
      form.append("updateimg", file.files[0]);

      axios
        .post(
          "http://127.0.0.1:8080/mypage/updateimg",
          form,
          { withCredentials: true },
          {
            "Content-Type": "multipart/form-data",
          }
        )
        .then((e) => {
          console.log("유저 프로필 이미지 수정 완료");
          getUserInfo();
        })
        .catch((err) => {
          console.error(err);
        });
    };
    // 유저의 정보를 가져와 mypage에 데이터 입력
    async function getUserInfo() {
      try {
        const { data } = await axios.get(
          "http://127.0.0.1:8080/mypage/userinfo",
          {
            withCredentials: true,
          }
        );
        const img = "http://127.0.0.1:8080" + `${data.img}`;
        profileImg.setAttribute("src", img);
        userId.innerHTML = data.user_id;
        console.log(data);
      } catch (error) {
        console.error(error);
      }
    }
    getUserInfo();
  </script>
</html>

 

signup.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h2>signup page</h2>
    <form action="http://127.0.0.1:8080/signup" method="post">
      <label for="">id</label>
      <input type="text" name="user_id" />
      <label for="">pw</label>
      <input type="text" name="user_pw" />
      <button>signup</button>
    </form>
  </body>
</html>

 

실습 순서

1. 회원가입 페이지에서 회원가입 진행

2. 로그인 후 메인페이지에서 이미지 업로드

3. backend uploads 폴더에 이미지 업로드

4. mypage에서 파일 선택 후 img change 버튼 클릭

5. 이미지 파일이 uploads 폴더에 업로드 되고 화면에 보이는 프로필 이미지 변경 확인

728x90