[#67] Multer 파일 업로드 + 게시글 작성/조회

2025. 5. 2. 15:23·LG 유플러스 유레카 SW/React

많은 파일을 안전하게, 효율적으로 관리하기 위해 업로드된 파일을 특정 폴더에 저장하고 파일 이름을 변경해서 저장

  • 파일명 충돌 방지
  • 보안 및 추적성 확보

➡️ multer 사용

 

Multer

Express에서 multipart/form-data 형식을 처리하기 위한 미들웨어
  • multipart/form-data는 일반적인 body-parser나 express.json()으로 파싱되지 않음
  • multer는 파일을 읽어서 서버 디렉토리에 저장하거나 메모리에 올려줌
  • 저장 경로, 파일 이름, 확장자 검사, 파일 크기 제한 등 커스터마이징이 쉬움

 

설치

npm install multer

 

업로드할 디렉토리가 없다면 생성

import multer from "multer";
import path from "path";
import fs from "fs";

// 업로드할 디렉토리가 없으면 생성
const uploadDir = "uploads";
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);

 

diskStorage 설정

  • 파일을 디스크에 저장하려면, Multer의 diskStorage를 사용
  • diskStorage는 파일이 저장될 경로와 저장될 파일의 이름을 정의
const storage = multer.diskStorage({
  // 파일이 저장될 경로 정의
  destination: function (req, file, cb) {
    cb(null, "uploads/"); // "uploads/" 폴더에 저장
  },
  // 파일이 저장될 이름 정의
  filename: (req, file, cb) => {
    // 랜덤한 파일명 생성
    const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
    cb(null, uniqueSuffix + path.extname(file.originalname)); // 고유한 파일명 생성
  },
});

 

Multer 미들웨어 설정

const upload = multer({ storage }); // storage 설정을 이용해서 파일 업로드를 처리할 미들웨어 생성

 

게시글 작성 API 구현

  • upload.single("files"): files라는 필드로 들어온 단일 파일을 처리. 업로드된 파일은 req.file에 저장
app.post(
  "/postWrite",
  upload.single("files"), // 폼 데이터의 "files" 필드로 들어온 단일 파일을 처리하고, req.file에 저장
  async (req, res) => {
    try {
      const token = req.cookies.token;
      if (!token) return res.json({ error: "로그인이 필요합니다." });
      const userInfo = jwt.verify(token, secretKey);

      const { title, summary, content } = req.body;
      const postData = {
        title,
        summary,
        content,
        cover: req.file ? req.file.path : null,
        author: userInfo.userName,
      };
      await postModel.create(postData);
      res.json({ message: "게시글 작성 완료" });
    } catch (err) {
      return res.status(500).json({ error: "서버 에러" });
    }
  }
);
더보기

MongoDB에 문서를 생성하고 저장하는 방법

항목 Model.create() new Model() + .save()
문서 생성과 저장 한 줄로 문서 생성 + 저장 모델 인스턴스를 생성하고, 이후 명시적으로 저장
코드 길이 짧고 간결 더 유연한 조작 가능
중간 데이터 조작 불가능 가능
사용 예 간단한 insert 비밀번호 해시처럼 가공·검증 로직이 있는 경우 사용

 

게시글 목록/상세 조회

// 게시글 목록 조회
app.get("/postList", async (req, res) => {
  try {
    const posts = await postModel
      .find()
      .sort({
        createdAt: -1, // 최신순 정렬
      })
      .limit(3); // 개수 설정
    res.json(posts);
  } catch (err) {
    return res.status(500).json({ error: "게시글 목록 조회 실패" });
  }
});

// 게시글 상세 조회
app.get("/postDetail/:postId", async (req, res) => {
  try {
    const { postId } = req.params;
    const post = await postModel.findById(postId);
    if (!post)
      return res.status(404).json({ error: "게시글을 찾을 수 없습니다." });
    res.json(post);
  } catch (err) {
    return res.status(500).json({ error: "게시글 상세 조회 실패" });
  }
});

 

이미지 경로 처리

프론트엔드에서 이미지 경로를 <img src="/uploads/파일명">처럼 사용하기 위한 백엔드 설정

import { fileURLToPath } from "url";

