Next.js,typeScript

Next.js 로컬 다중 파일 업로드

초이진영 2024. 4. 19. 15:28

Next.js : 14.1.4

typeScript : 5

formidable : 3.5.1

버전을 사용합니다.

 

인스타그램처럼 사진을 선택할 당시에 미리 서버에 파일을 저장시키는 방식.

저는 게시판 글작성할 때 사진도 같이 올릴 수 있는 기능.

 

1. 이미지 업로드시 서버 저장

2. 저장 후 저장된 파일 이름 return 받아서 저장해 놓기

3. 글 작성 후 글 등록 버튼 누르면 글 등록할 때 저장해 놓은 파일 이름 같이 저장하기

4. 이미지 업로드 했지만 글작성 안 하고 나갈 시 파일 삭제하기 ( 미구현)

 

 

formidable 이란 라이브러리를 사용할 것이기 때문에 라이브러리부터 설치하기.

 

- npm install formidable

ts를 사용한다면 오류가 발생할 것이다 그러면

declare를 해주던가

npm i --save-dev @types/formidable을 해주면 해결됩니다.

 

formidable 이란?

Node.js에서 사용되는 파일 업로드 및 폼 데이터 파싱을 위한 라이브러리이다.

HTTP 요청에서 multipart 형식의 데이터를 처리하는 데 사용한다.

 

작성한 소스 공유 및 설명은 주석으로 달아놓겠습니다.

 

 

클라이언트 컴포넌트

'use client'

import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth";
import { useState } from "react";

export default function Write() {
    let [imgUrl, setImgUrl] = useState([]);  // 선택한 사진 img url
    let [filenames, setFileNames] = useState([]);  // 선택한 사진 저장한 후 사진 파일명 -> 글 저장시 DB 매핑

    return (
        <>
            <div className="p-20">
                <h4>글 작성</h4>
                <form action="/api/post/new" method="POST">
                    <input name="title" placeholder="글 제목" />
                    <input name="content" placeholder="글 내용" />

                    <input type="file" multiple accept="image/*" onChange={async (e) => {
                        if (!e.target.files) { // null 체크 -> 선택했다가 취소했을경우에도 여기로 들어옴
                            setImgUrl([]);    // imgUrl 변수 초기화
                            setFileNames([]); // filenames 변수 초기화
                            return;
                        }
                        let files = e.target.files; // 업로드 한 파일들
                       
                        if (imgUrl) {
                            imgUrl.map(item => {
                                // 이전에 URL.createObjectURL() 을 사용하여 생성한 URL을 해제하는 메서드
                                URL.revokeObjectURL(item);
                            })

                        }
                        let urls: any = [];  // img 미리보기를 위한 url 생성후 담아놓을 변수

                        const formData = new FormData(); // 서버에 넘길 formData
                       
                        Array.from(files).map((file, idx) => {
                            // img의 URL을 생성해내는 코드  매개변수에는 파일이 들어가야함
                            urls[idx] = URL.createObjectURL(file);  // 미리보기 url 생성
                            formData.append("files",file);   // formData에 append
                        })

                        setImgUrl(urls);   // 미리보기 url들을 state 변수에 저장

                        // 여기까지 서버에 보낼준비 완료


                        // 서버에 보내기
                        await fetch(`/api/post/image`,{   // 각자 서버 컴포넌트 경로 적으시고
                            method : 'POST',
                            body : formData
                        })
                        .then(res =>{
                            return res.json();
                        })
                        .then(result =>{
                            if(result.result){
                                console.log("사진 저장 성공");
                                setFileNames(result.data);  // 저장된 파일 이름 가져와서 state변수에 저장 -> db매핑을 위해
                            }
                        })
                        .catch(error =>{
                            console.log(error);
                        })
                    }}
                    />
                    <button type="submit">등록</button>
                </form>

                {
                    imgUrl && (
                        <>
                            <div style={{ marginTop: "20px" }}>
                                {
                                    imgUrl.map((item,idx) => (
                                        <img className="img" style={{margin:"10px"}} src={item} alt="upload_img"
                                                    width={500} height={300} key={idx} />
                                    ))
                                    // imgUrl로 map 함수를 사용해 사진 띄우기
                                }
                                {
                                  // 사진을 한개만 올리도록 한다면 이렇게 사용
                                 /* <img className="img" src={imgUrl} alt="upload_img" width={500} height={300} /> */
                                }
                            </div>
                        </>
                    )
                }
            </div>
        </>
    )
}

 

