이미지 업로드 multer 사용 [TOY / SimpleSNS]

알락·2022년 12월 9일
0

배경

원래 기존 프로젝트에서는 이미지 업로드와 다운로드를 편하게 구현하기 위하여 BLOB을 통하여 송수신 하게 만들었다. 그리고 나머지 메시지 데이터와 함께 JSON으로 서버에 보내어 몽고DB에 한꺼번에 저장해버린다. 불러올 때도 마찬가지로 한꺼번에 불러오면 되어서 구현은 편하였다.

사실 프로그램 기능 제공에는 문제가 없게 구현된다. 꼭 서버에 메시지와 이미지를 저장하고 다시 화면에서 확인하는 과정이 문제없이 작동하기 때문이다.

하지만 같이 공부하는 동기 분 중, 이미지 업로드 관련하여 피드백을 주셔서 다른 방법으로 이전을 하려고 했다.


설명

⌞ 안녕, BLOB

[BLOB 형태로 저장되는 이미지]

기존 방법은 데이터베이스에 한꺼번에 데이터들을 저장하는 것이다. 이 말인 즉슨 이미지 파일도 어떻게든 내가 바이너리파일을 우겨서 문자열로 만들어 데이터베이스에 저장하고 있었다는 것이다. 텍스트를 저장하는 것은 용량이 별로 안돼서 읽고 쓰기 작업이 금방 끝난다. 하지만 이미지는 용량이 적어도 1MB 가 넘는 것들이 많다. 이미지를 문자열로 만들었다고 해도, 변환된 이미지 문자열의 크기는 1MB가 넘는다. 이 이미지가 요청될 때마다 데이터베이스에서 빼오는 작업도 해야한다. 조금만 생각해도 텍스트와는 차원이 다른 작업을 하고 있다는 것을 알 수 있다.

[데이터베이스에 저장하는 방식]

그리고 기존 방식은 데이터들을 한꺼번에 클라이언트에게 전송하도록 구현하고 있다. 특히 무한스크롤을 구현하면서 한 번에 5개의 메시지들을 확인하고 있는데, 그렇다면 메시지 5개의 이미지가 모두 들어있다면 위에서 설명한 데이터베이스에서 이미지 빼오기 작업을 한 요청에 5번 하게되는 것이다. 그리고 이렇게 데이터베이스에서 서버로 읽어들여온 메시지 데이터들을 또 한꺼번에 클라이언트에게 전달한다. 한꺼번에 전해지는 데이터 크기가 커져버린다.

⌞ 정적 파일 관리

express 에는 정적파일을 관리하고, 요청이 있을 시 해당 파일을 전송해주는 static 기능이 있다. 해당 파일에 접근하는 방법은 그저 host 주소와 파일의 상대경로로 URL을 통해 요청하면 된다.
또한 multer 라이브러리를 사용하면 클라이언트에 업로드 요청한 이미지 파일을 정적파일을 관리하는 폴더에 저장할 수 있다.

[express static을 이용하는 방법]

그럼 이제 메시지를 저장할 때, 같이 저장된 이미지의 상대 경로를 저장해놓고, 클라이언트의 요청 때 해당 메시지 데이터와 함께 메시지에 포함된 이미지의 상대경로만 전해주기만 하면된다. 그럼 클라이언트 리액트는 메시지 데이터를 바탕으로 메시지 컴포넌트들을 렌더링하면서 필요한 이미지들을 서버에 요청할 것이다. 서버는 그 때서야 진짜 이미지 파일들을 클라이언트에게 전송한다.

이 때 주의할 점은 클라이언트에서 http 요청 Content-Typemulitipart/form-data로 설정되어야 한다는 것이다.


구현

서버

[app.js 설정]

// 생략...
const multer = require("multer");

//업로드 되는 파일의 저장장소와 파일명 규칙 설정
const storage = multer.diskStorage({
    destination: function(req, file, cb){
        cb(null, "./public/images/");
    },
    filename: function(req, file, cb){        
        cb(null, `${crypto.randomBytes(16).toString("base64url")}.${file.mimetype.split("/")[1]}`);
    }
})
// multer upload 미들웨어 생성
const upload = multer({storage});

// 생략...

// static 파일 요청 관리
// public 디렉터리에서 정적 파일들을 모아 관리
app.use(express.static(__dirname + "/public"));

// 이미지 업로드 라우팅 및 multer 미들웨어 사용
app.post("/uploadImage", upload.single("image") ,uploadImage);

// 생략...

위의 코드는 multer 설정과 만들어진 미들웨어를 특정 라우팅에서 사용되게 구현하고 있음을 확인할 수 있다.
이 프로젝트 같은 경우는 public 디렉터리에서 저장된 정적 파일들을 응답으로 보낸다. public 디렉터리는 프로젝트 폴더에 존재해야 한다. 이미지들은 public 디렉터리에 images 라는 디렉터리에서 관리하게 만들어줬다.

