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})
}
이렇게 하면 로컬에 원하는 경로에 파일을 저장할 수 있다.