๐ŸŒ™

[Swift] SplashView - ์Šคํ”Œ๋ž˜์‹œ ํ™”๋ฉด

yeggrrr๐Ÿผ 2025. 9. 28. 19:01
728x90

 



์Šคํ”Œ๋ž˜์‹œ?

์•ฑ์„ ์ฒ˜์Œ ์‹คํ–‰ํ–ˆ์„ ๋•Œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ง€๋Š” ์ฒซ ํ™”๋ฉด์„ '์Šคํ”Œ๋ž˜์‹œ ํ™”๋ฉด(Splash Screen)์ด๋ผ๊ณ  ํ•œ๋‹ค.
๋‹จ์ˆœํžˆ ๋ธŒ๋žœ๋“œ ๋กœ๊ณ ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒฝ์šฐ๋„ ์žˆ๊ณ ,
์ดˆ๊ธฐํ™” ์ž‘์—…์ด๋‚˜ ์›๊ฒฉ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์ „์— ์ž ์‹œ ๋…ธ์ถœ๋˜๋Š” ์šฉ๋„๋กœ ํ™œ์šฉ๋˜๊ธฐ๋„ ํ•œ๋‹ค.
๊ทธ๋Ÿฌ๋‚˜ ๊ตฌํ˜„ ๋ฐฉ์‹์— ๋”ฐ๋ผ UX์™€ ์‹ฌ์‚ฌ ๊ธฐ์ค€์— ์ฐจ์ด๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์˜ฌ๋ฐ”๋ฅธ ๊ตฌํ˜„๋ฒ•์„ ์ •๋ฆฌํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.


โ–ท LaunchScreen
- ์‹œ์Šคํ…œ์—์„œ ์ œ๊ณตํ•˜๋Š” ์ดˆ๊ธฐ ํ™”๋ฉด
- ์•ฑ ์‹คํ–‰ ์งํ›„ ๊ฐ€์žฅ ๋จผ์ € ํ‘œ์‹œ๋˜๋ฉฐ, Xcode์˜ LaunchScreen.storyboard๋ฅผ ํ†ตํ•ด ๊ตฌ์„ฑ
- ์žฅ์ : ๊น”๋”ํ•˜๊ณ  ์‹คํ–‰ ์งํ›„ ํ‘œ์‹œ, iOS ์‹ฌ์‚ฌ ๊ฐ€์ด๋“œ๋ผ์ธ์— ๋ถ€ํ•ฉ
- ๋‹จ์ : ๋™์  ๋กœ์ง ์ ์šฉ ๋ถˆ๊ฐ€ → ๋„คํŠธ์›Œํฌ ์ด๋ฏธ์ง€ ๋กœ๋”ฉ, ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‚ฝ์ž… ๋“ฑ์ด ๋ถˆ๊ฐ€๋Šฅํ•จ

โ–ท Custom SplashView
- ์•ฑ ์‹คํ–‰ ํ›„, rootViewController์—์„œ ์ง์ ‘ ๋„์šฐ๋Š” ViewController
- ์žฅ์ : ๋™์  ์ด๋ฏธ์ง€ ๊ต์ฒด, ์• ๋‹ˆ๋ฉ”์ด์…˜, API ์—ฐ๋™ ๊ฐ€๋Šฅ
- ๋‹จ์ : ์•ฑ ์‹คํ–‰ ์†๋„์— ๋”ฐ๋ผ ํ™”๋ฉด ์ „ํ™˜ ์ง€์—ฐ ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ

์ฆ‰, ๊ณ ์ •์ ์ธ ์Šคํ”Œ๋ž˜์‰ฌ ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•ด์•ผํ•œ๋‹ค๋ฉด? LaunchScreen ์‚ฌ์šฉ
์„œ๋ฒ„๋กœ ๋ถ€ํ„ฐ ๋ฐ›์€ ์ด๋ฏธ์ง€ ๋“ฑ ๋™์ ์ธ ์Šคํ”Œ๋ž˜์‹œ ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•ด์•ผํ•œ๋‹ค๋ฉด? ์ปค์Šคํ…€ ๋ทฐ ๊ตฌ์„ฑ

