2021년 12월부터 2022년 4월까지 진행된 Node.js를 이용한 학교 문제은행 어플 서버 개발과 Vanilla Web 기술을 이용한 관리자 페이지 개발 외주에서 배운 내용을 정리하기 위해 쓴 글입니다.
AWS EC2 t2.micro instance에 Ubuntu 20.04 버전을 설치하여 작업하였습니다.
이번 외주는 학교에서 사용할 문제은행 어플을 만드는 외주였습니다. 학생들이 문제를 풀 수 있도록 문제를 등록하는 과정이 필요했는데, 엑셀 파일과 문제 이미지가 들어있는 폴더를 통째로 업로드해야 했습니다. 어떻게 할지 고민하다가 방법을 찾아내었습니다.
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에서 파일의 처리를 진행합니다.
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()
함수를 이용하여 파일 이름을 확장자 기준으로 쪼갠 후, 뒷부분만 체크하여 확장자를 확인했습니다.
이렇게 지정한 Storage
와 fileFilter
를 multer
함수에 정의합니다. 이제 사용준비가 모두 끝났습니다.
저는 파일 업로드의 비동기 처리를 위해 util.promisify()
함수를 사용하였습니다. util
라이브러리는 기본 내장 라이브러리이며, 따로 설치할 필요가 없습니다.
const upload = util.promisify(Multer.any('fileupload'));
await upload(req, res);
fileupload
라는 name
을 가진 input
태그에서 넘어오는 모든 파일을 가져오겠다고 선언하고, await로 선언한 함수를 실행하면, 위에서 지정한 폴더로 제가 업로드 한 파일이 모두 업로드되게 됩니다.
파일이 모두 업로드 되었고, 이제 그 파일들을 같이 읽어온 엑셀 파일에 따라서 후처리를 해줘야 합니다. 같이 업로드 하는 엑셀 파일에 그 데이터들이 들어있었으므로, 엑셀을 읽어야 하는데, 저는 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: ''
로 지정했다면, 빈 값에 대한 원소값은 자동으로 빈 문자열이 들어오게 됩니다. 따라서, 모양이 균일한 데이터를 얻을 수 있습니다.
해당 외주를 진행할 때, 가장 애먹었던 부분이고, 가장 많은 QA를 만들었던 부분입니다. 오늘은 대략적인 방법만 적었지만, 다음 포스팅부터는 QA나 세세한 예외처리 항목에 대해서 적도록 하겠습니다.