React와 Node.js를 활용한 PDF 생성 및 미리보기 이미지 처리(2)
이번 포스팅에서는 Client에서 보낸 File Data를 Server에서 처리하는 과정에 대해 알아본다.
디렉토리 구조
MVC 패턴의 일부 개념을 사용하면서도 미들웨어 패턴과 라우터 기반 패턴이 혼합된 형태로 디렉토리 구조를 잡았고, typescript를 사용하려 했으나 시간 관계상 점진적으로 변경하기로 하였다.
/server
├── /public
│ ├── /files
│ │ └── test.pdf
│ ├── /images
│ │ └── test-preview.png
├── /src # 소스 코드 폴더
│ ├── /controller # 요청 처리를 위한 컨트롤러 파일
│ │ └── ebook.js
│ ├── /data # 데이터베이스 모델 정의
│ │ └── ebook.js
│ ├── /middlewares # 미들웨어 정의
│ │ ├── convertPdf.js # PDF 파일의 첫 페이지를 PNG 이미지로 변환 미들웨어
│ │ └── uploadBook.js # PDF 파일 저장 미들웨어
│ ├── /router # 라우터 정의
│ │ └── ebook.js
│ └── app.ts # 메인 애플리케이션 파일
├── /config.ts # 환경 설정 파일
└── .env # 환경 변수 파일
app
의존성 및 설정
express
: Node.js 웹 애플리케이션 프레임워크로, HTTP 요청을 처리한다.cors
: CORS(Cross-Origin Resource Sharing)를 설정하여 다른 도메인에서의 요청을 허용한다.morgan
: HTTP 요청 로깅 미들웨어로, 로그 정보를 콘솔에 출력한다.helmet
: 보안 관련 헤더를 설정해 애플리케이션을 보호한다.config
: 서버 설정 값을 가져오는 모듈로, 환경 변수 및 설정 파일을 다룬다.sequelize
: ORM(Object Relational Mapping) 도구로, 데이터베이스와의 상호작용을 처리한다.
import express, { Request, Response } from "express";
import cors from "cors";
import moran from "morgan";
import helmet from "helmet";
import { config } from "./config";
import { sequelize } from "./db/database";
import ebookRouter from "./router/ebook.js";
import { initSocket } from "./connection/socket";
const app = express();
// CORS 설정
const corsOptions = {
origin: config.cors.allowedOrigin, // allowedOrigin 값에 기반하여 특정 도메인만 CORS 요청을 허용
optionsSuccessStatus: 200, // 요청의 성공 상태
credentials: true, // 쿠키와 인증 정보를 포함한 요청을 허용
};
app.use(express.json()); // JSON 형식의 요청 본문을 파싱
app.use(helmet()); // 보안 설정을 추가하여 공격을 방지
app.use(cors(corsOptions)); // CORS 설정을 적용
app.use(moran("tiny")); // HTTP 요청 로깅을 설정
app.use(express.static("public")); // 정적 파일(이미지, HTML 등)을 public 디렉토리에서 서빙
app.use("/weblink", weblinkRouter);
// Sequelize로 데이터베이스 연결 및 서버 시작
sequelize.sync().then(() => {
console.log(`Server is started.... ${config.host.port}`);
const server = app.listen(config.host.port);
initSocket(server);
});
전체적인 흐름
서버가 시작되면 Express 애플리케이션이 설정되며 미들웨어가 실행되며, 라우터 /ebook 엔드포인트에 대해 요청을 처리합한다. 데이터베이스와 동기화된 후 서버가 지정된 포트에서 시작고, 추후 WebSocket을 사용하여 채팅 기능을 추가할 예정이므로 WebSocket을 초기화 하여 클라이언트와 실시간 연결을 유지할 준비가 된다. 이 코드는 Express와 Sequelize 기반의 API 서버이며, WebSocket을 활용한 실시간 기능을 포함한 구조다.
router
라우터에서는 전자책 업로드 및 관리 기능을 제공하는 API 서버의 라우팅 로직이다. 인증된 사용자만이 전자책을 업로드, 업데이트, 삭제할 수 있으며, 업로드 과정에서는 PDF 파일을 PNG 이미지로 변환하는 기능을 포함하고 있다.
import express from "express";
import "express-async-errors";
import { body } from "express-validator";
import { isAuth } from "../middleware/auth.js";
import { isUpload } from "../middleware/uploadBook.js";
import { convertPdf } from "../middleware/convertPdf.js";
import { validate } from "../middleware/validator";
import * as ebookController from "../controller/ebook.js";
const router = express.Router();
const validateEbook = [
// 유효성 검사 미들웨어
// ...,
validate, // express-validator가 적용된 후 발생한 에러를 처리하는 미들웨어.
];
router.get("/", isAuth, ebookController.getBooks);
router.post(
"/",
isAuth, // 인증 여부를 확인하는 미들웨어.
validateEbook,
isUpload.single("file"), // 업로드된 파일을 처리하는 미들웨어. file 필드에서 단일 파일을 받음.
convertPdf, // PDF 파일을 PNG로 변환하는 미들웨어.
ebookController.uploadBook // 업로드된 전자책 데이터를 처리하는 컨트롤러.
);
// ... 수정, 삭제 라우터
export default router;
middleware
uploadBook.js
Multer 라이브러리를 사용하여 서버에 파일을 업로드하는 기능을 구현했다. Multer는 파일 업로드를 관리하는 미들웨어로, 파일의 저장 위치와 파일명 등을 설정할 수 있다.
이 미들웨어에서는,
파일 업로드 기능에서 보안과 파일 형식 제한을 적절하게 처리하고, 파일 이름에 특수 문자를 제거하고, 한글 파일명 문제를 해결하기 위한 기능을 수행한다.
import path from "path";
import multer from "multer";
import fs from "fs";
// 업로드 폴더 경로 설정 (public/uploads)
const uploadPath = path.join(process.cwd(), "public", "files");
// 서버 시작 시 업로드 경로가 존재하지 않으면 폴더를 생성
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath, { recursive: true });
console.log("files directory created inside public");
}
// Multer 설정
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadPath); // public/uploads 경로로 파일 저장
},
filename: (req, file, cb) => {
const safeFileName =
Date.now() +
"-" +
Buffer.from(file.originalname, "latin1")
.toString("utf8")
.replace(/[^a-zA-Z0-9.\-_]/g, "");
cb(null, safeFileName); // 파일 이름 설정
},
});
// 파일 업로드 제한
export const isUpload = multer({
storage,
limits: {
fileSize: 510 * 1024 * 1024, // 파일 크기 제한 (10MB)
},
fileFilter: (req, file, cb) => {
if (file.mimetype === "application/pdf") {
cb(null, true); // PDF 파일만 허용
} else {
cb(new Error("Only PDF files are allowed"), false);
}
},
});
destination
: 업로드된 파일을 저장할 디렉토리를 지정한다. 모든 파일이 uploadPath(public/files)에 저장된다.filename
: 파일 이름을 안전하게 만들기 위해, 현재 시간을 기준으로 파일 이름을 설정하고, 파일명에서 특수 문자를 허용하지 않으며, 한글 파일명은 latin1에서 utf8로 변환한다. 이 과정은 한글이나 특수 문자가 포함된 파일명이 깨지는 것을 방지하는 역할을 한다.storage
: 앞서 정의한 storage 설정을 사용하여 파일의 저장 경로와 파일 이름을 결정한다.limits
: 업로드할 파일 크기를 510MB로 제한한다.fileFilter
: 파일의 MIME 타입을 검사하여, PDF 파일만 업로드가 가능하도록 설정한다. PDF가 아닌 파일을 업로드하려고 할 경우 에러를 발생시킨다.
convertPdf.js
이 코드는 PDF 파일을 PNG 이미지로 변환하는 미들웨어이다. node-poppler
라이브러리를 사용하여 PDF 파일의 첫 페이지를 PNG 파일로 변환한 후, 변환된 파일 경로를 요청 객체에 추가하는 로직으로 구성되어 있다.
import { Poppler } from "node-poppler";
import path from "path";
import fs from "fs";
const poppler = new Poppler();
// Express 미들웨어로 작동하며, PDF 파일을 변환한 후에 요청을 다음 미들웨어로 넘긴다.
export const convertPdf = async (req, res, next) => {
try {
const file = req.file;
// 파일 존재 여부 검사
if (!file) {
return res.status(400).json({ message: "No file uploaded" });
}
// 이미지 저장 경로 설정
const uploadPath = path.join(process.cwd(), "public", "images");
// 이미지 경로가 없으면 생성
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath, { recursive: true });
console.log("Images directory created inside public");
}
const pdfFilePath = file.path; // 업로드된 PDF 파일의 경로
// 변환된 이미지 파일이 저장될 경로
const outputFilePath = path.join(
uploadPath,
`${path.parse(file.filename).name}-preview`
);
// PDF 파일의 첫 페이지를 PNG 이미지로 변환
await poppler.pdfToCairo(pdfFilePath, outputFilePath, {
pngFile: true, // PNG 파일로 변환
singleFile: true, // 첫 페이지만 변환
// 첫 페이지와 마지막 페이지를 동일하게 설정
firstPageToConvert: 1,
lastPageToConvert: 1,
});
req.outputFilePath = outputFilePath; // 변환된 이미지 경로 저장
next(); // 변환이 완료되면 다음 미들웨어로 넘어감
} catch (error) {
res.status(500).json({
message: "PDF to image conversion failed",
error: error.message,
});
}
};
이 미들웨어는 업로드된 PDF 파일의 첫 페이지를 PNG 이미지로 변환하여 서버의 public/images 디렉토리에 저장한다. 변환된 이미지 파일의 경로는 req.outputFilePath에 저장되며, 이후의 미들웨어나 컨트롤러에서 이 파일 경로를 참조할 수 있다. 파일 업로드 시, 해당 파일이 없으면 400 에러를 반환하며, 변환 중 오류가 발생하면 500 에러를 반환한다.
Controller
실제 비즈니스 로직을 처리하는 역할하며, 파일 업로드 후 해당 파일의 정보를 데이터베이스에 저장하고 클라이언트에 응답을 반환한다.
import * as ebookRepository from "../data/ebook.js";
export async function getBooks(req, res) {
const filename = req.query.filename;
const data = await (filename
? ebookRepository.getAllByFilename(filename)
: ebookRepository.getAll());
res.status(200).json(data);
}
export async function uploadBook(req, res) {
try {
const { file } = req;
const outputFilePath = `${req.outputFilePath}.png`; // convertPdf에서 저장한 이미지 경로를 참조
const originalname = Buffer.from(file.originalname, "latin1").toString(
"utf8"
); // 한글 파일명 처리
if (!file || !outputFilePath) {
return res.status(400).json({ message: "File or output path missing" });
}
const book = await ebookRepository.create(
file.filename, // 파일 이름
originalname, // 한글 파일명 처리
outputFilePath, // 이미지 경로
file.path, // PDF 파일 경로
file.mimetype, // 파일 타입 (예: application/pdf)
file.size, // 파일 크기
req.userId // 업로드한 사용자 ID
);
res.status(201).json({ message: "File uploaded successfully", book });
} catch (error) {
console.error("File upload failed:", error);
res
.status(500)
.json({ message: "File upload failed", error: error.message });
}
}
data
Sequelize를 사용하여 데이터베이스에서 전자책(Ebook) 데이터를 관리하는 모델 및 데이터 처리 로직을 구현했다. 이 모델은 전자책의 파일 정보와 관련된 데이터뿐만 아니라 사용자 정보도 연관되어 있으며, 주요 기능은 데이터베이스에서 전자책 정보 CRUD 기능을 포함한다. 또한, 전자책의 썸네일 이미지 파일을 Base64 형식으로 변환하여 클라이언트에 전달하는 기능도 포함하고 있다.
import SQ from "sequelize";
import { sequelize } from "../db/database.js";
import { User } from "./auth.js";
import fs from "fs";
const DataTypes = SQ.DataTypes;
const Sequelize = SQ.Sequelize;
// model 정의
const Ebook = sequelize.define("ebook", {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
allowNull: false,
primaryKey: true,
},
// ... (다른 필드들)
});
Ebook.belongsTo(User);
const INCLUDE_USER = {
attributes: [
"id",
// ...,
[Sequelize.col("user.name"), "name"],
[Sequelize.col("user.username"), "username"],
],
include: {
model: User,
attributes: [],
},
};
const ORDER_DESC = { order: [["createdAt", "DESC"]] };
// 이미지 파일을 읽어서 Base64로 인코딩하는 함수
function encodeImageToBase64(filePath) {
const image = fs.readFileSync(filePath);
return Buffer.from(image).toString("base64");
}
export async function getAll() {
const ebooks = await Ebook.findAll({
...INCLUDE_USER,
...ORDER_DESC,
});
// 각 Ebook의 thumbnail 경로를 Base64로 변환
const ebooksWithBase64Thumbnail = ebooks.map((ebook) => {
const base64Thumbnail = encodeImageToBase64(ebook.thumbnail);
return {
...ebook.dataValues,
thumbnail: base64Thumbnail,
};
});
return ebooksWithBase64Thumbnail;
}
export async function create(
filename,
// ...,
userId
) {
return Ebook.create({
filename,
// ...,
}).then((data) => this.getById(data.dataValues.id));
}
encodeImageToBase64
: 썸네일 이미지를 읽어서 Base64 문자열로 변환하여 클라이언트에서 이미지 파일을 직접 요청하지 않고 데이터 URI 형식으로 이미지를 전달할 수 있게 한다.
마무리
이번 포스팅에서는 Node.js와 Express를 사용하여 파일 업로드 및 클라이언트와의 상호작용을 구현하는 방법에 대해 알아보았다. 파일 업로드를 위해 multer
미들웨어를 사용하고, 클라이언트와의 통신을 위해 JSON 형식의 응답을 사용했다. 또한, 데이터베이스에 파일 정보를 저장하고 관리하기 위해 Sequelize를 사용했다. 이러한 기술들을 조합하여 파일 업로드 및 관리 기능을 구현하는 방법을 살펴보았다.
다음 포스팅에서는 multer
와 node-poppler
라이브러리에 대해 다룰 예정이다.