์ด๋ฒˆ ๊ธ€์—๋Š” 'API ๊ธฐ๋ฐ˜ ๋™์  ์ด๋ฏธ์ง€ ๋กœ๋”ฉ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•'์— ๋Œ€ํ•ด์„œ ์ •๋ฆฌํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.
๊ตฌํ˜„ํ•˜๋ ค๋Š” ๋™์ž‘์€ ์•ฑ ๋ธŒ๋žœ๋“œ ์ด๋ฏธ์ง€๋ฅผ Default๋กœ ์ €์žฅํ•ด๋†“๊ณ ,
์„œ๋ฒ„์—์„œ ์ด๋ฏธ์ง€๋ฅผ ๋ณด๋‚ด์ฃผ๋ฉด ํ•ด๋‹น ์ด๋ฏธ์ง€๋ฅผ ๋‹ค์Œ ์‹คํ–‰๋•Œ๋ถ€ํ„ฐ ๋…ธ์ถœํ•˜๋ ค๊ณ  ํ•œ๋‹ค. (๋งŒ์•ฝ ์—†๋‹ค๋ฉด, ๋‹ค์‹œ Default๋กœ)

ํ๋ฆ„์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

โ‘  ์•ฑ ์‹คํ–‰ → LaunchScreen ๋…ธ์ถœ(์ด๋•Œ ๋ฐฐ๊ฒฝ์ƒ‰์„ ์•ฑ ๋ธŒ๋žœ๋“œ ์ปฌ๋Ÿฌ๋กœ ์ง€์ •ํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœ)
โ‘ก ์ปค์Šคํ…€ SplashView ์ „ํ™˜ ํ›„ Default ์ด๋ฏธ์ง€ ํ‘œ์‹œ → API ํ˜ธ์ถœ → ๋‹ค์Œ ๋ทฐ์— ๋ณด์—ฌ์ค„ ์ด๋ฏธ์ง€ ์ €์žฅ
โ‘ข Duration ๋งŒํผ ๋ณด์—ฌ์ค€ ํ›„, ๋ฉ”์ธ ๋ทฐ(MainViewController)๋กœ ์ „ํ™˜

<๊ตฌํ˜„ ์‹œ ๊ณ ๋ ค์‚ฌํ•ญ>
- ์‹ฌ์‚ฌ ๊ฐ€์ด๋“œ๋ผ์ธ: LaunchScreen์€ ๊ด‘๊ณ ์„ฑ ์ด๋ฏธ์ง€ ์‚ฝ์ž… ๋ถˆ๊ฐ€. ๋‹จ์ˆœ ๋ธŒ๋žœ๋“œ/์•ฑ ์ •์ฒด์„ฑ์„ ๋‚˜ํƒ€๋‚ด์•ผํ•จ. (๊ทธ๋ƒฅ ๋นˆ ํ™”๋ฉด์œผ๋กœ ๋‘ฌ๋„ ๋ฌด๊ด€ํ•จ)
- ์„ฑ๋Šฅ ์ตœ์ ํ™”: API ํ˜ธ์ถœ ๋ฐ ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ๋Š” ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ, ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ธ”๋กœํ‚น ๊ธˆ์ง€.
- Fallback ์ „๋žต: API ์‹คํŒจ ์‹œ์—๋„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์•ฑ์ด ์‹คํ–‰๋˜๋„๋ก ๊ธฐ๋ณธ ์ด๋ฏธ์ง€๋ฅผ ํ•ญ์ƒ ์ค€๋น„ํ•ด์•ผํ•จ.
- ๋ฐฑ์—… ์ •์ฑ…: Application Support์— ์ €์žฅํ•œ ์บ ํŽ˜์ธ์„ฑ ์ด๋ฏธ์ง€๋Š” iCloud ๋ฐฑ์—… ์ œ์™ธ(isExcludedFromBackup)
   ๋กœ ์„ค์ •ํ•ด ์šฉ๋Ÿ‰ ๋ฐ ์‹ฌ์‚ฌ ๋ฆฌ์Šคํฌ๋ฅผ ์ค„์—ฌ์•ผํ•จ
