안드로이드에서 서버로 이미지 전송하기

LeeEunJae·2022년 10월 10일
1

서버에 파일을 전송할 때, 다음과 같이 form-data 를 사용해서 보내는데
이것을 안드로이드에서는 어떻게 구현하는지 알아보도록 하겠습니다.

안드로이드 Retrofit2 에서는 form-data 로 데이터를 전송하기 위한 기능을 제공합니다.

📌 라이브러리 추가 및 매니페스트 설정

implementation 'com.squareup.retrofit2:converter-scalars:2.5.0'
implementation 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:converter-gson:2.1.0'

Retrofit2 를 사용하기 위해서 Gradle 에 라이브러리를 추가 해줍니다.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.INTERNET"/>

인터넷 권한과 갤러리 접근을 위한 권한을 설정합니다.

📌레트로핏 기본 세팅

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitInstance {
    val BASE_URL = "https://386e-119-67-181-215.jp.ngrok.io"

    val client = Retrofit
        .Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    fun getInstance(): Retrofit{
        return client
    }
}

다음과 같이 object 를 생성하고 레트로핏을 빌드 한 객체를 리턴하는 함수를 두면,
레트로핏이 필요한 곳에서 RetrofitInstance.getInstance() 로 사용할 수 있습니다.

📌API

import okhttp3.MultipartBody
import retrofit2.Call
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part

interface MyApi {
    @Multipart
    @POST("/save/image")
    fun sendImage(
        @Part imageFile: MultipartBody.Part
    ): Call<String>
}

@Multipart 어노테이션을 사용해서 File을 보낼 것임을 명시해줍니다.
이미지 파일을 전송할 것이기 때문에 데이터타입은 MultipartBody.Part 로 지정해야 합니다.

@Multipart
@POST("서버경로")
fun profileSend(
    @Part("userId") userId: String,
    @Part imageFile : MultipartBody.Part
): Call<String>

만약 이미지 파일 외에 다른 데이터도 전송하고 싶다면, 다음과 같이 추가해주면 됩니다.

📌MainActivity

// 임포트 생략..
class MainActivity : AppCompatActivity() {
    private val imageResult = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ){ result ->
        if(result.resultCode == Activity.RESULT_OK){
            val imageUri = result.data?.data ?: return@registerForActivityResult

            val file = File(absolutelyPath(imageUri, this))
            val requestFile = RequestBody.create(MediaType.parse("image/*"), file)
            val body = MultipartBody.Part.createFormData("profile", file.name, requestFile)

            Log.d("testt",file.name)

            sendImage(body)

            binding.imageView.setImageURI(imageUri)

        }
    }
    companion object{
        const val REQ_GALLERY = 1
    }
    private val binding by lazy{
        ActivityMainBinding.inflate(layoutInflater)
    }
    private val retrofit = RetrofitInstance.getInstance().create(MyApi::class.java)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        binding.imageBtn.setOnClickListener {
            selectGallery()
        }
    }
    private fun selectGallery(){
        val writePermission = ContextCompat.checkSelfPermission(
            this,
            android.Manifest.permission.WRITE_EXTERNAL_STORAGE
        )
        val readPermission = ContextCompat.checkSelfPermission(
            this,
            android.Manifest.permission.READ_EXTERNAL_STORAGE
        )

        if(writePermission == PackageManager.PERMISSION_DENIED ||
            readPermission == PackageManager.PERMISSION_DENIED){
            ActivityCompat.requestPermissions(
                this,
                arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    android.Manifest.permission.READ_EXTERNAL_STORAGE),
                REQ_GALLERY
            )
        }else{
            val intent = Intent(Intent.ACTION_PICK)
            intent.setDataAndType(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                "image/*"
            )

            imageResult.launch(intent)
        }



    }

    // 절대경로 변환
    fun absolutelyPath(path: Uri?, context : Context): String {
        var proj: Array<String> = arrayOf(MediaStore.Images.Media.DATA)
        var c: Cursor? = context.contentResolver.query(path!!, proj, null, null, null)
        var index = c?.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
        c?.moveToFirst()

        var result = c?.getString(index!!)

        return result!!
    }

    fun sendImage(body: MultipartBody.Part){
        retrofit.sendImage(body).enqueue(object: Callback<String>{
            override fun onResponse(call: Call<String>, response: Response<String>) {
                if(response.isSuccessful){
                    Toast.makeText(this@MainActivity, "이미지 전송 성공", Toast.LENGTH_SHORT).show()
                }else{
                    Toast.makeText(this@MainActivity, "이미지 전송 실패", Toast.LENGTH_SHORT).show()
                }
            }

            override fun onFailure(call: Call<String>, t: Throwable) {
                Log.d("testt", t.message.toString())
            }

        })
    }
}

코드 설명을 덧붙이자면,

val file = File(absolutelyPath(imageUri, this))

absolutelyPath 함수에 갤러리에서 가져온 사진의 uri 를 넣고, 사진의 절대경로를 가져옵니다. 그 절대 경로를 통해 File() 함수로 이미지 파일을 얻고, file 변수에 저장합니다.

val requestFile = RequestBody.create(MediaType.parse("image/*"), file)

다음 구문을 통해 Request 형식으로 바꾸고...

val body = MultipartBody.Part.createFormData("profile", file.name, requestFile)

다음 구문을 통해 form-data 형식으로 바꿔줍니다.

sendImage(body)

그 다음은 sendImage 메서드를 호출해 서버에 이미지를 전달 합니다.

📌 Node.js 서버

https://kong-dev.tistory.com/151

Node.js 서버는 위 링크를 참조해서 만들고, 테스트 했습니다.

const storage = multer.diskStorage({
    destination(req, file, done) { // 이미지를 저장할 경로 지정
      done(null, 'profileImages/');
    },
  

    filename(req, file, done) {
      const ext = path.extname(file.originalname);
      const fileName = `${path.basename(
        file.originalname,
        ext
      )}_${Date.now()}${ext}`;
      done(null, fileName);
    },
  });
  const limits = { fileSize: 5 * 1024 * 1024 };
  
  const multerConfig = {
    storage,
    limits,
  };
  
  const upload = multer(multerConfig);

// 프로필이미지를 서버에 업로드
router.post("/save/image", upload.single('profile'), (req, res)=>{
    const body = req.body
    
    console.log(req.file);
    res.send('upload!');
    
})

upload.single('profile') 에서 profile 은 클라이언트 단에서 form-data 를 생성할 때 지정했던 profile 과 동일 해야합니다.

val body = MultipartBody.Part.createFormData("profile", file.name, requestFile)

서버를 시작하고 클라이언트가 이미지를 전달하면 '서버경로/profileImages/이미지파일' 경로에 전달받은 이미지를 저장하게 됩니다.

👀 참고 자료

https://onedaycodeing.tistory.com/168

profile
매일 조금씩이라도 성장하자

1개의 댓글

comment-user-thumbnail
2023년 7월 4일

안녕하세요. 이 글 보고 코틀린 서버전송 해보려고 하는데 메인액티비티가 실행이 되지않아서 연락남깁니다. 따로 버전이나 뭐맞춰야하는게있나요?

답글 달기