이번에 이미지 업로드 하는 부분을 리팩토링 하게 되었다. 기존에는 모바일 기기에 있던 이미지를 base64로 변환해 업로드하면 서버에서 그 데이터를 받아 리사이즈하고 그 데이터를 DB에 저장하는 방식을 사용했었다.
서버에서 직접 이미지 데이터를 받아서 DB에 저장하고 리사이즈하고 하는 방식은 리소스가 많이 들고 오래 걸리다 보니 효율이 좋지 않아 이 부분을 개선하기로 했다.
Base64는 이진 데이터를 ASCII 문자로 인코딩하는 방식 중 하나인데 이진 데이터를 텍스트 형식으로 변환하여 다른 시스템이나 프로토콜 간에 데이터를 안전하게 전송하거나 저장할 수 있게 해 준다.
base64로 인코딩해서 보낼 경우 텍스트 형식이기 때문에 텍스트 데이터를 전송 데이터로 취급하는 프로토콜들이 많아 범용적으로 사용되고 이진 데이터 보다 손상될 확률이 적어 데이터의 안전성은 올라가지만 데이터가 커지고 받은 서버에서도 디코딩해서 사용하여야 하기 때문에 용량의 증가와 처리 부하로 인한 효율이 떨어진다.
그래서 개선방법은 이미지 업로드와 관련된 작업을 서버에서 AWS S3로 이동시키는 방식이다. 클라우드 기반의 객체 스토리지 서비스 AWS S3를 사용하여 getURL을 사용하여 이미지를 클라이언트에게 전달하고, putURL을 사용하여 클라이언트가 이미지를 업로드할 수 있도록 한다.
클라이언트는 직접 이미지를 S3에 업로드하고, 서버는 이미지 데이터를 직접 저장하지 않고 S3의 URL만을 DB에 저장한다. 이렇게 함으로써 서버의 부하를 줄이고, 클라이언트와 S3 간의 직접 통신을 통해 빠르고 안정적으로 이미지를 처리할 수 있다.
더 나아가 클라이언트가 이미지를 사용할 때, 필요한 크기로 리사이즈할 수 있는데, DB에 저장된 getURL에 필요한 리사이즈 옵션을 추가하여 쿼리로 전달하면, S3는 해당 옵션에 따라 이미지를 리사이즈한 후 클라이언트에게 전달해주기 때문에 이를 통해 서버에서 이미지를 동적으로 리사이즈하는 번거로움을 피할 수 있다.
백엔드에서 이미지 업로드를 할 때 이미지 데이터를 base64 대신 이진 데이터로 전송해 달라는 요청을 받았다. 이런저런 방법을 찾아보다가 간단하게 구현하는 방법을 찾았는데 fetch를 사용하는 것이었다.
fetch({uri : fileURI})
이렇게 로컬에 이미지의 위치를 fileURI로 넘겨주면 fetch를 통해 이미지 데이터가 이진데이터로 변환되어 잘 전송이 되는 것 같았다. 이렇게 모든 게 순탄하게 넘어갈 줄 알았었는데... 개발에 버그를 빼면 섭섭하지!
Android에서 이상한 버그가 발생했다. IOS에서는 발생하지 않는데 안드로이드에서 업로드를 한번 하고 나서 곧바로 다시 한번 이미지를 업로드하려면 'TypeError: Network request failed' 에러가 뜨면서 실패하는데 2~3초 지난 후에 다시 시도하면 문제없이 작동한다.
한번 업로드 후에 2~3초가 지난 후에 다시 시도하면 또 문제없이 작동하는데 곧바로 하면 안 되고 희한하게 10장의 이미지를 한꺼번에 하면 또 문제없이 되는데 한 장을 업로드하고 다시 하려고 하면 또 안 되는 버그였다.
이것저것 찾아보니
이런 정체 모르는 에러가 났는데 해결 방법들을 다 따라 해봤지만 해결되지 않았다. 여기서 말하는 Flipper도 사용하지 않는데 fetch 자체가 요청되지 않는 버그였다. 버그 내용도 없고 'TypeError: Network request failed'라고만 뜨는데 RN의 자체 버그가 아닐지 예상해 본다.
그래서 우선 fetch를 사용해서 이미지 업로드를 하는 방법을 백엔드 팀원 분과 파다가 fetch를 통해 데이터를 변환하는 방법을 알게 되어서 다행히 해결하게 되었다.
const imageFile = await fetch(fileUri);
const imageBlob = await imageFile.blob();
await fetch(urls.putUrl, {
method: 'PUT',
headers: { 'Content-Type': fileData.contentType },
body: imageBlob,
});
fetch에 로컬 이미지의 path를 넣어 자동으로 이진 데이터를 변환해 사용하는 것이 아니라 내가 직접 fetch로 로컬 이미지의 path를 fetch 해서 blob() 메서드를 통해 blob 형태로 변환하여 body에 넣어 PUT을 하니 버그가 더 이상 발생하지 않았다. 로컬 스토리지 file uri도 fetch가 가능한 것을 이번에 처음 알았다. 생각보다 fetch의 기능이 많아 또 한 번 놀랐다! React Native에서는 데이터를 blob 형태로 변환하기가 어려운데 fetch를 사용하니 이렇게 쉽게 변환할 수 있다니! MDN의 fetch 링크도 공유하니 한번 읽어보면 언젠가 쓸 일이 생기지 않을까?
fetch에서 더 나아가 더 좋은 방법을 찾게 되었다. (팀장님의 능력!)
그것은 바로 react-native-blob-util 라이브러리이다.
왜 더 좋냐? 그 이유는 쉽게 찾을 수 있었다.
우선 파일을 읽고 쓰는 등의 작업을 간편하게 처리할 수 있는 API를 제공해 주기 때문에 파일을 읽고 쓰는 메서드, 파일 크기 및 MIME 유형 확인, 파일 경로 추출 등의 기능을 간단하게 수행할 수 있다고 한다. 파일을 읽고 쓰는데 보통 react-native-fs 라이브러리를 사용하는데 react-native-blob-util를 사용하면 다른 것들을 추가적으로 사용할 필요가 없다 이 말이다.
대용량 파일에 대한 스트림 처리를 지원해서 이를 통해 메모리 부하를 최소화하고 성능을 향상할 수 있다. 예를 들면 큰 파일을 조각으로 나누어 처리하거나, 파일의 일부분만 읽거나 쓸 수 있어 효율적인 파일 처리가 가능하다고 하는데 이 외에도 정말 많은 장점들이 있었다.
BASE64 Bridging 없이 저장소로부터 데이터 직접 전송하고 Native와 React Native 사이의 JS Bridging의 성능 손실을 줄이기 위한 네이티브 간 파일 조작 API를 제공하고 안드로이드 같은 경우에는 Android Media Store에 데이터 액세스 및 쓰기가 가능하다 등의 장점과 더 많은 장점들이 있었는데 정말 이게 다 된다면
react-native-blob-util은 React Native 환경에 특화되어 있어서, RN의 네이티브 모듈과의 통합이 용이해서 Blob과 File을 다룰 때에는 무조건 이 라이브러리를 사용하면 좋을 것 같다는 생각이 팍! 팍! 들었다.
이런 이유들로 react-native-blob-util 사용하여 다음 코드로 정말 기능을 완성했다.
await ReactNativeBlobUtil.fetch(
'PUT',
presigendURLs.putUrl,
{
'Content-Type': fileData.contentType,
},
ReactNativeBlobUtil.wrap(fileUri.substring(7)),
);
이렇게 짧은 코드이지만 우여곡절이 있었고 나은 해결 방법을 찾아보다가 정말 만족스러운 라이브러리를 찾아서 어려움을 잘 해결하게 되었다. 물론 나 혼자만의 힘은 아니지만 동료들의 도움을 받아서 만족스러운 결과를 얻게 되어 기쁘다!