- ์šฉ๋Ÿ‰ ์ œํ•œ: ๋น„์ •์ƒ ๋Œ€์šฉ๋Ÿ‰ ๋ฆฌ์†Œ์Šค ์œ ์ž…์„ ๋ง‰๊ธฐ ์œ„ํ•ด '์ด๋ฏธ์ง€ ์ตœ๋Œ€ ์šฉ๋Ÿ‰'์„ ์ดˆ๊ณผํ•˜๋ฉด ํ๊ธฐํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•จ.

 


์•„๋ž˜ ์˜ˆ์‹œ ์ฝ”๋“œ๋Š” '์„œ๋ฒ„ ์ด๋ฏธ์ง€๋Š” ์ €์žฅํ•ด๋‘์—ˆ๋‹ค๊ฐ€ ๋‹ค์Œ ์‹คํ–‰๋ถ€ํ„ฐ ๋ณด์—ฌ์ฃผ๊ณ , ์‹คํŒจ ๋˜๋Š” ์ด๋ฏธ์ง€๊ฐ€ ์—†๋‹ค๋ฉด ๊ธฐ๋ณธ ์ด๋ฏธ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ' ๊ตฌ์กฐ์ด๋‹ค.

import UIKit

// ์Šคํ”Œ๋ž˜์‹œ ์ด๋ฏธ์ง€ ํŒŒ์ผ๊ณผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ
enum SplashAssetStore {
    private static var baseDir: URL = {
        let dir = try! FileManager.default
            .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
            .appendingPathComponent("splash", isDirectory: true)
        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        return dir
    }()

    private static let fileName = "splash.jpg"
    private static let urlKey   = "splash.lastRemoteURL"
    private static let dateKey  = "splash.savedAt"

    static func loadCachedImage() -> UIImage? {
        let path = baseDir.appendingPathComponent(fileName)
        guard let data = try? Data(contentsOf: path) else { return nil }
        return UIImage(data: data)
    }

    static func save(imageData: Data, remoteURL: URL) throws {
        let path = baseDir.appendingPathComponent(fileName)
        try imageData.write(to: path, options: .atomic)

        UserDefaults.standard.set(remoteURL.absoluteString, forKey: urlKey)
        UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: dateKey)
    }

    static func savedRemoteURL() -> URL? {
        guard let s = UserDefaults.standard.string(forKey: urlKey) else { return nil }
        return URL(string: s)
    }

    static func clear() {
        let path = baseDir.appendingPathComponent(fileName)
        try? FileManager.default.removeItem(at: path)
        UserDefaults.standard.removeObject(forKey: urlKey)
        UserDefaults.standard.removeObject(forKey: dateKey)
    }
}
  • Application Support์— splash.jpg๋ฅผ ์ €์žฅ
  • ํŒŒ์ผ์€ ๋‹ค์Œ ์‹คํ–‰ ๋•Œ ๋กœ๋“œ
  • UserDefaults์—๋Š” ์–ด๋–ค URL์—์„œ ๋‚ด๋ ค์˜จ ์ด๋ฏธ์ง€์ธ์ง€, ์–ธ์ œ ์ €์žฅํ–ˆ๋Š”์ง€ ๊ธฐ๋ก
๋”๋ณด๊ธฐ

- ์•ฑ ์‚ญ์ œ ํ›„์—๋„ ๋‚จ๊ธธ ํ•„์š”๊ฐ€ ์—†๋‹ค๋ฉด Caches ๋””๋ ‰ํ„ฐ๋ฆฌ
- ์ข€ ๋” ์•ˆ์ •์ ์œผ๋กœ ์œ ์ง€ํ•˜๋ ค๋ฉด Application Support ๊ถŒ์žฅ โœ…

