봄날은 갔다. 이제 그 정신으로 공부하자

node.js로 웹서비스 만들기 (21. node.js에서 Cloud Storage로 이미지 업로드하기) 본문

학습

node.js로 웹서비스 만들기 (21. node.js에서 Cloud Storage로 이미지 업로드하기)

길재의 그 정신으로 공부하자 2023. 8. 23. 11:11

이전 글에서 Cloud Storage 버킷 만들고 웹서비스에서 사용할 비밀키까지 다운로드 완료했습니다.

이번 글에서는 이전 글에서 설정한 버킷 정보와 비밀키를 사용해  node.js에서 이미지를 업로드하는 기능을 구현한 것을 기록하도록 하겠습니다.

 

 

1. 프로젝트로 Cloud Storage 비밀 키 이동

프로젝트 루트 폴더에 secure 폴더를 만들고 이전 글에서 다운로드 받은 비밀 키를 복사합니다.

 

2. 작업 전에 이미지 업로드에 필요한 node.js 미들웨어 설치합니다.

  - multer는 이미지를 업로드하는데 사용할 미들웨어

  - multer-google-storage는 multer와 연동해 cloud storage로 파일 업로드하는데 사용하는 미들웨어

> npm install multer
> npm install multer-google-storage

 

3. index.js 파일에 사용을 위해 아래 코드를 추가합니다.

파일 업로드를 위해 index.js 파일에 추가하는 작업은 다음과 같습니다.

  - 파일 업로드를 수행하는 multer를 추가하고

  - 파일 저장 위치를 multer-google-storage를 사용해서 GCP Cloud Storage로 지정한 후 

  - router.post로 업로드 요청 대기 및 업로드 완료 후 응답값 전송

// multer와 multer-google-storage 추가
const multer = require('multer');
const multerGoogleStorage = require('multer-google-storage');

…

// multer와 multer-google-storage를 사용하여 업로드 객체 생성
// multer의 storage로 multer-google-storage를 사용하여 GCP Cloud Storage 사용
//   - bucket: Cloud Storage버킷 이름
//   - projectId: Cloud Storage버킷이 포함된 프로젝트 ID
//   - keyFilename: 위에서 작업한 키파일 경로와 이름
//   - filename: Cloud Storage에 저장될 파일 경로 및 이름
//      “quizimage” 폴더에 저장하고 중복 방지를 위해 현재 시간을 prefix로 추가함.
//   - 업로드 파일 제한 용량: 5MB
const upload = multer({
    storage: multerGoogleStorage.storageEngine({
        bucket: '<Cloud Storage Bucket Name>',
        projectId: 'mygcpfirstsample',
        keyFilename: '<secure 폴더에 저장된 비밀키>',
        filename: (req, file, cb) => {
            cb(null, `quizimage/${Date.now()}_${file.originalname}`);
        },
    }),
    limits: { fileSize: 5*1024*1024},
});

…

// 이미지 업로드 POST가 오면 “upload”를 사용해 이미지를 업로드하고 결과를 받은 후
// 결과가 성공이면 이미지가 저장된 위치를 json 포맷으로 프론트에 전달
router.post('/upload/image', upload.single('file'), (req, res) => {
    console.log('[POST] /upload/image file: ' + JSON.stringify(req.file));
    res.json({filename: req.file.filename});
});

 

req.file를 JSON.stringify(req.file)로 스트링으로 형변환해서 살펴보면 다음과 같습니다.

{
    "fieldname":"file",
    "originalname":"app_icon.png",
    "encoding":"7bit",
    "mimetype":"image/png",
    "path":"https://newquiz_stg.storage.googleapis.com/quizimage/1688377299869_app_icon.png",
    "filename":"quizimage/1688377299869_app_icon.png"
}

path가 cloud storage url 주소까지 포함된 것을 알 수 있는데, 저는 가변성을 고려해 filename을 사용 하였습니다.

 

4. 프론트에 이미지 업로드 기능 구현

이제 백앤드에서 이미지를 받아서 올릴 준비가 되었습니다.

그럼 프론트에서 파일을 백앤드로 파일을 전달하는 기능을 구현해 보도록 하겠습니다.

create_quiz.ejs 파일에 아래와 같이 추가합니다.

<!— form 태그로 아름답게 추가하면 좋은데, 퀴즈 제출하는 화면이다 보니 이것 저것 예외 처리할 
    것도 있고 해서 “제출하기” 버튼 클릭 시 이미지 업로드 되도록 처리했습니다. —>
