백앤드(Back-End)/Node.JS

[Node.js] Koa Multer 파일 업로드

RyanSin 2021. 1. 19. 00:12
반응형

- 지난 시간

안녕하세요. 지난 시간에는 Koa 웹 애플리케이션 프레임워크을 사용해서 RESTFaul API 서버를 만들어 보았습니다.

 

혹시 지난 글을 못 보신 분들은 아래 글을 통해 설치 및 구축을 하고 오시면 감사하겠습니다.

any-ting.tistory.com/18

 

[Node.js] Koa RESTFul API 서버 만들기

- 지난 시간 안녕하세요. 지난 시간에는 Node.js 설치 및 Koa 웹 애플리케이션 프레임워크를 구축하는 시간을 가졌습니다. 혹시 지난 글을 못 보신 분들은 아래 글을 통해 설치 및 구축을 하고 오시

any-ting.tistory.com

- 개요

이번 시간에는 RESTFul API에서 파일 업로드하는 부분에 대해서 알아보겠습니다.

 

Node.js에서 파일을 업로드하는 미들웨어는 다양합니다. 하지만 이 글에서는 @koa/multer라는 미들웨어를 사용해보겠습니다.

 

@koa/multer 미들웨어는 multer 미들웨어에 종속되어 사용됩니다. 그래서 같이 사용해야 됩니다.

- 설치

새로운 프로젝트를 만들어서 진행할 예정입니다. 아래 라이브러리(모듈)을 설치해주세요. :)

 

  • npm init : 프로젝트 생성(package.json 파일 생성)
  • npm install koa : Koa 웹 어플리케이션 프레임워크
  • npm install @koa/router : Koa Route
  • npm install @koa/cors : http 통신 허용
  • npm install @koa/multer : multer 기반 모듈
  • npm install multer : 파일 업로드 미들웨어

@koa/multer를 사용해야 하는 이유는 koa/multer 라이브러리가 deprecated(사용하지 않음) 되었습니다.

 

npm 사이트 정보

-

기본세팅

const Koa = require('koa');
const Router = require('@koa/router');
const cors = require('@koa/cors');

/**
 * Multer 미들웨어는 파일 업로드를 위해 사용되는 multipart/form-data에서 사용된다.
 * 다른 폼으로 데이터를 전송하면 적용이 안된다.
 * Header의 명시해서 보내주는게 좋다.
 */
const multer = require('@koa/multer');

//파일을 저장할 디렉토리 설정 (현재 위치에 uploads라는 폴더가 생성되고 하위에 파일이 생성된다.)
const upload = multer({ 
    dest: __dirname+'/uploads/', // 이미지 업로드 경로
})

app.use(cors())// Test를 하기 위해서 세팅 "실제 서버에 배포할 때는 아이피를 설정 해야된다."

app.listen(3000, () => console.log("Koa Multer Server Start"));

주석에도 설명을 남겨놨지만 Client에서 HTTP Header에 multipart/forom-data 라고 지정하지 않으면 안됩니다. (참고!)

 

3000번 포트 번호로 서버 시작

- 기능

- 단일 파일업로드

/**
 * @author Ryan
 * @description 단일 파일 업로드
 * 
 * 클라이언트에서 file이라는 Key(fieldname) 값을 통해 파일을 전송하면 ctx.request.file 안에 파일 정보를 얻을 수 있다.
 * 
 * 단일 이미지 전송이기 때문에 여러 파일을 보내게 되면 에러가 발생된다.
 * 
 */
router.post('/single/upload', upload.single('file'), (ctx, next) => {

    const { fieldname, originalname, encoding, mimetype, destination, filename, path, size } = ctx.request.file
    const { name } = ctx.request.body;

    console.log("body 데이터 : ", name);
    console.log("폼에 정의된 필드명 : ", fieldname);
    console.log("사용자가 업로드한 파일 명 : ", originalname);
    console.log("파일의 엔코딩 타입 : ", encoding);
    console.log("파일의 Mime 타입 : ", mimetype);
    console.log("파일이 저장된 폴더 : ", destination);
    console.log("destinatin에 저장된 파일 명 : ", filename);
    console.log("업로드된 파일의 전체 경로 ", path);
    console.log("파일의 바이트(byte 사이즈)", size);

    ctx.body = {ok: true, data: "Single Upload Ok"}

})

 

 

- 멀티 파일 업로드

/**
 * @author Ryan
 * @description 여러 파일 업로드
 * 
 * 클라이언트에서 file이라는 Key(fieldname) 값을 통해 파일을 전송하면 ctx.request.files 안에 파일 정보를 배열([]) 형태로 얻을 수 있다.
 * 
 * array('fieldname', maxCount) 필드 이름과 최대 파일 수를 정합니다.
 * 지정된 수 보다 더 많은 파일을 업로드하면 에러가 발생합니다.
 */