Caches ๋””๋ ‰ํ„ฐ๋ฆฌ
- ํŠน์ง•: ์‹œ์Šคํ…œ์ด ํ•„์š”ํ•  ๋•Œ ์ž๋™์œผ๋กœ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋Š” ๊ณต๊ฐ„
- ์‚ฌ์šฉ ๋ชฉ์ : ๋‹ค์‹œ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ(ex. ์ด๋ฏธ์ง€ ์บ์‹œ, API ์‘๋‹ต ์บ์‹œ)
- ์žฅ์ : ๋””์Šคํฌ ๊ณต๊ฐ„์ด ๋ถ€์กฑํ•˜๋ฉด iOS๊ฐ€ ์•Œ์•„์„œ ์ •๋ฆฌํ•ด์ฃผ๋ฏ€๋กœ ๊ด€๋ฆฌ ๋ถ€๋‹ด์ด ์ ์Œ
- ๋‹จ์ : ์•ฑ ์‚ญ์ œ ์ „์—๋„ ์‚ฌ๋ผ์งˆ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— '๊ผญ ์œ ์ง€ํ•ด์•ผ ํ•˜๋Š” ๋ฐ์ดํ„ฐ'๋ฅผ ์ €์žฅํ•˜๊ธฐ์—๋Š” ์ ํ•ฉํ•˜์ง€ ์•Š์Œ
Application Support ๋””๋ ‰ํ„ฐ๋ฆฌ
- ํŠน์ง•: ์•ฑ์ด ์‚ญ์ œ๋˜๊ธฐ ์ „๊นŒ์ง€๋Š” OS๊ฐ€ ์ž„์˜๋กœ ์‚ญ์ œํ•˜์ง€ ์•Š์Œ.
- ์‚ฌ์šฉ ๋ชฉ์ : ์•ฑ ์‹คํ–‰์— ๊ผญ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ, ์•ˆ์ •์ ์œผ๋กœ ๋ณด๊ด€ํ•ด์•ผ ํ•˜๋Š” ์„ค์ • ํŒŒ์ผ์ด๋‚˜ ๋ฆฌ์†Œ์Šค
- ์žฅ์ : ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ง€์šฐ์ง€ ์•Š๋Š” ์ด์ƒ ์•ฑ์ด ์‚ด์•„์žˆ๋Š” ๋™์•ˆ์€ ๋ณด์กด๋จ
- ๋‹จ์ : ๋””์Šคํฌ ๊ณต๊ฐ„์„ ์ฐจ์ง€ํ•˜๋ฏ€๋กœ ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋Š” ์ง์ ‘ ์ •๋ฆฌํ•ด์ค˜์•ผํ•จ

๊ฐœ์ธ์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ณ ์ž ํ•˜๋Š” ๊ตฌ์กฐ๋Š” ํ›„์ž! (์•ฑ ์‚ญ์ œ ์ „๊นŒ์ง€๋Š” ์ €์žฅ๋˜๋„๋ก ํ•˜๊ณ  ์‹ถ์Œ.)
- ์ด๋ฏธ์ง€ ํŒŒ์ผ์€ Application Support์— ์ €์žฅ
- ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(์ด๋ฏธ์ง€ URL, ์ €์žฅ ์‹œ๊ฐ, ๋…ธ์ถœ Duration ๋“ฑ)์€ UserDefaults์— ์ €์žฅ

import Foundation
import UIKit

struct SplashPayload: Decodable {
    let imageUrl: URL?
}

