iOS/흡구오디 -> 어딨쥐

[흡구오디] Swift - Cloud Storage로 사진 업로드/다운로드 하기 | 사진 업로드해서 보여주기

23g 2025. 9. 14.

안녕하세요

 

이제 가을인가봐요

겨울이 오기 전에 이 앱을 출시하겠어요!

(사유 : 전국방방곳곳 뛰댕겨야함)

 

오늘은 유저가 흡연구역을 추가할 때

 

사진을 찍어서 올리고

 

마커를 탭하면 그 사진 + 흡연구역에 대한 정보를 보여주는 

 

기능을 구현해보았답니다 : )

 

사진 업로드/다운로드 과정

먼저 과정 설계를 해야겠쥬

 

파이어스토어는 텍스트/숫자 같은 데이터만 저장할 수 있고, 이미지 저장은 안돼요 🙅
그래서 Cloud Storage를 같이 써야 함!

 

기존 흐름

사용자가 입력 폼 적어서 저장 -> firestore에 장소 이름/장소 설명/장소 태그 등을 저장

사용자가 마커를 탭하면 -> firestore에 있는 장소 이름/장소 설명/장소 태그 등을 불러와서 보여주기

 

앞으로는

사용자가 사진 찍고, 입력 폼 적어서 저장
-> 사진은 Cloud Storage에 업로드,
firestore에 이미지 URL + 이름/설명/태그 등을 저장


사용자가 마커를 탭하면
-> Firestore 데이터 + 이미지 URL 이용해 사진까지 보여주기

 

이렇게 제 3의 저장소에 이미지를 저장하고 그 저장소의 url을 가져와서 이렇게 저렇게 해줘야한다는 말씀!!!

 

저번 포스팅에서 카메라 켜서 사진 찍는거까지 했고

 

이번 포스팅에서는 본격적으로 사진을 서버에 업로드하고 그 사진을 가져와서 보여주는 방법에 대해 알아보겠어요.

 

Cloud Storage 활성화 하기

SDK로 FireStorage 설정해주고

https://github.com/firebase/firebase-ios-sdk

 

GitHub - firebase/firebase-ios-sdk: Firebase SDK for Apple App Development

Firebase SDK for Apple App Development. Contribute to firebase/firebase-ios-sdk development by creating an account on GitHub.

github.com

 

Cloud Storage도 활성화해 주세요

https://firebase.google.com/docs/storage/ios/start?hl=ko&_gl=1*4nyl1p*_up*MQ..*_ga*MTkzNTY2NjAwNy4xNzU3ODQ1ODk4*_ga_CW55HF8NVT*czE3NTc4NDU4OTckbzEkZzAkdDE3NTc4NDU4OTckajYwJGwwJGgw

 

Apple 플랫폼에서 Cloud Storage 시작하기  |  Cloud Storage for Firebase

의견 보내기 Apple 플랫폼에서 Cloud Storage 시작하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Cloud Storage for Firebase를 사용하면 이미지와 동영상 등 사용

firebase.google.com

 

그럼 여기까지 됐다 치고~

 

사진 업로드하기

func uploadImage(_ image: UIImage) {
    guard let imageData = image.jpegData(compressionQuality: 0.8) else { return }
    let storageRef = Storage.storage().reference()
    let fileName = "smokingAreas/\(UUID().uuidString).jpg"
    let imageRef = storageRef.child(fileName)
    
    imageRef.putData(imageData, metadata: nil) { [weak self] _, error in
      if let error = error {
        print("이미지 업로드 실패", error)
        return
      }
      imageRef.downloadURL { url, _ in
        self?.capturedImageUrl = url?.absoluteString
        print("업로드 완료 : ", self?.capturedImageUrl)
      }
    }
  }

핵심 정리

  • 참조(reference): 클라우드 파일 위치 가리키는 포인터
  • child: images/abc.jpg 같은 하위 경로 생성
  • putData: 실제 이미지 업로드
  • downloadURL: 업로드 후 접근 가능한 URL 가져오기

참조 만들기

// Get a reference to the storage service using the default Firebase App
let storage = Storage.storage()

// Create a storage reference from our storage service
let storageRef = storage.reference()

파일 업로드, 다운로드, 삭제, 메타데이터 가져오기 또는 업데이트를 하려면 참조를 만들어줘야 해요

참조는 클라우드의 파일을 가리키는 포인터로 생각하면 됩니다.

참조는 메모리에 부담을 주지 않으므로 원하는 만큼 만들 수 있으며 여러 작업에서 재사용할 수도 있다고 하네요

 

참조 child 만들기

    let fileName = "smokingAreas/\(UUID().uuidString).jpg"
    let imageRef = storageRef.child(fileName)

 

기존 참조에 child 메서드를 사용하여 'images/space.jpg'와 같이 트리에서 하위 위치를 가리키는 참조를 만들 수도 있습니다.

 

저는 file 이름을 위와 같이 만들어서 imageRef를 만들어 줬어요

 

putData

 

공식 문서의 아래 내용 참고해서 만들어줬어요

// Data in memory
let data = Data()

// Create a reference to the file you want to upload
let riversRef = storageRef.child("images/rivers.jpg")

// Upload the file to the path "images/rivers.jpg"
let uploadTask = riversRef.putData(data, metadata: nil) { (metadata, error) in
  guard let metadata = metadata else {
    // Uh-oh, an error occurred!
    return
  }
  // Metadata contains file metadata such as size, content-type.
  let size = metadata.size
  // You can also access to download URL after upload.
  riversRef.downloadURL { (url, error) in
    guard let downloadURL = url else {
      // Uh-oh, an error occurred!
      return
    }
  }
}

 