router.post('/multipart/upload', upload.array('file'), (ctx, next) => {

    const { name } = ctx.request.body;
    console.log("body 데이터 : ", name);

    //배열 형태이기 때문에 반복문을 통해 파일 정보를 알아낸다.
    ctx.request.files.map(data => {
        console.log("폼에 정의된 필드명 : ", data.fieldname);
        console.log("사용자가 업로드한 파일 명 : ", data.originalname);
        console.log("파일의 엔코딩 타입 : ", data.encoding);
        console.log("파일의 Mime 타입 : ", data.mimetype);
        console.log("파일이 저장된 폴더 : ", data.destination);
        console.log("destinatin에 저장된 파일 명 : ", data.filename);
        console.log("업로드된 파일의 전체 경로 ", data.path);
        console.log("파일의 바이트(byte 사이즈)", data.size);
    })

    ctx.body = {ok: true, data: "Multipart Upload Ok"}

})

 

 

- 단일 & 멀티 파일 업로드

/**
 * @author Ryan
 * @description 단일 및 여러 파일 업로드 
 * 
 * fields를 설정해서 특정 파일은 단일 또는 여러 파일 그리고 특정 파일을 나눠서 업로드가 가능하다.
 * 
 * Ex) 클라이언트가 요청할 때 pdf 파일은 한개를 받고 이미지 파일은 여러개를 받는 상황
 *     이런식으로 정의해서 사용할 수 있다.
 */
const fileFields = upload.fields([
    { name: 'file1', maxCount: 1 },
    { name: 'file2', maxCount: 8 },
]);

router.post('/fields/upload', fileFields, (ctx, next) => {


    const { file1, file2 } = ctx.request.files;
    const { name } = ctx.request.body;


    console.log("body 데이터 : ", name);

    //배열 형태이기 때문에 반복문을 통해 파일 정보를 알아낸다.
    file1.map(data => {
        console.log("file1");
        console.log("     ");
        console.log("폼에 정의된 필드명 : ", data.fieldname);
        console.log("사용자가 업로드한 파일 명 : ", data.originalname);
        console.log("파일의 엔코딩 타입 : ", data.encoding);
        console.log("파일의 Mime 타입 : ", data.mimetype);
        console.log("파일이 저장된 폴더 : ", data.destination);
        console.log("destinatin에 저장된 파일 명 : ", data.filename);
        console.log("업로드된 파일의 전체 경로 ", data.path);
        console.log("파일의 바이트(byte 사이즈)", data.size);
    })
    
    console.log("     ");
    console.log("-----------------------------------------------");
    console.log("     ");
    
    //배열 형태이기 때문에 반복문을 통해 파일 정보를 알아낸다.
    file2.map(data => {
        console.log("file2");
        console.log("     ");
        console.log("폼에 정의된 필드명 : ", data.fieldname);
        console.log("사용자가 업로드한 파일 명 : ", data.originalname);
        console.log("파일의 엔코딩 타입 : ", data.encoding);
        console.log("파일의 Mime 타입 : ", data.mimetype);
        console.log("파일이 저장된 폴더 : ", data.destination);
        console.log("destinatin에 저장된 파일 명 : ", data.filename);
        console.log("업로드된 파일의 전체 경로 ", data.path);
        console.log("파일의 바이트(byte 사이즈)", data.size);
    })

    ctx.body = {ok: true, data: "Fields Upload Ok"}

})

fields를 두 개 설정했기 때문에 구조분해를 통해 file1과 file2를 가져올 수 있습니다.

 

모든 업로드 되는 파일들은 현재 위치에 uploads라는 폴더 하위에 저장이 됩니다. (파일 이름은 임시 저장)

 

uploads 폴더 하위 이미지 저장

"파일 이름이 임시저장 하는 부분이 싫으시면 따로 설정 할 수 있습니다."

console.log
body 데이터 : Ryan
폼에 정의된 필드명 : file
사용자가 업로드한 파일 명 : Ryan.png
파일의 엔코딩 타입 : 7bit
파일의 Mime 타입 : image/png
파일이 저장된 폴더 : /Users/sinhangug/ryan/server/koa-multer/uploads/
destinatin에 저장된 파일 명 : 0d3908a8756cf6481a2742eb5597bcf2
업로드된 파일의 전체 경로 /Users/sinhangug/ryan/server/koa-multer/uploads/0d3908a8756cf6481a2742eb5597bcf2
파일의 바이트(byte 사이즈) 21111

로그에서 데이터를 확인 할 수 있습니다.

 

만약 임시 이름이 싫거나 특정 폴더를 지정하고 싶으면 아래 코드를 활용해서 옵션을 설정 할 수 있습니다.

 

// 파일 경로 및 이름 설정 옵션
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, '/tmp/my-uploads') // 파일 업로드 경로
  },
  filename: function (req, file, cb) {
    cb(null, file.fieldname + '-' + Date.now()) //파일 이름 설정
  }
})

