[Node.js] 폴더 업로드 후 엑셀 파일을 이용한 데이터 정제

이정찬·2022년 6월 1일
0

Node.js

목록 보기
1/1
post-thumbnail

2021년 12월부터 2022년 4월까지 진행된 Node.js를 이용한 학교 문제은행 어플 서버 개발과 Vanilla Web 기술을 이용한 관리자 페이지 개발 외주에서 배운 내용을 정리하기 위해 쓴 글입니다.
AWS EC2 t2.micro instance에 Ubuntu 20.04 버전을 설치하여 작업하였습니다.

이번 외주는 학교에서 사용할 문제은행 어플을 만드는 외주였습니다. 학생들이 문제를 풀 수 있도록 문제를 등록하는 과정이 필요했는데, 엑셀 파일과 문제 이미지가 들어있는 폴더를 통째로 업로드해야 했습니다. 어떻게 할지 고민하다가 방법을 찾아내었습니다.

1. multipart/formdata 사용

HTML의 form태그를 사용하면, form 태그 내부의 값들을 서버로 보내주는 작업을 할 수 있습니다. 여기에서, form의 옵션으로 multipart/formdata를 달아주면 폴더를 통째로 업로드 할 수 있습니다.

<!--questionPage.html-->
<form id="form" action="question" method="post" enctype="multipart/form-data">
  <label class="buttons" for="select">폴더 선택</label>
  <input type="file" class="buttons" id="select" name="fileupload" value="폴더 선택" webkitdirectory>
  <input type="submit" class="buttons" id="upload" value="업로드">    
</form>

fileupload name을 가진 input태그에서 폴더를 지정합니다. webkitdirectory 옵션을 줘서 디렉토리(폴더)를 고를 수 있도록 해야합니다. 이후, submit을 진행하면 선택한 폴더 안에 있는 모든 파일들이 백엔드로 전송됩니다. 이 코드에서는 post 메소드로 만들어진 question api로 보내기 때문에, 이후에는 Node.js로 만들어진 api에서 파일의 처리를 진행합니다.

2. Node.js에서 Multer 이용

Multer 라이브러리를 이용하면 업로드된 파일을 백엔드단에서 관리할 수 있습니다.
먼저, 라이브러리를 설치해 줍니다.

npm install multer

HTML의 input태그에서 name으로 fileupload를 주고, 그곳에서 폴더를 골랐기 때문에 Multer 라이브러리를 이용할 때, 이 name을 이용해 줍니다.

// question.js
const router = require('express').Router();
const path = require('path');
const fs = require('fs');
...

const makeFolder = async(dir) => {
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir);
    }
}

...

router.post('', async(req, res) => {
  	...
  	try {
   	     await makeFolder(path.join(__dirname, '../upload')); // upload라는 이름의 폴더 생성
 	}
 	catch (err) {
   		...
  	}
    ...
});

먼저, 파일이 저장될 폴더를 서버 내에 만들어줍니다. upload라는 이플의 폴더를 만드는데, 이미 있다면 만들지 않고 그냥 넘어갈 수 있도록 함수를 작성해줍니다. 이렇게 하면 파일이 업로드될 폴더의 준비는 끝났습니다.

// question.js
const router = require('express').Router();
const util = require('util');
const multer = require('multer');
...

const Storage = multer.diskStorage({
    destination: (req, file, callback) => {
        callback(null, 'upload/');
    },
    filename: (req, file, callback) => {
        callback(null, file.originalname);
    },
});

const fileFilter = (req, file, callback) => {
    const type = file.originalname.split('.')[1];
  
    if (type === 'jpg' || type === 'png' || type === 'jpeg' || type === 'gif' || type === 'xlsx') {
        callback(null, true);
    }
    else {
        req.fileValidationError = 'jpg, png, jpeg, gif 또는 xlsx 파일만 업로드 가능합니다.';
        callback(null, false);
    }
}

const Multer = multer({ 
    storage: Storage,
    fileFilter: fileFilter, // optional
});

...