// ์„œ๋ฒ„์—์„œ ์Šคํ”Œ๋ž˜์‹œ ์ด๋ฏธ์ง€ URL ๋ฐ›์•„์˜ค๊ธฐ + ์‹ค์ œ ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ
enum SplashService {
    static func fetchPayload() async throws -> SplashPayload {
        let url = URL(string: "https://yegr.com/api/splash")!
        var req = URLRequest(url: url)
        req.timeoutInterval = 5
        let (data, resp) = try await URLSession.shared.data(for: req)
        guard let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
            throw URLError(.badServerResponse)
        }
        return try JSONDecoder().decode(SplashPayload.self, from: data)
    }

    static func downloadImageData(from url: URL) async throws -> Data {
        let (data, resp) = try await URLSession.shared.data(from: url)
        guard let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
            throw URLError(.badServerResponse)
        }
        guard UIImage(data: data) != nil else {
            throw URLError(.cannotDecodeRawData)
        }
        return data
    }
}
  • fetchPayload()๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„์—์„œ imageUrl์„ ๋ฐ›์•„์˜ด
  • downloadImageData(from:)์€ ์‹ค์ œ ์ด๋ฏธ์ง€๋ฅผ ๋‚ด๋ ค๋ฐ›๊ณ  ์œ ํšจ์„ฑ ๊ฒ€์ฆ๊นŒ์ง€ ์ˆ˜ํ–‰ํ•จ
import UIKit
import SnapKit

final class SplashViewController: UIViewController {
    private let imageView = UIImageView()
    private let defaultImage: UIImage
    private let minimumDisplaySeconds: TimeInterval

    init(defaultImage: UIImage, minimumDisplaySeconds: TimeInterval = 2.0) {
        self.defaultImage = defaultImage
        self.minimumDisplaySeconds = minimumDisplaySeconds
        super.init(nibName: nil, bundle: nil)
    }
    required init?(coder: NSCoder) { fatalError("init(coder:) not implemented") }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .yegrBrandColor // ๋ธŒ๋žœ๋“œ ์ปฌ๋Ÿฌ๋กœ ์ง€์ •!
        imageView.contentMode = .scaleAspectFit
        view.addSubview(imageView)
        
        imageView.snp.makeConstraints {
            $0.center.equalToSuperview()
            $0.width.lessThanOrEqualTo(view.snp.width).multipliedBy(0.6)
            $0.height.lessThanOrEqualTo(view.snp.height).multipliedBy(0.6)
        }

        imageView.image = SplashAssetStore.loadCachedImage() ?? defaultImage

        Task { await prefetchNextRunImage() }
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        Task { @MainActor in
            try? await Task.sleep(nanoseconds: UInt64(minimumDisplaySeconds * 1_000_000_000))
            transitionToMain()
        }
    }

    @MainActor
    private func transitionToMain() {
        let main = MainViewController()
        let window = view.window ?? UIApplication.shared.connectedScenes
            .compactMap { ($0 as? UIWindowScene)?.keyWindow }
            .first
        window?.rootViewController = main
        window?.makeKeyAndVisible()

        let t = CATransition()
        t.type = .fade
        t.duration = 0.25
        window?.layer.add(t, forKey: kCATransition)
    }

    // ์„œ๋ฒ„์—์„œ ์ƒˆ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›์œผ๋ฉด ํŒŒ์ผ๋กœ ์ €์žฅ
    private func prefetchNextRunImage() async {
        do {
            let payload = try await SplashService.fetchPayload()
            guard let url = payload.imageUrl else { return } // ์ด๋ฏธ์ง€ ์—†์Œ → ํ˜„์ƒ ์œ ์ง€

            if SplashAssetStore.savedRemoteURL() == url { return }

            let data = try await SplashService.downloadImageData(from: url)
            try SplashAssetStore.save(imageData: data, remoteURL: url) // ๋‹ค์Œ ์‹คํ–‰๋ถ€ํ„ฐ ์‚ฌ์šฉ
        } catch {
	        print("์ด๋ฏธ์ง€ ๋ฐ›๊ธฐ ์‹คํŒจ!(์‹คํ–‰์— ์˜ํ–ฅ์—†์Œ ๊ธฐ๋ณธ/์บ์‹œ ์ด๋ฏธ์ง€๋กœ ํ‘œ์‹œ๋จ)")
        }
    }
}
  • ์‹คํ–‰ → ์ฆ‰์‹œ ์บ์‹œ ์ด๋ฏธ์ง€ ํ˜น์€ Default ์ด๋ฏธ์ง€ ํ‘œ์‹œ
  • ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์„œ๋ฒ„ ํ˜ธ์ถœ → ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ ์„ฑ๊ณต ์‹œ ํŒŒ์ผ ์ €์žฅ
  • ์ €์žฅ๋œ ์ด๋ฏธ์ง€๋Š” ๋‹ค์Œ ์‹คํ–‰ ์‹œ ์ฆ‰์‹œ ํ‘œ์‹œ
  • ์‹คํŒจ/์ด๋ฏธ์ง€ ์—†์Œ์ด๋ฉด ๊ทธ๋ƒฅ ๋„˜์–ด๊ฐ€๋„๋ก ๊ตฌํ˜„
import UIKit

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene,
               willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {
        guard let ws = scene as? UIWindowScene else { return }
        let window = UIWindow(windowScene: ws)

        // ์—์…‹์— ๋„ฃ์–ด๋‘” ๋ธŒ๋žœ๋“œ ๊ธฐ๋ณธ ์ด๋ฏธ์ง€ ์‚ฌ์šฉ
        let splashVC = SplashViewController(defaultImage: UIImage(named: "splash_default")!,
                                            minimumDisplaySeconds: 2.0)
        window.rootViewController = splashVC
        window.makeKeyAndVisible()
        self.window = window
    }
}

์œ„์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•จ์œผ๋กœ์„œ, LaunchScreen๊ณผ Custom SplashView์˜ ์ฐจ์ด๋ฅผ ์ •๋ฆฌํ•˜๊ณ ,
API ๊ธฐ๋ฐ˜ ๋™์  ์Šคํ”Œ๋ž˜์‹œ ํ™”๋ฉด์„ ์–ด๋–ป๊ฒŒ ์„ค๊ณ„ํ•˜๋ฉด ์ข‹์„์ง€ ๊ณ ๋ฏผํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

(์‚ฌ์‹ค ํšŒ์‚ฌ์—์„œ ์š”์ฒญํ–ˆ๋˜ ์‚ฌํ•ญ์ด๋ผ ๊ณ ๋ฏผํ•˜๊ฒŒ๋จ.
๋‹จ์ˆœํžˆ ๊ทธ๋ƒฅ ์ €์žฅํ•˜๊ณ  ๋‹ค์Œ ์‹คํ–‰์— ์ƒˆ๋กœ์šด ์ด๋ฏธ์ง€ ๋ณด์—ฌ์ฃผ๋ฉด ๋˜๊ฒ ์ง€~ ์ •๋„๋กœ๋งŒ ์ƒ๊ฐํ–ˆ๋‹ค๊ฐ€,
๋ง‰์ƒ ๊ตฌ์กฐ๋ฅผ ์งœ๋‹ค๋ณด๋‹ˆ ๋‹จ์ˆœํ•˜์ง€ ์•Š์€ ๋ถ€๋ถ„์ด ๋งŽ๋‹ค๋Š” ๊ฒƒ์„ ๊นจ๋‹ฌ์Œ..
๊ทธ๋ž˜์„œ API ๋งŒ๋“ค์–ด์ง€๊ธฐ ์ „์— ๋ฏธ๋ฆฌ ์ •๋ฆฌํ•ด๋‘๋ฉด ์ข‹๊ฒ ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๊ณ ,
์ด๋ฒˆ ๊ธ€์„ ๋ธ”๋กœ๊ทธ ์ฃผ์ œ๋กœ ์‚ผ๊ฒŒ ๋˜์—ˆ๋‹ค๋Š”..๐Ÿฅน)

๋!!!!


728x90

'๐ŸŒ™' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Swift] KeychainManager  (0) 2025.09.21
[Swift] Keychain  (0) 2025.09.14