// ES 모듈 환경에서 __dirname과 __filename을 사용하기 위해 변환
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// uploads 폴더를 정적 파일 경로로 설정하여 '/uploads/파일명'으로 접근할 수 있게 함 (이미지 등 정적 파일 제공 목적)
app.use("/uploads", express.static(path.join(__dirname, "uploads")));

// 특정 파일 요청 시 해당 파일을 직접 응답으로 전송
// CORS 오류를 방지하고 프론트엔드에서 이미지 접근이 가능하도록 처리
app.get("uploads/:filename", (req, res) => {
  const { filename } = req.params;
  res.sendFile(path.join(__dirname, "uploads", filename));
});

 

 

dangerouslySetInnerHTML

React에서 HTML 문자열을 DOM에 직접 삽입할 때 사용하는 특별한 prop

const htmlString = "<p>안녕하세요</p>";

return <div dangerouslySetInnerHTML={{ __html: htmlString }} />;

⚠️ 왜 "dangerously"라는 이름이 붙었는가 ?

  • HTML에 사용자 입력이 포함되면 XSS 공격에 취약할 수 있음
  • 반드시 신뢰할 수 있는 데이터에만 사용해야 함

 

DOMPurify

XSS 공격을 방지하기 위해 HTML을 안전하게 정화(sanitize)해주는 라이브러리

  • dangerouslySetInnerHTML처럼 HTML 문자열을 DOM에 직접 삽입할 때, 사용자 입력이나 외부 데이터에 <script> 같은 악성 코드가 포함될 수 있어 XSS 공격 위험
  • <script>, onerror, onclick 등의 위험한 요소가 제거됨
npm install dompurify
import DOMPurify from 'dompurify';

const htmlString = '<img src=x onerror="alert(`해킹됨`)">';
const cleanHTML = DOMPurify.sanitize(htmlString);

return <div className={css.content} dangerouslySetInnerHTML={{ __html: cleanHtml }}></div>;
728x90
반응형

'LG 유플러스 유레카 SW > React' 카테고리의 다른 글

[#66] 비밀번호 암호화 + JWT 토큰 발급 (feat. 로그인)  (2) 2025.05.01
[#65] React + Express + MongoDB 연동하기 (feat. 회원가입)  (3) 2025.04.30
[#64] TanStack Query(React Query) 사용해보기  (0) 2025.04.29
[#63] 날씨 오픈 API 연동  (1) 2025.04.28
[#62] 용돈 기입장 프로젝트 (feat. React)  (0) 2025.04.28
'LG 유플러스 유레카 SW/React' 카테고리의 다른 글
  • [#66] 비밀번호 암호화 + JWT 토큰 발급 (feat. 로그인)
  • [#65] React + Express + MongoDB 연동하기 (feat. 회원가입)
  • [#64] TanStack Query(React Query) 사용해보기
  • [#63] 날씨 오픈 API 연동
nueos
nueos
  • nueos
    nueos 공부 기록
    nueos
  • 전체
    오늘
    어제
    • 분류 전체보기 (191)
      • 해커톤 (1)
      • 네이버 BoostCamp (6)
      • LG 유플러스 유레카 SW (3)
        • React (21)
        • TypeScript (2)
        • JavaScript (2)
        • HTML+CSS (5)
        • Spring (7)
        • Java (6)
        • SQL (2)
        • Algorithm (8)
        • CX (6)
        • Git (2)
        • 프로젝트 (2)
        • 스터디 (9)
        • 과제 (8)
        • 특강 (1)
      • React (3)
      • Next (0)
      • Javascript (2)
      • HTML (2)
      • CSS (9)
      • Algorithm (6)
      • Database (0)
      • OS (13)
      • C++ (24)
      • Python (1)
      • jQuery (1)
      • Django (1)
      • Git (1)
      • 개발 지식 (3)
      • 정보 보안 (22)
      • 포렌식 (1)
      • 암호 (2)
      • 기타 (4)
      • 패스트캠퍼스 FE 프로젝트십 (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    heap
    큐
    디지랩챌린지
    힙
    스택
    기술로바꾸는세상
    제주해커톤
    Stack
    Queue
    완전 탐색
    디지털혁신
    제주지역혁신플랫폼지능형서비스사업단
    exhaustive search
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
nueos
[#67] Multer 파일 업로드 + 게시글 작성/조회
상단으로

티스토리툴바