이전글에서 image-picker 라이브러리를 사용하여 사용자 앨범에 접근하는 방법에 대해 정리하였다. 

 

이제 image file을 form-data 처리해주어 HTTP(axios | fetch)으로 서버에 전송해야 한다.

처음엔 axios를 사용하였는데 Network Error(Failed to connect)부터 서버쪽에 res.body undefined 까지 여러가지 문제가 발생했다.

 

1.Formdata 처리

image등의 파일을 HTTP 통신으로 전송할때는 반드시 formdata처리를 해주어야 한다.

 

const UploadImage = async() => {
  const image = {
    uri: '',
    type: '',
    name: '',
  };
  await launchImageLibrary({}, (res) => {
    if(res.didCancel){
      console.log('User cancelled image picker');
    }
    else if(res.errorCode){
      console.log('ImagePicker Error: ', res.errorCode);
    }
    else if(res.assets){ //정상적으로 사진을 반환 받았을 때
      console.log('ImagePicker res', res);
      image.name = res.assets[0].fileName;
      image.type = res.assets[0].type;
      image.uri = Platform.OS === 'android' ? res.assets[0].uri : res.assets[0].uri.replace('file://', '');
    }
  })
  const formdata = new FormData();
  formdata.append('ticketImg', image);

마지막줄에 formdata.append에서 'ticketImg' fieldname으로 서버쪽에 multer.single('ticketImg')에 key값과 같은 역할을 하므로 반드시 일치시켜주자.

 

2.Axios HTTP 통신

Content-Type을 multipart/form-data로 설정해준 Hearders를 만들어 axios 내가 만든 헤더를 포함시켜준다.

const UploadImage = async() => {
  const image = {
    uri: '',
    type: 'image/jpeg',
    name: 'test',
  };
  await launchImageLibrary({}, (res) => {
    if(res.didCancel){
      console.log('User cancelled image picker');
    }
    else if(res.errorCode){
      console.log('ImagePicker Error: ', res.errorCode);
    }
    else if(res.assets){ //정상적으로 사진을 반환 받았을 때
      console.log('ImagePicker res', res);
      image.name = res.assets[0].fileName;
      image.type = res.assets[0].type;
      image.uri = Platform.OS === 'android' ? res.assets[0].uri : res.assets[0].uri.replace('file://', '');
    }
  })
  const formdata = new FormData();
  formdata.append('ticketImg', image);
  const headers = {
    'Content-Type': 'multipart/form-data; boundary=someArbitraryUniqueString',
  };
  console.log(image);
  console.log(formdata);

  axios.post("http://localhost:3000/ticket/upload", formdata, {headers: headers})
  .then(response => {
    if(response){
        
      console.log(response.data)
    }
  })
  .catch((error)=> {
    if (error.response) {
      // The request was made and the server responded with a status code
      // that falls out of the range of 2xx
      console.log(error.response.data);
      console.log(error.response.status);
      console.log(error.response.headers);
    } else if (error.request) {
      // The request was made but no response was received
      // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
      // http.ClientRequest in node.js
      console.log(error.request);
    } else {
      // Something happened in setting up the request that triggered an Error
      console.log('Error', error.message);
    }
 })

 

 

3.Network Error(Failed to connect to)

역시 한번에 성공하는 법이 없다...

내경우에는 테스트 환경으로 내 노트북에서 로컬 서버를 구동하고 디바이스 또한 노트북에 연결하여 real device로 앱을 실행했기 때문에 axios에 url을 localhost:3000이 아닌 실제 ipv4 주소를 사용했어야 했다. (에뮬레이터의 경우 10.0.0.2:3000라고 어느 블로그에서 본 기억이 있음)

참고로 터미널에서 ipconfig 명령어를 통해 ipv4주소를 확인할 수 있다.

 

 

또한, 앱을 실행하는 real device가 노트북과 같은 와이파이를 사용해야 한다. 코드 변경한 부분이 없는데 갑자기 또 같은 에러가 발생했고 이때 핸드폰 인터넷을 lte로 바꿔준게 문제인거 같아서 확인해보니 정답이었다!

 

4.Server

먼저 프로젝트에 필요한 모듈들을 설치해주자.

 

1.aws-sdk : npm install --save aws-sdk

2.multer : npm install --save multer

3.multer-s3 : npm install --save multer-s3

 

정상적으로 설치 되어 package.json에서 확인할 수 있다.

 

네트워크 에러를 해결하고 직면한 res.file/res.body undefined 문제를 설명하기 위해 이쯤에서 서버 코드를 소개한다.

// routes/s3_router.js

var express = require('express');
var router = express.Router();
const s3Controller = require('../controller/s3_controller');
const { upload } = require('../services/multer');

router.post('/uploadProfileImg', upload.single('ticketImg'), s3Controller.uploadProfileImgToDb);

module.exports = router;
// config/s3_config.js

const aws = require('aws-sdk');
require('dotenv').config();

const s3 = new aws.S3({
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    region: process.env.AWS_REGION
});

module.exports = s3;
// services/multer.js

const multer = require('multer');
const multerS3 = require('multer-s3');
const s3 = require('../config/s3_config');

const upload = multer ({
    storage: multerS3({
      s3: s3,
      bucket: "ssimille-bucket",
      contentType: multerS3.AUTO_CONTENT_TYPE,
      acl: 'public-read',
      key: (req, file, cb) => {
        //file의 fieldname으로 S3에 저장될 폴더 경로 지정
        cb(null, `${file.fieldname}/${Date.now()}_${file.originalname}`);
      },
    }),
  })

exports.upload = multer(upload); // upload : storageOptions
// controller/s3_controller.js

const { insertProfileImgToDb } = require('../services/promise-mysql');
const myQurey = require('../query/query');

exports.uploadProfileImgToDb = async (req, res) => { //여기서 res로 client에 multer 반환 객체(ex: s3 image url) 전달   
  try {
    console.log('req.file.location: ', req.file.location) //single : req.file, array : req.files 
    await insertProfileImgToDb(myQurey.insertProfileImg, req.file.location)
    res.send(req,file.location) //client에게 s3 이미지 경로 반환
  } catch (error) {
    console.log('Enter error: ', error);    
  }
};

 

5.res.file/res.body undefiend

분명히 postman으로 API를 테스트 했을 때는 S3에 이미지가 정상적으로 업로드 되고 controller에서 console.log를 통해 업로드된 경로를 출력하는데 성공했다. 하지만 client에서 axios로 테스트할땐 네트워크 에러가 뜨지않고 서버에 정상적으로 요청이 됐음에도 불구하고 S3에 업로드 되지 않고 당연히도 controller에서 res.file(postman의 경우)이나 res.body(axios의 경우) 둘다 undefined으로 출력되는 것이다...

이거 때문에 진짜 정말 정말 많은 삽질을 했다(부들부들). header의 Content-type:multipart/form-data 이게 문제인가 이것저것 바꿔서 해보고 formdata에 필요한 다른 요소들이 있는건가 여러 블로그를 참조하며 다른 요소들도 추가해보았지만 해결되지 않았다. 계속해서 레퍼런스를 찾아보던 중 어느 블로그에서 multer 미들웨어가 axios의 formdata body를 파싱하지 못한다는 글을 보고 다른 라이브러를 사용하였다.

Axios 버전이 올라가면서 사용법이 약간 달라졌는데 transformRequest를 사용해주니 해결되었다.

const formdata = new FormData();
    formdata.append('profileImg', {
      uri: profileImgData.path.includes(':')
        ? profileImgData.path
        : 'file://' + profileImgData.path,
      type: profileImgData.mime,
      name: profileImgData.fileName,
    });
    console.log(formdata);
    const result = await axios.put(`${BASE_URL}/s3/profile-image`, formdata, {
      redirect: 'follow',
      headers: {
        'Content-Type': 'multipart/form-data',
      },
      transformRequest: (data, headers) => {
        return data;
      },
    });

 

 

6.Fetch HTTP 통신

const UploadImage = async() => {
  const image = {
    uri: '',
    type: '',
    name: '',
  };
  await launchImageLibrary({}, (res) => {
    if(res.didCancel){
      console.log('User cancelled image picker');
    }
    else if(res.errorCode){ 
      console.log('ImagePicker Error: ', res.errorCode);
    }
    else if(res.assets){ //정상적으로 사진을 반환 받았을 때
      console.log('ImagePicker data: ', res.assets);
      image.name = res.assets[0].fileName;
      image.type = res.assets[0].type;
      image.uri = Platform.OS === 'android' ? res.assets[0].uri : res.assets[0].uri.replace('file://', '');
    }
  })

  const formdata = new FormData();
  formdata.append('ticketImg', image); //key(=fieldname)이 곧 사진이 업로드 될 S3의 폴더 이름임

  const requestOptions = {
    method: 'POST',
    body: formdata,
    redirect: 'follow',
    // headers :{'Content-Type': 'multipart/form-data'} 헤더를 지정해줄거면 multipart/form-data로 지정해주어야함
    // headers를 위처럼 따로 지정해 주지 않아도 되긴 함
  };

  await fetch("http://~~:3000/s3/uploadProfileImg", requestOptions)
  .then(response => response.text())
  .then(result => console.log(result))
  .catch(error => console.log('error', error));

}

fetch로 바꿔주니 성공!!

 

이후 DB에 image url 전송하는 것까지 정상적으로 완료됨을 확인했다.