Skip to content
/ BuyOrNot Public

๐Ÿ† SeSAC x Memolease iOS 4๊ธฐ Light Service Level Project(LSLP) ์ˆ˜์ƒ์ž‘

Notifications You must be signed in to change notification settings

UngQ/BuyOrNot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

67 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ•น๏ธย Buy Or Not (์‚ด๊นŒ์š”? ๋ง๊นŒ์š”?)

  • ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„: 2024. 04. 13. ~ 2024. 05. 05.
  • ์ˆ˜์ƒ ๋‚ด์—ญ: SeSAC x Memolease iOS 4๊ธฐ Light Service Level Project(LSLP) ๊ฒฝ์ง„๋Œ€ํšŒ 3๋“ฑ

Apple iPhone 11 Pro Max Presentation

๐Ÿ—’๏ธย Introduction

  • ์‚ด์ง€ ๋ง์ง€ ๊ณ ๋ฏผ๋˜๋Š” ํŒจ์…˜ ์•„์ดํ…œ์„ ๊ณต์œ ํ•˜์—ฌ ํ•จ๊ป˜ ํˆฌํ‘œํ•œ ๊ฒฐ๊ณผ๋กœ ์‡ผํ•‘ ๊ฒฐ์ •์— ๋„์›€์ด ๋  ์ˆ˜ ์žˆ๋Š” ์–ดํ”Œ
  • Configuration: iOS 16.0+

๐Ÿ—’๏ธย Features

  • ํšŒ์›๊ฐ€์ž… ๋กœ์ง ๊ตฌํ˜„ (์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋“ฑ ์ •๊ทœ์‹ ๊ฒ€์‚ฌ) ๋ฐ ์ˆ˜์ •, ํƒˆํ‡ด, ๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ

  • ํฌ์ŠคํŠธ ์—…๋กœ๋“œ / ์‚ญ์ œ / ์กฐํšŒ

  • ํฌ์ŠคํŠธ ์ข‹์•„์š” / ์‹ซ์–ด์š”

  • ํ•ด์‹œํƒœ๊ทธ ๊ฒ€์ƒ‰ / ํŠน์ • ๊ฒŒ์‹œ๋ฌผ ์กฐํšŒ

  • ๋Œ“๊ธ€ ์ž‘์„ฑ / ์ˆ˜์ • / ์‚ญ์ œ / ์กฐํšŒ

  • ํ”„๋กœํ•„ ์กฐํšŒ ( ํŒ”๋กœ์šฐ, ํŒ”๋กœ์›Œ / ์ž‘์„ฑํ•œ ๊ฒŒ์‹œ๋ฌผ / ์ข‹์•„์š”ํ•œ ๊ฒŒ์‹œ๋ฌผ / ์‹ซ์–ด์š”ํ•œ ๊ฒŒ์‹œ๋ฌผ / ๊ฒฐ์ œํ•œ ๊ฒŒ์‹œ๋ฌผ ์กฐํšŒ )

  • ํŒ”๋กœ์šฐ / ์–ธํŒ”๋กœ์šฐ

  • PortOne ์ด์šฉํ•œ PG๊ฒฐ์ œ

  • 1:1 ์ฑ„ํŒ…๋ฐฉ ๊ฐœ์„ค / ๋‚ด ์ฑ„ํŒ…๋ฐฉ ์กฐํšŒ / ์ฑ„ํŒ…๋ฐฉ ๋Œ€ํ™” ๋‚ด์—ญ ์กฐํšŒ / ์†Œ์ผ“ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ๋Œ€ํ™” (Updated at 2024. 05. 24.)