저희는 위에서 만들어준 이미지 원본을 업로드해줄 것이기 때문에

// 위에서 이 변수 정의 해줘야함 private var capturedImageUrl: String?


imageRef.putData(imageData, metadata: nil) { [weak self] _, error in
      if let error = error {
        print("이미지 업로드 실패", error)
        return
      }
      imageRef.downloadURL { url, _ in
        self?.capturedImageUrl = url?.absoluteString
        print("업로드 완료 : ", self?.capturedImageUrl)
      }
    }

 

이렇게 푸시해줍니다.

 

 

그럼 이렇게 잘 올라오는 것을 확인할 수 있어요

 

그럼 imageURL까지해서 저장하는 코드는

private func didTappedSaveButton() {
    self.saveButton.rx.tap.subscribe(
      onNext: { [weak self] in
        self?.saveSmokinAreaInfo()
        self?.navigationController?.popToRootViewController(animated: true)
      })
    .disposed(by: self.disposeBag)
  }
  
  private func saveSmokinAreaInfo() {
    guard
      let name = self.nameTextField.text, !name.isEmpty,
      let description = self.descriptionTextView.text, !description.isEmpty,
      let lat = self.markerLat,
      let lng = self.markerLng
    else {
      // Alert 같은 걸 띄워서 사용자한테 알려주기
      let alert = UIAlertController(title: "입력 오류", message: "이름, 설명은 필수 입력 항목입니다.", preferredStyle: .alert)
      alert.addAction(UIAlertAction(title: "확인", style: .default))
      self.present(alert, animated: true)
      return
    }
    
    // 모델로 만들기
    let smokingArea = SmokingArea(
      imageURL: capturedImageUrl,
      name: name,
      description: description,
      areaLat: lat,
      areaLng: lng,
      selectedEnvironmentTags: self.selectedEnvironmentTags,
      selectedTypeTags: self.selectedTypeTags,
      selectedFacilityTags: self.selectedFacilityTags
    )
    
    // Firestore 저장
    db.collection("smokingAreas").addDocument(data: smokingArea.asDictionary) { error in
      if let error = error {
        print("저장 실패: \(error.localizedDescription)")
      } else {
        print("저장 성공")
      }
    }
  }
}

 

참고로 모델은

//
//  SmokingArea.swift
//  SmokingAreaOdi
//
//  Created by 이상지 on 8/29/25.
//

import Foundation

struct SmokingArea {
  var imageURL: String? //요거 추가함
  var name: String
  var description: String
  var areaLat: Double
  var areaLng: Double
  var selectedEnvironmentTags: [String]
  var selectedTypeTags: [String]
  var selectedFacilityTags: [String]
  // var uploadUser: String
  // var uploadDate
  
  var asDictionary: [String: Any] {
      return [
        "imageURL": imageURL, //요거 추가함
        "name": name,
        "description": description,
        "areaLat": areaLat,
        "areaLng": areaLng,
        "environmentTags": selectedEnvironmentTags,
        "typeTags": selectedTypeTags,
        "facilityTags": selectedFacilityTags
      ]
    }
}

 

이렇게 구성되어 있음

 

 

 

사진 다운로드(사용하기)

 

기존에는 파이어베이스의 데이터들만 받아서 띄워줬잖아요?

이제는 클라우드 스토리지 사진도 받아줘야한답니다.

public func configure(with data: SmokingArea) {
    DispatchQueue.main.async {
      self.nameLabel.text = data.name
      self.descriptionLabel.text = data.description
      
      //--------여기가 추가됨---------------------------
      // 이미지 로딩 전, 기본 이미지(placeholder) 설정 또는 nil로 초기화
      self.areaImageView.image = nil
      
      self.loadImage(from: data.imageURL)
      //---------------------------------------------
      
      self.tagSections.forEach { $0.removeFromSuperview() }
      self.tagSections.removeAll()
      
      let envSection = self.makeTagSection(title: "환경", tags: data.selectedEnvironmentTags)
      let typeSection = self.makeTagSection(title: "유형", tags: data.selectedTypeTags)
      let facilitySection = self.makeTagSection(title: "시설", tags: data.selectedFacilityTags)
      
      self.tagSections = [envSection, typeSection, facilitySection].filter { !$0.subviews.isEmpty }
      
      for section in self.tagSections {
        self.rootFlexContainer.flex.addItem(section).marginTop(20)
      }
      
      self.rootFlexContainer.flex.layout()
    }
  }

 

진짜진짜 최종

이미지 가져오기

private func loadImage(from urlString: String?) {
      guard let urlString = urlString, let url = URL(string: urlString) else { return }
      
      URLSession.shared.dataTask(with: url) { data, _, _ in
          if let data = data, let image = UIImage(data: data) {
              DispatchQueue.main.async {
                  self.areaImageView.image = image
              }
          }
      }.resume()
  }

 

저는 이렇게 URLSession을 통해 가져왔어요

이렇게 가져온 이미지 데이터는 디스패티 큐 통해 areaImageView에 업데이트 시켜줬어요!

 

이렇게 완성된 최종 모습!

 

내용과 사진은 그냥 아무거나 넣어놨어요

 

왜저리 늦게 뜨는지는 아직 잘 모르겠지만

 

지금 이 포스팅 쓰느라 완전 기졸초풍 직전이라 이만 집에 갈게요

 

그럼 안녕~

댓글