const upload = multer({ storage: storage })

 

  • destination 옵션은 어느 폴더안에 파일을 저장할 지 결정합니다. (이 옵션을 주지 않으면, 운영체제 시스템 임시 파일을 저장한느 기본 디렉토리를 사용합니다.)
  • filename 옵션은 폴더안에 저장되는 파일 이름을 결정합니다. (디렉토리를 생성하지 않으면 에러가 발생!! )

Storage 옵션

multer에는 MemoryStorage 방식과 DiskStorage 방식 두 가지가 있습니다.

 

보통 AWS S3와 같은 외부 파일 스토리즈 서버에 이미지를 업로드를 할 때는 MemoryStorage를 사용합니다.

 

RAM 안에 이미지 Butter라는 값을 통해 값을 확인 할 수 있습니다.

 

하지만 해당 운영체제 RAM 용량이 적으면 데이터 손실 또는 에러가 발생할 수 있습니다.

 

그렇기 때문에 DiskStorage에 파일을 쓰고 삭제하는 방식을 추천드립니다.

 

DiskStorage 방식은 말 그대로 "Disk에 파일을 저장하겠다" 라고 생각하시면 됩니다. 그렇게 때문에 uploads라는 폴더 아래에 파일이 저장됩니다.

 

Limites & Filter 옵션

클라이언트가 아무 이미지나 올리면 안되겠죠? 이럴 때 사용할 수 있는 옵션입니다.

 

//multer 미들웨어 파일 제한 값 (Doc 공격으로부터 서버를 보호하는데 도움이 된다.)
const limits = {
    fieldNameSize: 200, // 필드명 사이즈 최대값 (기본값 100bytes)
    filedSize: 1024 * 1024, // 필드 사이즈 값 설정 (기본값 1MB)
    fields: 2, // 파일 형식이 아닌 필드의 최대 개수 (기본 값 무제한)
    fileSize : 16777216, //multipart 형식 폼에서 최대 파일 사이즈(bytes) "16MB 설정" (기본 값 무제한)
    files : 10, //multipart 형식 폼에서 파일 필드 최대 개수 (기본 값 무제한)
}

/**
 * @author Ryan
 * @description 파일 업로드시 파일 체크 함수
 * 
 * @param {Object} file 파일 정보
 * 
 * {
 *     fieldname: 'file',
 *     originalname: '001.png',
 *     encoding: '7bit',
 *     mimetype: 'image/png'
 * }
 * 
 * @param {Function} callback 파일 업르도 허용 및 거부 처리
 *               
 *               허용: callback(null, true);
 *               거부: callback(null, false);
 */
const fileFilter = (req, file, callback) =>{

    const typeArray = file.mimetype.split('/');

    const fileType = typeArray[1]; // 이미지 확장자 추출
    
    //이미지 확장자 구분 검사
    if(fileType == 'jpg' || fileType == 'jpeg' || fileType == 'png'){
        callback(null, true)
    }else {

        // return callback(new Error("*.jpg, *.jpeg, *.png 파일만 업로드가 가능합니다."), false)
        return callback({message: "*.jpg, *.jpeg, *.png 파일만 업로드가 가능합니다."}, false)
    }
}

//파일을 저장할 디렉토리 설정 (현재 위치에 uploads라는 폴더가 생성되고 하위에 파일이 생성된다.)
const upload = multer({ 
    dest: __dirname+'/uploads/', // 이미지 업로드 경로
    limits: limits, // 이미지 업로드 제한 설정
    fileFilter : fileFilter // 이미지 업로드 필터링 설정
})

기존 multer 코드 안에 limits와 fileFilter 항목에 옵션을 설정할 수 있습니다.

 

만약 에러가 발생한다면 multer 미들웨어는 Koa 프레임워크에 위임 합니다.

 

Koa에 Error Handler를 설정해서 에러를 캐치하면 됩니다.

/**
 * @author Ryan
 * @description Error Handler
 * 
 * Express와 달리 Koa에서는 Error Handler를 라우터 보다 위에 선언해야됩니다.
 */
app.use(async(ctx, next) => {
    try {
        await next();
    } catch (err) {
        // will only respond with JSON
        ctx.status = err.statusCode || err.status || 500;
        ctx.body = { ok: false, data: err.message };
    }
})

위 코드는 가장 아래에 설정해야 됩니다.

 

app.use는 모든 요청에 대해서 캐치가 가능하기 때문에 우리는 에러가 발생했을 때만 해당 라우터가 실행하기 때문입니다.

 

소스 저장소

GitHub : github.com/Ryan-Sin/Node_Koa_Multer

 

Ryan-Sin/Node_Koa_Multer

Contribute to Ryan-Sin/Node_Koa_Multer development by creating an account on GitHub.

github.com

- 참고 사이트

GitHub :  github.com/expressjs/multer/blob/master/doc/README-ko.md

 

expressjs/multer

Node.js middleware for handling `multipart/form-data`. - expressjs/multer

github.com

GitHub : github.com/koajs/multer

 

koajs/multer

Middleware for handling `multipart/form-data` for koa, based on Express's multer. - koajs/multer

github.com