๐Ÿ—’๏ธย Technology Stack

  • Framework

    • UIKit (RxSwift ๊ธฐ๋ฐ˜)
  • Pattern

    • MVVM
      • UI์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜์—ฌ ์œ ์ง€๋ณด์ˆ˜์™€ ํ…Œ์ŠคํŠธ๊ฐ€ ์šฉ์ดํ•˜๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด MVVM ํŒจํ„ด์„ ์‚ฌ์šฉ
  • Library

    • ๋น„๋™๊ธฐ ๋ฐ ๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ

      • RxSwift
        • ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ๊ณผ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ์žˆ์–ด์„œ ํšจ์œจ์ ์ด๊ณ  ๊ฐ„๊ฒฐํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ์„ ์œ„ํ•ด ์‚ฌ์šฉ
      • RxGesture
        • RxSwift์™€ ์—ฐ๋™ํ•˜์—ฌ, ์ œ์Šค์ฒ˜ ์ธ์‹์„ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
    • ๋„คํŠธ์›Œํฌ

      • Alamofire
        • ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ๊ฐ„ํŽธํ•˜๊ณ  ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
    • ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ

      • Kingfisher
        • ์ด๋ฏธ์ง€๋ฅผ ๋‹ค์šด๋กœ๋“œํ•˜๊ณ  ์บ์‹ฑํ•˜๋Š” ์ž‘์—…์„ ๊ฐ„ํŽธํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
    • ๋ณด์•ˆ ๋ฐ ์ธ์ฆ

      • KeychainSwift
        • ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅํ•˜๊ณ  ์ž๋™ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
    • ๊ฒฐ์ œ

      • iamport-ios
        • ๊ฒฐ์ œ ๊ธฐ๋Šฅ์„ ์‰ฝ๊ฒŒ ํ†ตํ•ฉํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
    • ์• ๋‹ˆ๋ฉ”์ด์…˜

      • Lottie
        • ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
    • UI ๊ตฌ์„ฑ

      • Tabman
        • ์ง๊ด€์ ์ด๊ณ  ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์šด ํƒญ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
      • Snapkit
        • ์˜คํ†  ๋ ˆ์ด์•„์›ƒ์„ ์ฝ”๋“œ๋กœ ์‰ฝ๊ฒŒ ์ž‘์„ฑํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
      • Toast
        • ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฐ„๋‹จํ•œ ํŒ์—… ๋ฉ”์‹œ์ง€๋ฅผ ํ‘œ์‹œํ•˜์—ฌ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ง๊ด€์ ์œผ๋กœ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
      • IQKeyboardManager
        • ํ‚ค๋ณด๋“œ๊ฐ€ ๋‚˜ํƒ€๋‚  ๋•Œ UI๋ฅผ ์ž๋™์œผ๋กœ ์กฐ์ •ํ•˜์—ฌ ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ์„ ์œ„ํ•ด ์‚ฌ์šฉ

๐Ÿ’ฌ Description

1. ImageView์— RxGesture ์ ์šฉ, Double Tapped์‹œ ์ข‹์•„์š”๐Ÿ‘๐Ÿป/์‹ซ์–ด์š”๐Ÿ‘Ž๐Ÿป

keyfeature.mp4
  • ์ด๋ฏธ์ง€์˜ ๊ฐ€์šด๋ฐ x์ขŒํ‘œ๋ฅผ 0์œผ๋กœ ๊ฐ€์ •, ์™ผ์ชฝ ์˜์—ญ(x < 0)์„ ๋”๋ธ”ํƒญํ•˜๋ฉด "์ข‹์•„์š”", ์˜ค๋ฅธ์ชฝ ์˜์—ญ(x > 0)์„ ๋”๋ธ”ํƒญํ•˜๋ฉด "์‹ซ์–ด์š”" ๊ธฐ๋Šฅ์ด ๋™์ž‘ํ•˜๋„๋ก ๊ตฌํ˜„

  • ๋™์ผํ•œ ์˜์—ญ์— ๋‹ค์‹œ ํˆฌํ‘œํ•  ๊ฒฝ์šฐ ๊ธฐ์กด ํˆฌํ‘œ๋ฅผ ์ทจ์†Œ

    ์ฃผ์š”์ฝ”๋“œ
    cell.postImageView.rx.tapGesture(configuration: { gestureRecognizer, delegate in
      			gestureRecognizer.numberOfTapsRequired = 2 })
      		.when(.recognized)
      		.subscribe(onNext: { [weak self] gesture in
      			let touchPoint = gesture.location(in: gesture.view)
      			if let width = gesture.view?.bounds.width {
      				if touchPoint.x < width / 2 {
      					likeButtonTapped.onNext(row)
      					self?.playAppropriateAnimation(for: "like", likeCondition: cell.like, dislikeCondition: cell.dislike)
      				} else {
      					disLikeButtonTapped.onNext(row)
      					self?.playAppropriateAnimation(for: "dislike", likeCondition: cell.like, dislikeCondition: cell.dislike)
      				}
      			}
      		})
      		.disposed(by: cell.disposeBag)