router.post('', async(req, res) => {
  	...
    
    try { // 엑셀 읽어오고, 데이터 정제하는 부분
        const upload = util.promisify(Multer.any('fileupload'));
        await upload(req, res);
      	...
});

Multer를 통해 파일을 업로드 하는 과정입니다. 먼저, Storage를 선언합니다. 매개변수로 함수를 value로 갖는 json 을 하나 보내게 됩니다.
destination 값으로 파일이 업로드 될 폴더를 콜백함수 형태로 지정해줍니다.
filename값으로는 파일이 어떤 이름으로 저장될 지를 정할 수 있습니다. 저는 파일을 그대로 사용할 것이기 때문에, file.originalname으로 파일의 업로드 될 때의 이름을 사용하였습니다.

이후, fileFilter를 설정합니다. 이것은 옵션으로, 필요 없다면 설정하지 않아도 됩니다. 저는 받아야 할 파일의 확장자가 정해져 있었으므로, 함수 fileFilter를 통해 넘어간 파일이 해당하는 확장자가 맞는지 체크하는 로직을 내부에 작성하였습니다. split()함수를 이용하여 파일 이름을 확장자 기준으로 쪼갠 후, 뒷부분만 체크하여 확장자를 확인했습니다.

이렇게 지정한 StoragefileFiltermulter함수에 정의합니다. 이제 사용준비가 모두 끝났습니다.

저는 파일 업로드의 비동기 처리를 위해 util.promisify()함수를 사용하였습니다. util라이브러리는 기본 내장 라이브러리이며, 따로 설치할 필요가 없습니다.

const upload = util.promisify(Multer.any('fileupload'));
await upload(req, res);

fileupload라는 name을 가진 input태그에서 넘어오는 모든 파일을 가져오겠다고 선언하고, await로 선언한 함수를 실행하면, 위에서 지정한 폴더로 제가 업로드 한 파일이 모두 업로드되게 됩니다.

3. xlsx 라이브러리를 사용한 데이터 후처리

파일이 모두 업로드 되었고, 이제 그 파일들을 같이 읽어온 엑셀 파일에 따라서 후처리를 해줘야 합니다. 같이 업로드 하는 엑셀 파일에 그 데이터들이 들어있었으므로, 엑셀을 읽어야 하는데, 저는 xlsx 라이브러리를 사용하였습니다.

외부 라이브러리이므로, 설치가 필요합니다.

npm install xlsx

설치를 진행한 후, 여타 라이브러리와 동일하게 require구문으로 import해서 사용할 수 있습니다.

// question.js
const xlsx = require('xlsx');
...

router.post('', async(req, res) => {
  	...
    const originalNames = req.files.map(element => {
      	return element.originalname;
    });
  	const excel = originalNames.find(element => element.includes('xlsx'));
  	const excelFile = xlsx.readFile(path.join(__dirname, '../upload/' + excel));
  	const sheetName = excelFile.SheetNames[0]; // 첫 번째 시트
  	const firstSheet = excelFile.Sheets[sheetName]; // 첫 번째 시트 열어줌
  	const jsonData = xlsx.utils.sheet_to_json(firstSheet, { defval: '', });
  	...
});

req.files로 넘어온 모든 file 객체 배열을 전부 탐색하여 파일의 이름을 가져옵니다. 그 중, find() 함수와 includes() 함수를 사용하여 확장자명이 xlsx인 파일을 가져옵니다.

이후 xlsx.readFile() 함수를 사용하여 위에서 Multer를 이용해 업로드 한 파일 중, 엑셀 파일을 읽어옵니다. 제가 읽을 파일은 시트가 한개짜리 파일이었기 때문에, 0번 시트(첫 번째 시트)의 이름만 가져와서 firstSheet 변수에 시트의 정보를 담아줍니다.

그리고 xlsx.utils.sheet_to_json(firstSheet, { defval: '', }) 이라고 써주면, 해당 엑셀 파일을 json 배열로 반환해줍니다. 원소는 한 개의 row를 이루는 title값을 key로 갖는 json입니다. 자동으로 엑셀 파일의 맨 위 row의 셀 값들을 key로 지정하고, 그 밑의 셀 값들을 value로 지정합니다.

defval을 지정해주면, 엑셀로 json 배열을 만들 때 빈 셀을 만났을 때의 처리를 진행해줍니다. defval이 없다면 빈 셀은 들어가지 않아서 배열의 모든 json 원소들의 형태가 달라질 수 있습니다. defval: '' 로 지정했다면, 빈 값에 대한 원소값은 자동으로 빈 문자열이 들어오게 됩니다. 따라서, 모양이 균일한 데이터를 얻을 수 있습니다.

4. 마치며

해당 외주를 진행할 때, 가장 애먹었던 부분이고, 가장 많은 QA를 만들었던 부분입니다. 오늘은 대략적인 방법만 적었지만, 다음 포스팅부터는 QA나 세세한 예외처리 항목에 대해서 적도록 하겠습니다.

profile
개발자를 꿈꾸는 사람

0개의 댓글