클라이언트 끝

 

 

서버 컴포넌트

import { NextApiRequest, NextApiResponse } from "next";
import formidable from "formidable";
import fs from "fs/promises";

 
// bodyParser를 false로 설정하는 이유
// Next.js가 요청 본문을 해석하지 않도록 설정하는 것이다.
// Next.js가 요청 본문을 해석하면 개발자가 원하는대로 파일 저장하는데 문제가 생길 수 있다.
export const config = {
    api: {
        bodyParser: false,
    }
}

// 파일을 저장할 함수 readFile
const readFile = (req: NextApiRequest, saveLocally: boolean) :
                               Promise<{ fields: formidable.Fields; files: formidable.Files }> => {
 
    const options: formidable.Options = {};
    if (saveLocally) { // saveLocally 변수는 로컬에 저장할지 말지 여부를 결정해줄 변수라고 생각
        options.uploadDir = process.env.FILE_UPLOAD_PATH; // 파일 저장 경로 // .env 파일에 변수로 따로 뺴놓음

        options.filename = (name, ext, path, form) => {
            let date = Date.now();
            let filename = "forum" +"_"+ date + "_" + path.originalFilename// 저장할 파일 이름
           // path.originalFilename 에 파일의 원본명이 있다.
            return filename;
        }
        // 이렇게 formidable의 options 설정 끝
    }

    const form = formidable(options);  // 위에서 만든 options으로 formidable 객체 생성

    return new Promise((resolve, reject) => {
        form.parse(req, (err, fields, files) => {  // 파일을 파싱하고 저장까지 해주는 부분 form.parse()
            // console.log(files); 여기에 파일정보들이 들어있고 배열형태로 있다.
            if (err) {
                reject(err);
            }
            resolve({ fields, files })
           
            // 1. form.parse를 실행한다 이때
            // 첫번째 인자는 요청 객체(req)
            // 두번째 인자는 (err,fields,files) 를 인자로 하는 함수
            // 즉  form.parse(req, function(err,fields,files) )  라고 생각
            // form.parse의 첫번째 작업인 req객체를 파싱한다.
            // 이때 성공적으로 파싱이 완료되면 resolve를 실행하며 fields와 files를 넘겨주어 파일들을 저장한다.
            // 파싱에 실패할 경우 reject를 이용하여 err 를 반환한다.

            // 즉 파일을 저장하는건 form.parse()
        })
    })
}


// handler
export default async function Image(req: NextApiRequest, res: NextApiResponse) {
    let file_upload_path = process.env.FILE_UPLOAD_PATH; // 파일 저장경로
        if(!file_upload_path){  // ts때문에 null 체크
            file_upload_path = "";
        }  
    try{
        await fs.readdir(file_upload_path);  // 파일 경로를 읽는다. 이때 경로가 없다면 예외발생
    }catch (error){
        await fs.mkdir(file_upload_path);  //  저장경로를 만들어준다.
    }

    const {files} = await readFile(req,true);  // 위에서 만든 readFile()을 실행한다 req객체와 true 로컬에 저장하겠다는 변수 -> return은 파일객체를 받는다.
    let filenames : any = [];
    files?.files?.map((item,idx) =>{  // 파일객체에서 저장된 파일 이름들을 컴포넌트에 보내주기위해 저장하는 부분
        let date = Date.now();
        let filename = "forum" +"_"+ date + "_" + item.originalFilename;
        filenames[idx] = filename;
    })

    // 저장완료
    return res.status(200).json({result : true, "message" : "파일 업로드 성공" , data : filenames})

}

 

이렇게 하면 로컬에 원하는 경로에 파일을 저장할 수 있다.