이제 어떤 이미지를 요청한다고하면 http://localhost:{포트번호}/images/{이미지이름}으로 요청하면 된다.

[controller.js 구현]

const uploadImage = async(req, res, next)=>{
        const filename = req.file.filename;
        const path = refinePath(req.file.path);

        return res.status(200).send({filename, path});
    }

일단 에러가 날 경우를 논외로하고 이미지 업로드에 대한 컨트롤러를 구현하였다. 최종적으로 이미지가 업로드된 상대경로를 반환한다.
req.file 같은 경우는 위에서 살펴본 upload 미들웨어가 파싱하여 만들어 준 객체이다. 이 때문에 uploadImage 컨트롤러에서 사용할 수 있게 된 것이다.


클라이언트

[Send.js 컴포넌트]

const Send = ({messageId, closeReplyHandler, getMessageSended})=>{
	
  	// 생략...
  
	const [imagePreview, setImagePreview] = useState("");
    const [imageUpload, setImageUpload] = useState("");
  
  	const imageChangeHandler = async (e)=>{
        const filelist = e.target.files;
        const image = filelist[0];
        
        const reader = new FileReader();

        const data = new FormData();
        data.append("image",filelist[0]);

        reader.onload = () => {
            setImagePreview(reader.result);
        }
        reader.readAsDataURL(image);
		
      	//=============================
        //         주목할 부분
      	//=============================
        const result = await axios.post(
            "http://localhost:4000/uploadImage",
            data,
            {headers:{
                "Content-Type": "multipart/form-data;",
                "Accept" : "*/*",
            }}
        ).then(result=>{
            setImageUpload(result.data.path);
        })
     }
    
	  // 생략...
    
    return(
      {/* 생략... */}
      <input 
      	type="file" 
      	className="image-upload-input" 
      	onChange={imageChangeHandler} 
		accept=".jpeg, .jpg, .png, gif"/>
      {/* 생략... */}
    )
    

}

기존 파일 업로드 input을 이용하고 있고, 사실 클라이언트가 이미지 파일을 선택하자마자 자동으로 파일이 Server로 전송되게 만들었다. 그리고 저장된 경로를 반환받는다. 이후 메시지 작성이 완료되면 반환된 경로와 함께 메시지 데이터를 서버에 저장하여 최종적으로 데이터베이스에 저장된다.

지금보니 axios의 헤더의 Accept 설정은 이미지만 보낼 수 있게 수정이 필요할 것 같다.

[message.js 컴포넌트]

// 생략...

{message.image ? 
  	<div className="image-wrapper my-3 w-full h-[300px] rounded-xl border-radius-full relative overflow-hidden">
  		<img className="absolute" src={`http://localhost:4000${message.image}`} onClick={clickImageHandler}/>
	</div> 
: <></>
}

// 생략...

다른 부분은 다 제외하고 이미지 부분 view가 어떻게 수정되었는지만 설명하려고 한다. 현재 파일의 상대경로만 저장하고 있기 때문에 src 어트리뷰트 값에 호스트명을 하드코딩 했다. 이렇게 만들어줘야 최종적으로 클라이언트에서 서버로 http 통신을 통해 이미지를 요청할 수 있다.


정리

[기존 응답 시간]

[현 응답 시간]

기존 방식과 현 방식의 성능 차이를 비교해보려고 한다. 기존 데이터베이스에 이미지를 저장해 빼오는 방식은 처음 메시지 5개를 요청하는데 200ms 가까이 시간이 소요된다.
하지만 현재 이미지들의 상대경로만 응답 받고 이미지들마다 따로 서버에 요청하는 방식은 총 합쳐도 30ms 보다 적게 소요되고 있다.

확실한 성능 차를 보이고 있는 것을 확인할 수 있다.


개선

  • 현재 클라이언트에서 이미지 변경이 있을 때마다 새로운 이미지가 서버에 저장된다. 이는 최종 메시지데이터와 불필요한 이미지 모두 저장하고 있게 되어 개선이 필요하다.
  • 사용자가 늘어나 이미지 파일 개수가 대량으로 늘어나게 되면 static으로 이미지를 제공한다고 하더라도 메시지 렌더링에 필요한 이미지를 찾는데 시간이 소요되게 된다. 이 때 이미지를 빠라게 찾아내는 방식이 있을지 찾아봐야 한다.
  • 사실 이미지 파일들은 클라우드 저장소를 이용해서 URI를 데이터베이스에 저장하는 방식까지 생각하고 있다. 이 방법이라면 바로 위 개선 문제는 해결될 수 있다. 클라우드 저장소를 이용하는 방식도 결국 개선안에 든다.
profile
블록체인 개발 공부 중입니다, 프로그래밍 공부합시다!

0개의 댓글