2. MVVM์˜ ํšจ๊ณผ์ ์ธ ํ™œ์šฉ๋„๋ฅผ ๋†’์ด๊ธฐ ์œ„ํ•œ ViewModel Protocl ์ •์˜

  • ViewModel์˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜ํ•˜์—ฌ ์ผ๊ด€๋œ ๊ตฌ์กฐ ์œ ์ง€

  • Input, Output ํƒ€์ž…์„ ํ†ตํ•˜์—ฌ ์ž…, ์ถœ๋ ฅ ๋ช…ํ™•

  • RxSwift์˜ disposeBag์„ ์‚ฌ์šฉํ•œ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ

    ์ฃผ์š”์ฝ”๋“œ
    protocol ViewModelType {
    
      associatedtype Input
      associatedtype Output
    
      var disposeBag: DisposeBag { get set }
    
      func transform(input: Input) -> Output
    }

3. Router Pattern ์ ์šฉํ•˜์—ฌ, ํšจ๊ณผ์ ์œผ๋กœ 30๊ฐœ ์ด์ƒ์˜ API ํ†ต์‹  ๊ด€๋ฆฌ

  • TargetType ํ”„๋กœํ† ์ฝœ๊ณผ Router ์—ด๊ฑฐํ˜•์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์–‘ํ•œ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ •์˜ํ•˜๊ณ , ๊ฐ ์š”์ฒญ์˜ ์„ธ๋ถ€ ์‚ฌํ•ญ์„ ์„ค์ •

  • ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ํ•˜๋‚˜์˜ Router ์—ด๊ฑฐํ˜•์œผ๋กœ ๊ด€๋ฆฌํ•จ์œผ๋กœ์จ ์ฝ”๋“œ์˜ ๋ชจ๋“ˆํ™”์™€ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ž„

  • ์ƒˆ๋กœ์šด API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ Router ์—ด๊ฑฐํ˜•์— ์ƒˆ๋กœ์šด ์ผ€์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ํ•„์š”ํ•œ ์†์„ฑ์„ ์ •์˜ํ•˜๋ฉด ๋˜๋ฏ€๋กœ, ํ™•์žฅ์„ฑ์ด ๋›ฐ์–ด๋‚จ

    ์ฃผ์š”์ฝ”๋“œ
    protocol TargetType: URLRequestConvertible {
      var baseURL: String { get }
      var method: HTTPMethod { get }
      var path: String { get }
      var header: [String: String] { get }
      var parameters: String? { get }
      var queryItems: [URLQueryItem]? { get }
      var body: Data? { get }
    }
    
    enum Router {
      case tokenRefresh
    }
    
    extension Router: TargetType {
        ...
    }

