프로젝트 설명 : 사용자가 .txt 파일을 업로드하면 웹사이트가 그 파일을 읽어 보여주는 기능을 추가한다. (편의상, 지난 영화 웹사이트 안에서 추가 서비스를 제공하는 형태로 진행하였다.)
소요기간 : 2022.02.03
전체코드 보기 : 깃허브 링크 바로가기
주요 흐름
/convert 페이지로 들어왔을 때의 모습
텍스트 파일을 업로드 하였을 때 그 내용이 아래에 표시되는 모습
목록에 있던 텍스트 파일의 제목을 클릭하면 구체적인 파일 내용을 볼 수 있는 창으로 이동
Express에서 파일 업로드를 수행 및 처리하기 위해서는 Multer라는 package를 사용하면 된다. 즉, Multer라는 package를 사용해 Node js 에서 파일을 업로드할 수 있게 된 것이다.
추가) 업로드 후 저장되는 filename 을 랜덤이 아니라, 내가 원하는 대로 지정해주기 위해 다음과 같이 storage 를 사용하였다. (If no filename is given, each file will be given a random name that doesn’t include any file extension.)
//src/middlewares.js
//기존코드
export const convertFiles = multer({ dest: "texts/" });
//수정된 코드
let storage = multer.diskStorage({
destination: function(req, file, cb) {
cb(null, 'texts/');
},
filename: function(req, file, cb) {
cb(null, file.originalname);
}
});
export const convertFiles = multer({ storage: storage });
fs는 node.js에 들어 있는 module로 file system의 약자이다. 서버의 파일/폴더에 접근할 수 있는 함수들이 들어 있다. fs 모듈의 메서드를 사용하여 파일을 읽고 쓰는 등의 처리를 할 수 있다.
그 중, 우리가 사용할 메소드는 readdir(폴더 읽기)와 readFile(파일 읽기) 기능이다.
//src/controllers/convertController.js
import { readdir, readFile } from 'fs';
export const getConvert = (req, res) => {
//texts 폴더에 있는 파일들 읽기
readdir('texts', (err, files) => {
try{
console.log(files)
return res.render("convert/convert.pug", {pageTitle : "추가 서비스: TXT to HTML", files})
}catch(err){
return res.render("convert/convert.pug", {pageTitle : "추가 서비스: TXT to HTML"}, {errorMessage : "Error"})
}
})
}
export const postConvert = (req, res) => {
const { file } = req;
//texts/${file.filename} 파일 읽기
readFile(`texts/${file.filename}`, 'utf8', (err, data) => {
try{
readdir('texts', (err, files) => { //texts 폴더 안 파일 다시 읽기 (비동기로 하면 참 좋을텐ㄷ.....ㅔ)
return res.render("convert/convert.pug", {pageTitle : "추가 서비스: TXT to HTML", data, files }
)}
)
} catch(err) {
return res.render("convert/convert.pug", {pageTitle : "추가 서비스: TXT to HTML"}, {errorMessage : "Error"})
}
});
}
export const converDetail = (req, res) => {
const {
params :
{ id }
} = req;
console.log(id);
readFile(`texts/${id}`, 'utf8', (err, data) => { //texts/${id} 파일 읽기
try{
console.log(data)
return res.render("convert/detail.pug", {pageTitle : `${id}`, data})
} catch(err) {
return res.render("convert/detail.pug", {pageTitle : `${id}` , errorMessage : "Error"})
}
});
}
그러나, 위 코드의 문제점은 다음과 같다.
1. 파일 및 폴더를 읽는 작업이 들어있으나, 비동기적인 처리가 되어 있지 않다.
2. 파일 및 폴더를 읽는 작업 코드가 반복적으로 들어있다. -> 따로 extract 해보자.
//src/controllers/convertController.js
import fs from "fs";
const getDataFromFilePromise = (filePath, options = null) => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, options, function (err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
};
const getFilesFromDirPromise = (path, options = "utf8") => {
return new Promise((resolve, reject) => {
fs.readdir(path, options, (err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
});
});
};
export const getConvert = async (req, res) => {
const files = await getFilesFromDirPromise("/texts");
return res.render("convert/convert.pug", {pageTitle : "추가 서비스: TXT to HTML", files});
};
export const postConvert = (req, res) => {
const { file } = req;
return res.render("convert/convert.pug", {pageTitle : "추가 서비스: TXT to HTML", data, files });
};
export const convertDetail = async (req, res) => {
const {
params: { id }
} = req;
const data = await getDataFromFilePromise(`/texts/${id}`, "utf8");
return res.render("convert/detail.pug", {pageTitle : `${id}`, data});
};
getDataFromFilePromise : 파일로부터 데이터 읽는 코드 (프로미스)
getFilesFromDirPromise : 폴더로부터 파일 읽는 코드 (프로미스)
//promise 생성자 : new Promise(){..} let promise = new Promise(function(resolve, reject) { // executor (제작 코드, '가수') });
- JavaScript에서 Promise는 비동기적으로 실행하는 작업의 결과(성공 or 실패)를 나타내는 객체입니다. 여기서 주목해야 하는 점은
객체
라는 것인데, 비동기의 결과를 객체화 시킨다는 점이 Promise의 가장 큰 장점이자 특징이 됩니다.- new Promise에 전달되는 함수는 executor(실행자, 실행 함수) 라고 부릅니다. executor는 new Promise가 만들어질 때 자동으로 실행되는데, executor는 보통 시간이 걸리는 일을 수행합니다(그리고 그것을 비동기적으로 수행합니다). 일이 끝나면 resolve나 reject 함수를 호출합니다.
resolve(value)
— 일이 성공적으로 끝난 경우 그 결과를 나타내는 value와 함께 호출
reject(error)
— 에러 발생 시 에러 객체를 나타내는 error와 함께 호출
파일로부터 데이터 읽는 코드(getDataFromFilePromise) 와 폴더로부터 파일 읽는 코드(getFilesFromDirPromise) 에서 각각 fs.readFile와 fs.readdir 를 사용하고 있어 시간이 소요되므로, Promise 객체를 사용해준 것이다.
const getDataFromFilePromise = (filePath, options = null) => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, options, function (err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
};
const getFilesFromDirPromise = (path, options = "utf8") => {
return new Promise((resolve, reject) => {
fs.readdir(path, options, (err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
});
});
};
- await('기다리다’라는 뜻을 가진 영단어 – 옮긴이)는 말 그대로 Promise가 처리될 때까지 함수 실행을 기다리게 만듭니다. Promise가 처리되면 그 결과와 함께 실행이 재개되죠.
- await는 async 함수 안에서만 동작합니다.
- await는 promise.then보다 좀 더 세련되게 Promise의 결과값이나 에러를 얻을 수 있도록 해주는 문법입니다. promise.then보다 async/await를 사용하는 것이 가독성도 더 좋고 편리합니다.
getConvert 와 convertDetail 에서 getFilesFromDirPromise 와 getDataFromFilePromise 함수를 사용하고 그 결과값을 템플릿에 던지는(결과값 나올 때까지 기다려줘야함) 있기 때문에 async await 사용해줬다.
export const getConvert = async (req, res) => {
const files = await getFilesFromDirPromise("/texts");
// 프라미스가 이행될 때까지 기다림. 프라미스가 처리되면 실행이 재개
return res.render("convert/convert.pug", {pageTitle : "추가 서비스: TXT to HTML", files});
};
export const convertDetail = async (req, res) => {
const {
params: { id }
} = req;
const data = await getDataFromFilePromise(`/texts/${id}`, "utf8");
// 프라미스가 이행될 때까지 기다림. 프라미스가 처리되면 실행이 재개
return res.render("convert/detail.pug", {pageTitle : `${id}`, data});
};
참고:
https://velog.io/@cyranocoding/2019-08-02-1808-%EC%9E%91%EC%84%B1%EB%90%A8-5hjytwqpqj