<div class="input-group">
    <span class="input-group-text" style="width: 90px">사진 첨부</span>
    <input id="inlink" type="file" class="form-control" name="inlink" class="ml-3, mt-1" placeholder="[옵션] 사진을 첨부해주세요." aria-label="Attachment">
    <button type="button" class="btn btn-warning" id="clearFile">삭제하기</button>
</div>

…

<button type="button" class="btn btn-primary w-50" id="done">제출하기</button> 

…

<script>
    <!— 첨부 파일을 제거 —>
    $("#clearFile").click(function() {
        inlink.value  = '';
    });

    <!— 제출하기 버큰 클릭 시 유효성 문제 없는지 확인하고 첨부된 이미지가 있는 경우에,
        ajax를 통해 이미지 업로드 요청하고 이미지 업로드 결과가 성공이면 이지지 filename을
        사용해 퀴즈를 저장하는 addQuiz() 함수 호출  —>
    $("#done").click(function() {
        if (contents.value.length == 0) {
            alert("문제 내용을 입력해주세요.");
         }else if (desc.value.length == 0) {
             alert("정답 설명을 입력해주세요.");
         } else {
             if (inlink.value.length > 0) {
                 const formData = new FormData();
                 formData.append("file", inlink.files[0]);
                 $.ajax({
                     type: "POST",
                     url: "/upload/image",
                     processData: false,
                     contentType: false,
                     data: formData,
                     cache:false,
                     timeout:10000,
                     success: (data, status) => {
                         addQuiz(contents.value, desc.value, data.filename)
                      }, error: (err) => {
                          alert("이미지 업로드 실패: " + err);
                      }
                 })
            } else {
                 addQuiz(contents.value, desc.value, null);
            }
        }
    });

    <!— 퀴즈 등록을 요청하는 함수 —>
    function addQuiz(contents, desc, inlink){
        $.post("/quiz/create_ox_quiz", {
            contents: contents,
            desc: desc,
            inlink: inlink
        }, function(data, status){
          alert("문제 등록: " + status);            
          window.location  = '/quiz';
      }
    });

 

위와 같이 구현한 이유는 퀴즈 제출 시 이미지를 첨부할 때마다 계속 서버에 이미지가 저장되는 것은 낭비 일 것 같아 제출하기 버튼 클릭 시 <input> 태그에 파일이 추가되었는지 확인하고 추가된 경우, 해당 이미지를 서버에 업로드하고 이후 결과를 보고 성공인 경우, filename을 사용해 퀴즈를 저장하도록 처리함.

 

또한 이 코드를 보면 이미지가 첨부된 경우 “제출하기” 버튼 클릭 시 한번, 이미지 업로드 성공 후, 또 한번 합해서 두 번의 POST 호출이 있는데, 이와 같이 처리한 이유는 이미지 업로드와 퀴즈 등록을 다른 프로세스로 구분해서 처리하는게 나을 것 같고 또한 이미지 업로드 실패 시굳이 다른 데이터를 전달할 필요가 없어보여 이와 같이 POST를 두번 호출하도록 처리하였습니다.

 

5. 업로드된 이미지 확인

GCP의 Cloud Storage에 접속해서 업로드한 이미지가 정상적으로 등록되었는지 확인해보도록 하겠습니다.

첫째, 백앤드에서 파일 업로드 json의 path을 브라우저 주소 입력창에 복사 이미지가 다운로드 되면 정상

둘째, GCP Cloud Storage 버킷에 접속해서 quizimage 폴더가 정상적으로 생성(없으면 자동 생성됨.)되었고 이미지가 정상적으로 저장되어 있으면 정상

 

* 이슈:  한글 파일 깨지는 문제 수정

파일 이름이 영어인 경우에는 문제가 없었지만 파일 이름이 한글인 경우에는 Cloud Storage에 저장은 잘 되나 파일 이름이 깨지는 문제가 발생했습니다. 더 큰 문제는 이렇게 깨진 이름 때문에 Cloud Storage 버킷내 상세 폴더에도 접근 불가한 문제가 발생한다는 것이었습니다.

원인 파악을 위해 열심히 구글링해보니 multer 버전이 1.4.5인 경우, 문제가 발생할 수 있어 버전을 1.4.4로 다운그레이드해야 한다고 해서 아래와 같이 버전 다운로드 하니 잘 되네요.

  > npm install multer@^1.4.4

 

 

 

Comments