4. RxSwift์˜ retry(when:)์„ ์ด์šฉํ•œ ํ† ํฐ ๊ฐฑ์‹  ๋ฐ ํ†ต์‹  ์žฌ์‹œ๋„

  • ๋„คํŠธ์›Œํฌ ์š”์ฒญ์ด AccessToken ๋งŒ๋ฃŒ ์˜ค๋ฅ˜์ธ HTTP ์ƒํƒœ ์ฝ”๋“œ 419๋กœ ์‹คํŒจํ•  ๊ฒฝ์šฐ, RefreshToken์„ ์ด์šฉํ•˜์—ฌ ํ† ํฐ์„ ๊ฐฑ์‹ ํ•˜๊ณ  ์›๋ž˜์˜ ์š”์ฒญ์„ ๋‹ค์‹œ ์‹œ๋„

    ์ฃผ์š”์ฝ”๋“œ
    static func performRequest<T: Decodable>(route: Router, decodingType: T.Type?) -> Single<T> {
      return Single<T>.create { single in
          do {
              let urlRequest = try route.asURLRequest()
    
              AF.request(urlRequest)
                  .validate(statusCode: 200..<300)
                  .responseDecodable(of: T.self) { response in
                      switch response.result {
                      case .success(let result):
                          single(.success(result))
                      case .failure(let error):
                          single(.failure(error))
                      }
                  }
          } catch {
              single(.failure(error))
          }
    
          return Disposables.create()
      }
      .retry(when: { errors in
          errors.flatMap { error -> Single<Void> in
              guard let afError = error as? AFError, afError.responseCode == 419 else {
                  throw error
              }
              return refreshToken().flatMap { _ in
                  performRequest(route: route, decodingType: T.self).map { _ in Void() }
              }
          }
      })
    }
    • performRequest<T: Decodable>: ์ฃผ์–ด์ง„ ๋ผ์šฐํŠธ์— ๋”ฐ๋ผ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•˜๊ณ , ๊ฒฐ๊ณผ๋ฅผ Single๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    • ์š”์ฒญ์ด ์‹คํŒจํ•˜๋ฉด retry(when:) ์—ฐ์‚ฐ์ž๊ฐ€ ์‹คํ–‰๋˜์–ด ์˜ค๋ฅ˜๋ฅผ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.
    • ์˜ค๋ฅ˜๊ฐ€ HTTP 419 ์ƒํƒœ ์ฝ”๋“œ์ธ ๊ฒฝ์šฐ refreshToken() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ† ํฐ์„ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.
    • ํ† ํฐ์ด ๊ฐฑ์‹ ๋˜๋ฉด ์›๋ž˜์˜ ์š”์ฒญ์„ ๋‹ค์‹œ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.
    • refreshToken(): ํ† ํฐ ๊ฐฑ์‹ ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฉ”์„œ๋“œ๋กœ, ์„ฑ๊ณต์ ์œผ๋กœ ํ† ํฐ์ด ๊ฐฑ์‹ ๋˜๋ฉด Single๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ์žฌ์‹œ๋„๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ์‹คํŒจํ•˜๋ฉด ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ ์žฌ์‹œ๋„๋ฅผ ์ค‘๋‹จํ•˜๊ณ  ๋กœ๊ทธ์ธ ์ฐฝ์œผ๋กœ ์ด๋™ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

5. SocketIO ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ๊ตฌํ˜„

  • ์†Œ์ผ“ ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ์ผ๊ด€๋˜๊ฒŒ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•˜์—ฌ Singleton Pattern ์ ์šฉ

    ์ฃผ์š”์ฝ”๋“œ
    final class SocketIOManager {
    
      static var shared: SocketIOManager = SocketIOManager()
    
      var manager: SocketManager?
      var socket: SocketIOClient?
    
      let baseURL = URL(string: "\(APIKey.baseURL.rawValue)/v1")!
    
      var receivedChatData = PassthroughSubject<ChatContentModel, Never>()
    
      private init() {}
    
      func fetchSocket(roomId: String) {
      	manager = SocketManager(socketURL: baseURL, config: [.log(true), .compress])
    
      	socket = manager?.socket(forNamespace: "/chats-\(roomId)")
    
      	socket?.on(clientEvent: .connect) { data, ack in
      		print("socket connected", data, ack)
      	}
    
      	socket?.on(clientEvent: .disconnect) { data, ack in
      		print("socket disconnected", data, ack)
      	}
    
      	socket?.on("chat") { dataArray, ack in
      		print("chat received", dataArray, ack )
    
      		if let data = dataArray.first {
      			 do {
    					let result = try JSONSerialization.data(withJSONObject: data)
           				let decodedData = try JSONDecoder().decode(ChatContentModel.self, from: result)
    					self.receivedChatData.send(decodedData)
        				} catch {
              				print(error.localizedDescription)
         				}
      		}
      	}
      }
    
      func establishConnection() {
      	socket?.connect()
      }
    
      func leaveConnection() {
      	socket?.disconnect()
      }
    
    }

