본문 바로가기
iOS/개발

Swift SupaBase SDK로 Storage에 이미지 업로드

by 바등쪼 2023. 3. 14.

최근 진행중인 프로젝트에서 SupaBase를 사용하고 있어서 iOS에서 SupaBase를 이용해 원격 DB를 다루는 작업을 진행하고 있습니다.

SupaBase 자체가 정보가 많지 않은데 특히 iOS에서의 관련 정보가 엄청 부족해서 (사실상 없다고 봐야합니다.) 이것저것 삽질하면서 공부하고 있습니다.

이번에는 Storage에 이미지를 업로드하면서 했던 삽질들과 구현 방법을 공유해보고자 합니다.

 

우선, 2023년 3월 14일 기준 SupaBase의 제대로된 Swift용 Docs는 없습니다...ㅠ

https://github.com/supabase-community/supabase-swift

 

GitHub - supabase-community/supabase-swift: A Swift client for Supabase

A Swift client for Supabase. Contribute to supabase-community/supabase-swift development by creating an account on GitHub.

github.com

이곳이 깃허브 홈페이지인데 insert와 select에 대한 짧은 설명은 있지만 다른 고차원적인 기능들과 Storage관련 내용들은 전무합니다.

https://github.com/supabase-community/storage-swift

 

GitHub - supabase-community/storage-swift: Swift client library to interact with Supabase Storage

Swift client library to interact with Supabase Storage - GitHub - supabase-community/storage-swift: Swift client library to interact with Supabase Storage

github.com

Storage 관련 레포는 이곳인데 리드미가 비어 있고 링크만 하나 달랑 걸려있는데 들어가보면

404가 우리를 반겨줍니다 ㅎㅎ

여기서 포기할수는 없으니 직접 라이브러리를 뜯어보고 삽질을 시작했습니다ㅎㅎ

 

SupaStorageClient 생성

SupaBase의 웹쪽 문서도 보고 예제도 봤지만 iOS랑은 다른 부분이 꽤 많았습니다.

우선 iOS에서는 Storage에 접근할 때, SupabaseStorageClient 인스턴스를 생성하고 이것을 통해 작업을 수행해야합니다.

SupaBase의 기본 DB 테이블에 접근할 때에는 SupabaseClient 인스턴스를 사용해야 했었는데 아직까지는 두 기능이 완벽하게 합쳐지지는 않았나봅니다..!

class SupaStorageClient {
  static let shared = SupabaseStorageClient(url: "\(supabaseUrl)/storage/v1", headers: [
    "Authorization": "Bearer \(supabaseKey)",
    "apikey": supabaseKey,
  ])
  
  private init() {}
}

이런식으로 생성을 해줍니다. 중요한 점!!은 headers를 꼭 넣어야한다는 점입니다!! 그것도 이 예시처럼 그대로 Authorization과 apikey에 정확히 넣어야 합니다. + Bearer도 필수!

url도 supabase에서 발급해준 url 뒤에 "/storage/v1"을 붙여야합니다!

 

그리고 SupaBase 홈페이지에서 Bucket을 생성하고 일단은 Public으로 설정합니다. (권한 및 정책을 지정할 수 있지만 일단은 Public으로 놓고 세팅하는게 마음 편합니다.)

생성한 버킷

이미지 업로드 함수 생성

import Foundation
import SupabaseStorage

// 클래스 만들고 그 안에 아래의 함수 넣기!

func uploadImage(image: Data) async -> Result<String, MyError> {
    guard let userId = SupaSession.shared?.user.id else {
      return .failure(.noSession)
    }
    
    do {
      let fileName = "\(Int(Date().timeIntervalSince1970))" + "_" + UUID().uuidString // 원하시는 이름으로 지정하면 됩니다!
      let path = "/\(userId)/\(fileName).jpg" // path도 뒤에 확장자 제대로 붙이는거 말고는 원하는 경로로!
      let file = File(name: fileName, data: image, fileName: fileName, contentType: "image/jpg")
      let response = try await SupaStorageClient.shared.from(id: "activity-images").upload(path: path, file: file, fileOptions: nil)
      
      guard let result = response as? [String: String] else {
        print("### reponse 딕셔터리 캐스팅 실패")
        return .failure(.etc)
      }
      
      print(result)
      return Result.success(result["Key"] ?? "")
    } catch {
      let error = error as? StorageError
      return Result.failure(error?.message)
    }
  }

이제 이미지를 업로드하는 함수를 구현하면 됩니다. 위의 코드가 그 예시입니다!

SupaSession을 통해 현재 로그인한 유저 정보를 가져옵니다. (이건 필수는 아닙니다.)

fileName을 생성합니다. 원하시는 이름으로 만들면 됩니다.

path를 생성합니다. => 꼭 저장하고자 하는 파일의 확장자와 일치시켜야합니다 ⭐️ path가 단순 경로가 아니라 저장할 파일의 이름까지 결정합니다.

그리고 File 인스턴스를 생성합니다. 여기서 삽질을 좀 했는데 공식 문서가 없다보니까 코드만 보고 fileName과 contentType이 옵셔널인것을 보고 nil을 넣었는데 이렇게 하면 작동을 안하더군요..

File(name:data:filename:contentType) 필드는 가능하면 다 채워주세요! name과 fileName은 그냥 빈 스트링으로 넣어도 작동은 잘 하는 것 같습니다. contentType도 꼭 저장하고자 하는 파일에 알맞게 적어주세요! 이번에는 이미지를 저장하기 때문에 "image/jpg"로 넣었습니다.

File을 생성했으면 이제 이전에 만든 SupaStorageClient에 접근하여 이미지를 업로드합니다. from(id:)을 통해 저장할 버킷을 지정합니다. 그리고 upload(path:file:fileOptions) 에 앞서 만든 객체들을 넣고 업로드합니다!

 

정상적으로 업로드를 했다면 response가 NSDictionary 형태로 넘어옵니다. (Xcode에서의 타입은 Any라고 나옵니다만 런타임에 딕셔너리로 받아오는 것 같습니다.)

이 데이터를 우리가 사용하기 편한 Swift의 Dictionary로 변환하여 result 변수를 생성합니다.

이 result의 value 값이 우리가 업로드한 이미지의 주소의 일부입니다.

풀 주소는 SupaBase Storage 페이지 가서 이미지를 직접 찾아보면 확인해볼 수 있습니다.

 

정리

사실 적고나니 별거 아닌거 같지만.. 정보가 없어서 직접 변수 하나하나 실행시켜보고 디버깅 하면서 방법을 찾았습니다.

upload 할때 path를 꼭 잘 적어야 하고 File 인스턴스 생성을 꼼꼼하게 해야합니다.

혹시 403에러가 발생한다면 Storage의 Policy를 확인해주세요! insert 권한이 없을 확률이 높습니다.

지금까지 문서가 잘되어있고 기존 사용자가 많은 라이브러리를 사용하다가 이렇게 Swift 관련 자료가 적고 생긴지 얼마 안된 라이브러를 사용하다 보니 간단한 작업도 시간이 오래 걸리는 것 같습니다. 그래도 새로운 걸 배우고 문제를 해결해 나가는 과정은 언제나 재미있죠..!!! 혹시 저와 비슷한 이슈를 겪을 분들을 위해 이렇게 글로 남겨보았습니다!

댓글