logo
Published on

React Native 웹뷰에서 이미지 저장하기

Authors
  • avatar
    Name
    MJ
    Twitter

웹에서 이미지 저장 기능이 있는 경우

피어나서비스를 개발하면서 웹에서 이미지를 저장해야 하는 기능이 필요했습니다. 하지만, 웹뷰앱으로 개발하다 보니 앱에서는 다운로드할 수 없는 문제가 생겼습니다.

해결 과정

시도에 앞서 제약조건은 다음과 같았습니다.

  1. Webview 컴포넌트에서 동작이 이루어질 것
  2. Webview의 postMessage와 onMessage로 동작할 것
  3. AOS와 iOS에서 정상적으로 동작할 것

웹뷰 서비스이기 때문에 Webview 컴포넌트의 의존성을 벗어나기는 힘들었습니다. React Native단에서 다운로드를 구현할 수 있었지만, 웹으로 개발하여 개발 생산성을 올리고자 했던 목표와 벗어났기 때문에 Web에서의 이벤트를 RN에서도 공유받기를 원했습니다. 그래서 웹에서의 구현을 앱에서도 동작할 수 있게 만들어야 했습니다.

blob 방식

// Web
fetch(FLOWER_CARD, { cache: 'no-cache' })
  .then((response) => response.blob())
  .then((blob) => {
    const href = window.URL.createObjectURL(blob)
    const link = document.createElement('a')
    link.href = href
    link.download = FLOWER_CARD
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    toast.success('이미지가 저장됐어요!')
  })

blob 링크를 만들어 이동시키면 이미지를 다운로드 받지 않을까?라는 생각에 초기에 웹도 blob방식으로 개발하였습니다. 일단 AOS, iOS 둘 다 blob 링크로 이동하여 다운로드가 동작하지 않았기 때문에 링크 자체로 이동하여 파일을 다운로드 받는 방식은 사용할 수 없었습니다. 따라서, blob 데이터를 FileReader로 읽어 Web -> App으로 전송하여 base64 문자열로 인코딩해서 전송하였습니다

base64 문자열 media

// Web
const reader = new FileReader()
reader.onloadend = () => {
  WebviewBridge.postMessage({
    type: 'card-image',
    data: reader.result,
  })
}
reader.readAsDataURL(blob)

// App Webview.onMessage
// iOS: success, AOS fail
await CameraRoll.saveAsset(base64Image)

base64 문자열을 이미지로 저장할 수 있는 라이브러리를 찾다보니 @react-native-camera-roll/camera-roll을 찾게 되었습니다.

iOS에서는 다른 변환과정없이 정말 쉽게 base64 문자열을 이미지로 저장할 수 있었습니다. 문제는 AOS에서 발생했는데, 버그의 원인을 좀처럼 잡을 수 없었습니다.

그렇게 Github 문서를 읽던 중 다음과 같은 글을 발견했습니다. 원인은, android에서는 save 함수는 file:///../file.[png]형식으로 로컬 시스템에 있는 이미지를 저장할 수 있다는 것이었습니다. 그래서 base64를 react-native-fs 라이브러리를 통해 로컬 디렉토리에 저장하였고, 저장된 이미지를 CameraRoll 라이브러리를 통해 갤러리에 저장할 수 있게 구현하였습니다.

// App Webview.onMessage
// AOS
const imageData = base64Image.split(';base64,').pop()
const filePath = `${RNFS.CachesDirectoryPath}/temp_image_${new Date().getTime()}.jpg`
await RNFS.writeFile(filePath, imageData!, 'base64')
await CameraRoll.saveAsset(`file://${filePath}`, { type: 'photo' })
// 이미지 저장 후 다시 로컬에서 지워준다.
await RNFS.unlink(filePath)

구현 중 주의사항

@react-native-camera-roll/camera-roll 안드로이드 설정

React Native에서 linking이 되는 버전들의 경우에는(^RN0.69) 공식 문서의 설정을 하지 않아도 됩니다.

Android Permission

Android API >= 33인 경우, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO를 요청하고 Android API < 33인 경우, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE를 요청해야 합니다.

추가로

웹뷰에서 userAgent에 따라 로직을 분기한 경우가 있는데, 개발자 도구 Dimensions을 iPhone이나 Android 기기로 바꾸면 userAgent도 함께 바뀌기 때문에 테스트하기 용이해집니다.