6. @propertyWrapper๋ฅผ ํ™œ์šฉํ•œ UserDefaults ์บก์Šํ™”

  • ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” UserDefaults ์˜ ์ƒ์šฉ๊ตฌ ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ณ  ๊ฐ€๋…์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•˜์—ฌ ๊ตฌํ˜„

  • @propertyWrapper ์†์„ฑ์„ ์ ์šฉํ•œ MyDefaults ๊ตฌ์กฐ์ฒด ์ƒ์„ฑ ํ›„ UserDefaultsManager ์ •์˜

    ์ฃผ์š”์ฝ”๋“œ
    @propertyWrapper
    struct MyDefaults<T> {
      let key: String
      let defaultValue: T
    
      var wrappedValue: T {
          get {
              UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
          }
          set {
              UserDefaults.standard.setValue(newValue, forKey: key)
          }
      }
    }
    
    enum UserDefaultsManager {
    
      enum Key: String {
          case userId
          ...
      }
    
      @MyDefaults(key: Key.userId.rawValue, defaultValue: "")
      static var userId: String
      ...
    }
    
    //๊ธฐ์กด ์‚ฌ์šฉ๋ฒ•
    UserDefaults.standard.string(forKey: UserDefaultsKey.userId.key) ?? ""
    
    //๊ฐœ์„ ๋œ ์‚ฌ์šฉ๋ฒ•
    let myId = UserDefaultsManager.userId //get
    UserDefaultsManager.userId = "๋ณ€๊ฒฝ๋œ๋‹‰๋„ค์ž„" //set

7. Reactive๋ฅผ extensionํ•˜์—ฌ, Cursor-based Pagination ๊ตฌํ˜„

  • Base๊ฐ€ UIScrollView ํƒ€์ž…์ธ ๊ฒฝ์šฐ, UIScrollView๊ฐ€ ๋ฐ”๋‹ฅ์—์„œ 400 ํฌ์ธํŠธ์— ๋„๋‹ฌํ•  ๋•Œ๋งˆ๋‹ค ์ด๋ฒคํŠธ๋ฅผ ๋ฐฉ์ถœํ•˜๋Š” reachedBottom ํ”„๋กœํผํ‹ฐ๋ฅผ ์ •์˜

    ์ฃผ์š”์ฝ”๋“œ
    extension Reactive where Base: UIScrollView {
      var reachedBottom: Observable<Void> {
      	return contentOffset
      		.debounce(.milliseconds(100), scheduler: MainScheduler.instance)
      		.flatMap { [weak base] _ -> Observable<Void> in
      			guard let scrollView = base else { return .empty() }
      			let contentHeight = scrollView.contentSize.height
      			let scrollViewHeight = scrollView.bounds.size.height
      			let scrollPosition = scrollView.contentOffset.y + scrollViewHeight
      			let threshold = contentHeight - 400
      			if scrollPosition >= threshold {
      				return .just(())
      			} else {
      				return .empty()
      			}
      		}
      }
    }

๐ŸŽฎย ์ฃผ์š”๊ธฐ๋Šฅ UI

แ„’แ…ฌแ„‹แ…ฏแ†ซแ„€แ…กแ„‹แ…ตแ†ธ-แ„แ…กแ†ฏแ„แ…ฌ แ„Œแ…กแ„ƒแ…ฉแ†ผแ„…แ…ฉแ„€แ…ณแ„‹แ…ตแ†ซ แ„‘แ…ณแ„…แ…ฉแ„‘แ…ตแ†ฏแ„‰แ…ฎแ„Œแ…ฅแ†ผ แ„€แ…ฆแ„‰แ…ตแ„€แ…ณแ†ฏแ„Œแ…กแ†จแ„‰แ…ฅแ†ผ-แ„‰แ…กแ†จแ„Œแ…ฆ
ํšŒ์›๊ฐ€์ž… ~ ํƒˆํ‡ด Keychain ํ™œ์šฉํ•œ ์ž๋™๋กœ๊ทธ์ธ ํ”„๋กœํ•„ ์ˆ˜์ • ํฌ์ŠคํŠธ CRUD
Simulator Screen Recording - iPhone 15 Pro - 2024-05-05 at 11 05 03 แ„‚แ…ขแ„‘แ…ณแ„…แ…ฉแ„‘แ…ตแ†ฏ แ„ƒแ…กแ„…แ…ณแ†ซแ„‘แ…ณแ„…แ…ฉแ„‘แ…ตแ†ฏ แ„‘แ…กแ†ฏแ„…แ…ฉแ„‹แ…ฎ
ํฌ์ŠคํŠธ ์ข‹์•„์š”/์‹ซ์–ด์š” ๋‚ด ํ”„๋กœํ•„ ์กฐํšŒ
(๋‚ด ๊ฒŒ์‹œ๊ธ€, ์ข‹์•„์š”/์‹ซ์–ด์š”ํ•œ ๊ฒŒ์‹œ๊ธ€, ๊ฒฐ์ œ๋‚ด์—ญ ์กฐํšŒ)
๋‹ค๋ฅธ ์œ ์ € ํ”„๋กœํ•„ ์กฐํšŒ ํŒ”๋กœ์šฐ
แ„แ…กแ„แ…ฆแ„€แ…ฉแ„…แ…ตแ„Œแ…ฉแ„’แ…ฌ-แ„‰แ…กแ„‹แ…ญแ†ผแ„Œแ…กแ„‡แ…งแ†ฏแ„Œแ…ฉแ„’แ…ฌ แ„ƒแ…ขแ†บแ„€แ…ณแ†ฏ Simulator Screen Recording - iPhone 15 Pro - 2024-05-05 at 20 07 20
๊ฒŒ์‹œ๋ฌผ ์กฐํšŒ ๋Œ“๊ธ€ ์ž‘์„ฑ, ์‚ญ์ œ, ์ˆ˜์ • ๊ฒฐ์ œ ๊ธฐ๋Šฅ

- ์ฑ„ํŒ… ๊ธฐ๋Šฅ ( Updated at 2024. 05. 24. )

Simulator Screen Recording - iPhone 15 Pro - 2024-05-24 at 12 38 06 Simulator Screen Recording - iPhone 15 Pro - 2024-05-24 at 12 38 45
๋‚ด ์ฑ„ํŒ…๋ฐฉ ๋ชฉ๋ก ๋‹ค๋ฅธ์œ ์ € ํ”„๋กœํ•„์—์„œ ์ฑ„ํŒ…๋ฐฉ ์ง„์ž…

๐Ÿ“ธ ํ”„๋กœ์ ํŠธ ๋ฐœํ‘œ ๋ฐ ์ˆ˜์ƒ์‹ ๊ธฐ๋…์‚ฌ์ง„

IMG_8219 IMG_8279

About

๐Ÿ† SeSAC x Memolease iOS 4๊ธฐ Light Service Level Project(LSLP) ์ˆ˜์ƒ์ž‘

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages