/MOGAKCO

๐ŸŒฑ SLP : ๋ชจ๊ฐ์ฝ”

Primary LanguageSwift

๋ชจ๊ฐ์ฝ”

๐ŸŒฑ ๋ชจ๊ฐ์ฝ”, ์œ„์น˜ ๊ธฐ๋ฐ˜ ์Šคํ„ฐ๋”” ๋ฉ”์ดํŠธ ๋งค์นญ ์„œ๋น„์Šค

  • ๊ฐœ๋ฐœ๊ธฐ๊ฐ„ : 2022.11.07 ~ 2022.01.11

  • SSAC 2๊ธฐ Service Level Project์˜ ์ผํ™˜์œผ๋กœ 1์ธ iOS ๊ฐœ๋ฐœ

  • 1์ธ iOS ๊ฐœ๋ฐœ ๋‹ด๋‹น / ๊ธฐํš, ๋””์ž์ธ, ์„œ๋ฒ„๋Š” ๊ต์œก๊ณผ์ •์„ ํ†ตํ•œ ์ œ๊ณต

  • Swagger, Confulence, Figma, Notion, Zep ์‚ฌ์šฉ



๐ŸŒฑ ๋ชจ๊ฐ์ฝ” ํ™”๋ฉด ๋ฐ ๊ธฐ๋Šฅ ์†Œ๊ฐœ


์˜จ๋ณด๋”ฉ ์ดˆ๊ธฐ์„ค์ • ํ™ˆ ๊ฒ€์ƒ‰
IMG_5711 IMG_5712 IMG_5715 IMG_5680
์š”์ฒญ ์ˆ˜๋ฝ ์ฑ„ํŒ… ๋‚ด์ •๋ณด
IMG_5681 IMG_4918 IMG_853658CCAC24-1 IMG_5717

  • ๋ฌธ์ž์ธ์ฆ ๊ธฐ๋ฐ˜ ํšŒ์›๊ฐ€์ž… ๋ฐ ํƒˆํ‡ด

  • ์‹ค์‹œ๊ฐ„ ์œ„์น˜ ๊ธฐ๋ฐ˜ ์ฃผ๋ณ€ ์Šคํ„ฐ๋””๋ฉ”์ดํŠธ ํƒ์ƒ‰

  • ์Šคํ„ฐ๋”” ๊ธฐ๋ฐ˜ ์Šคํ„ฐ๋””๋ฉ”์ดํŠธ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ

  • ์Šคํ„ฐ๋””๋ฉ”์ดํŠธ ์š”์ฒญ/์ˆ˜๋ฝ ๊ธฐ๋Šฅ

  • ์ผ๋Œ€์ผ ์ฑ„ํŒ…

  • ๋ฆฌ๋ทฐ ๋‚จ๊ธฐ๊ธฐ

  • ๋‚ด ์ •๋ณด ๊ด€๋ฆฌ



๐ŸŒฑ ๋ชจ๊ฐ์ฝ” ๊ธฐ์ˆ  ์Šคํƒ ์†Œ๊ฐœ


image



๐ŸŒฑ ๋ชจ๊ฐ์ฝ” ํšŒ๊ณ 


๐Ÿ”บ ์ƒˆ๋กญ๊ฒŒ ๋ฐฐ์šฐ๊ณ , ์ข‹์•˜๋˜ ์  Keep

  1. ์˜จ๋ณด๋”ฉ์„ collectionView + pageControl๋กœ ๊ตฌํ˜„ํ–ˆ๋Š”๋ฐ Rx+main ํ”„๋กœ์ ํŠธ๊ฐ€ ์ œ๋ฒ• ๊ตฌ๊ธ€๋งํ•˜๊ธฐ ์ข‹์€ ํ”„๋กœ์ ํŠธ๋‹ค!!

  1. scrollViewWillEndDragging์„ ์•Œ์—‘์Šค๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ• : onboardView.collectionView.rx.willEndDragging
onboardView.collectionView.rx.willEndDragging
            .withUnretained(self)
            .subscribe(onNext: { (vc, arg1) in
                let (_, targetContentOffset) = arg1
                let page = Int(targetContentOffset.pointee.x / self.onboardView.collectionView.frame.width)
                self.onboardView.pageControl.currentPage = page
            })
            .disposed(by: disposeBag)

  1. ์˜จ๋ณด๋”ฉ์—์„œ ๋‹ฌ๋ผ์ง€๋Š” ๋ผ๋ฒจ๊ณผ ํŠน์ • ๋ถ€๋ถ„์— ์ƒ‰์ด ์ ์šฉ๋˜๋Š” ๊ฑด switch๋ฌธ์œผ๋กœ ํ•ด๊ฒฐํ–ˆ๋‹ค.
  • ๋ผ๋ฒจ๋กœ ํ•ด์•ผ ํ•˜๋Š” ์ด์œ ๋Š” ๋กœ์ปฌ๋ผ์ด์ง•/๋ณด์ด์Šค์˜ค๋ฒ„ ๋Œ€์‘ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

  1. PlainTextField ๋งŒ๋“ค ๋•Œ clearButtonRect ๋ฉ”์†Œ๋“œ ์ƒˆ๋กญ๊ฒŒ ์ ์šฉํ–ˆ๋‹ค. editingDidBegin/editingDidEnd๋„ ์ปค์Šคํ…€ ํ…์ŠคํŠธ ํ•„๋“œ์—์„œ ๊ตฌํ˜„ํ•ด์„œ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋ณ„๋„๋กœ isSelected ๊ฐ™์€ ์ฒ˜๋ฆฌ๋ฅผ ์•ˆํ•ด๋„ ๋œ๋‹ค.
    private func setupState() {
        self.addTarget(self, action: #selector(editingDidBegin), for: .editingDidBegin)
        self.addTarget(self, action: #selector(editingDidEnd), for: .editingDidEnd)
    }
}

// MARK: - TextField State

extension PlainTextField {
    @objc func editingDidBegin() {
        lineView.backgroundColor = Color.black
    }
    
    @objc func editingDidEnd() {
        lineView.backgroundColor = Color.gray3
        self.resignFirstResponder()
    }
}


  1. Rx์—์„œ ์ œ๊ณตํ•˜๋Š” ๋‹ค์–‘ํ•˜๊ณ  ์ƒˆ๋กœ์šด ์—ฐ์‚ฐ์ž๋“ค์„ ์ ์šฉํ•ด๋ณผ ์ˆ˜ ์žˆ๋Š” ๊ฒฝํ—˜ ํš๋“!
  • 1 ) withLatestFrom ์—ฐ์‚ฐ์ž๋ฅผ ํ†ตํ•ด์„œ ๋ฒ„ํŠผ ํƒญ ์‹œ์— ๊ฐ๊ฐ isValid ์ฆ‰, ํ…์ŠคํŠธํ•„๋“œ์˜ ๊ฐ’์ด ์œ ํšจํ•œ์ง€์— ๋Œ€ํ•œ Bool๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ•จ.
  • 2 ) compactMap์„ ์‚ฌ์šฉํ•ด์„œ coordinate ๊ฐ’์„ ๋ฐ›์•„์˜ค๋Š” ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์คฌ์Œ. compactMap์€ 1์ฐจ์› ๋ฐฐ์—ด์—์„œ ์˜ต์…”๋„ ๋ฐ”์ธ๋”ฉ ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ์žฅ์ ์ด ์žˆ์–ด์„œ ๊ฐ„ํŽธํ•จ์„ ๋Š๋‚Œ.
  • 3 ) throttle ๋ฌธ์ž ๋ฉ”์‹œ์ง€ ์ธ์ฆ ์ฝ”๋“œ๋ฅผ ๋ฐ›์•„ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž์˜ ๊ณผ๋„ํ•œ ํƒญ์„ ํ†ตํ•ด API ์„œ๋ฒ„ ํ†ต์‹ ์ด ๋˜๋Š” ๊ฑธ ๋ง‰๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•จ.
output.tap
    .withUnretained(self)
    .throttle(.milliseconds(500), scheduler: MainScheduler.instance)
    .subscribe(onNext: { (vc, isValid) in
        isValid ?
        vc.messageViewModel.requestLogin() :
        vc.showToast(Toast.phoneTypeError.message)
    })
    .disposed(by: disposeBag)

  1. ์„ ํƒ ์‹œ expandable ๋˜๋Š” ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ cell์—๋„ ๋งˆ์ง„๊ฐ’์„ ์ค„ ์ˆ˜ ์žˆ๊ณ , stackView์—๋„ ๋งˆ์ง„๊ฐ’์„ ์ค„ ์ˆ˜ ์žˆ๋‹ค๋Š” ์‚ฌ์‹ค์„ ๋ฐฐ์› ๋‹ค. (inset)
$0.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
            $0.isLayoutMarginsRelativeArrangement = true
    override func layoutSubviews() {
        super.layoutSubviews()
        contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16))
    }

  1. ๊ทธ๋™์•ˆ ์ฝ”๋“œ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ ˆ์ด์•„์›ƒ์„ ์žก์„ ๋•Œ SnapKit์„ ์ž์ฃผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ์ƒˆ๋กญ๊ฒŒ ์•Œ๊ฒŒ ๋œ ๋ฉ”์†Œ๋“œ๋“ค. ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ์ฑ„ํŒ…๋ทฐ์—์„œ ์ž…๋ ฅ์ฐฝ ๋ ˆ์ด์•„์›ƒ๊ณผ ์ต์ŠคํŽœ๋”๋ธ” UI ๋“ฑ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.
  • priority(.low) ๋ฅผ ํ•ด์ฃผ๋ฉด ์šฐ์„ ์ˆœ์œ„๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • updateConstraints : ์ œ์•ฝ ๋ณ€๊ฒฝ ์‹œ
  • remakeConstraints : ๊ธฐ์กด ์ œ์•ฝ์ด ๋‹ค ์‚ฌ๋ผ์ง€๊ณ  ๋‹ค์‹œ ์„ค์ •

  1. RxCoreLocation์„ ์จ๋ณด๋Š” ๊ฒƒ์ด ์•ˆ๋˜์–ด ์•Œ๊ฒŒ ๋œ DelegateProxy๋ฅผ extension์œผ๋กœ ๋„ฃ์–ด Rx๋กœ CoreLocation ๊ตฌํ˜„ํ–ˆ๋‹ค. ์‹ ๊ธฐํ•œ ์ ์€ Rx์—์„œ ์ž์ฒด์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ๋„ ์—‘์ฝ” ๋ฒ„์ „์—์„œ๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ด์„œ ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ•ด์„œ ํ•ด๋‹น ๋ฐฉ๋ฒ•์œผ๋กœ ํ•ด๊ฒฐํ–ˆ์Œ

  1. ๋ธŒ๋ผ์ผ“์ด ์žˆ๋Š” ๋ฐฐ์—ด์„ ์„œ๋ฒ„ ํ†ต์‹  ์‹œ body์— ๋‹ด์•„ ๋ณด๋‚ผ ๋•Œ URLEncoding(arrayEncoding: .noBrackets) ์„ ํ†ตํ•ด ๋ธŒ๋ผ์ผ“์„ ์ œ๊ฑฐํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•˜๊ณ , ์•Œ๋ผ๋ชจํŒŒ์ด์–ด์—์„œ ์ œ๊ณตํ•ด์ฃผ๋Š” ์ธ์ฝ”๋”ฉ ๋ฉ”์†Œ๋“œ๋“ค์„ ๊ณต๋ถ€ํ•˜๋Š” ๊ณ„๊ธฐ๊ฐ€ ๋๋‹ค.

์Šคํฌ๋ฆฐ์ƒท 2023-01-09 ์˜คํ›„ 3 11 44


  1. ์ ์ ˆํ•˜๊ฒŒ ๋ทฐ๋ฅผ ์ปค์Šคํ…€ํ•ด ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•œ ์ ๊ณผ, ํด๋ž˜์Šค ๋‚ด๋ถ€์— ํ•„์š”ํ•œ ์—ด๊ฑฐํ˜•์„ ํ†ตํ•ด ๋ถˆํ•„์š”ํ•œ ์ฝ”๋“œ์˜ ๋ฐ˜๋ณต์„ ์ค„์ธ ์ .
  • ์ ‘๊ทผ์ œ์–ด์ž ์‚ฌ์šฉํ•ด์„œ ์™ธ๋ถ€์—์„œ ์ ‘๊ทผํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ๋ง‰์•˜๊ณ , ๋Ÿฐํƒ€์ž„ ์‹œ ์„ฑ๋Šฅ์„ ๋†’์˜€๋‹ค.
  • แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2023-01-12 แ„‹แ…ฉแ„Œแ…ฅแ†ซ 12 59 33


๐Ÿ”ป ์•„์‰ฝ๊ณ  ๊ฐœ์„ ํ•ด์•ผ ํ•  ์  Problem & Try

  1. Input/Output ํŒจํ„ด์„ ๊ณต๋ถ€ํ•˜๊ณ  ๋ฐ”๋กœ ์ ์šฉํ•˜๊ธฐ์— ์–ด๋Š ์ •๋„ ์‹œ๊ฐ„์ด ๊ฑธ๋ ธ๋˜ ์  (์ดˆ๊ธฐ์„ค์ •์—์„œ ์ œ๋Œ€๋กœ ์ ์šฉ)
  • ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„์„ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์–ด ์ข‹์•˜์ง€๋งŒ ํ›„๋ฐ˜๋ถ€์—๋Š” ๊ณต์ˆ˜์‚ฐ์ • ์‹คํŒจ์˜ ์›์ธ์ด ๋˜์–ด ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ฒŒ ๋๋‹ค.

์Šคํฌ๋ฆฐ์ƒท 2023-01-12 ์˜ค์ „ 12 51 18


  1. API ๋ณ„๋กœ ์‘๋‹ต๊ฐ’๊ณผ ์ƒํƒœ์ฝ”๋“œ๊ฐ€ ์˜ค๋Š” ๊ฒฝ์šฐ or ์ƒํƒœ์ฝ”๋“œ๋งŒ ์˜ค๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋Š”๋ฐ ํ•˜๋‚˜์˜ APIManager์˜ ํƒ€์ž… ๋ฉ”์†Œ๋“œ๋กœ ๊ด€๋ฆฌํ•ด์ฃผ๋ ค๊ณ  ์—ฌ๋Ÿฌ๋ฒˆ ์‹œ๋„ํ•˜๋‹ค๋ณด๋‹ˆ ๋”œ๋ ˆ์ด๋๋‹ค๋Š” ์ .
final class APIManager {
    private init() { }
    static let shared = APIManager()
    
    typealias Completions<T> = ((T?, Int?, APIError?) -> Void)
    
    func request<T: Decodable>(_ type: T.Type = T.self,
                               _ convertible: URLRequestConvertible,
                               completion: @escaping Completions<T>) {
        
        AF.request(convertible)
            .responseDecodable(of: type) { [weak self] response in
                guard let self = self else { return }
                guard let statusCode = response.response?.statusCode else { return }
                completion(nil, statusCode, nil)
                print("โš ๏ธ ์ƒํƒœ์ฝ”๋“œ๋งŒ!!! ===", type, "/", statusCode)
                
                switch response.result {
                case .success(let data):
                    completion(data, statusCode, nil)
                    print("โœ… ์„ฑ๊ณต!!! ===", data, "/", statusCode)
                    
                case .failure(_):
                    guard let error = APIError(rawValue: statusCode) else { return }
                    if error.rawValue == 401 {
                        ErrorManager.refreshToken {
                            self.request(type, convertible, completion: completion)
                        }
                    }
                    completion(nil, statusCode, error)
                    print("โŒ ์‹คํŒจ!!! ===", type, "/", error, "/", error.localizedDescription)
                }
            }
    }
}
  • ์ด ๊ฒฝ์šฐ, ๊ผญ ํ•˜๋‚˜์˜ ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค๋Š” ๊ฒƒ๊ณผ ์ถ”ํ›„ Rx์—์„œ ์ œ๊ณตํ•ด์ฃผ๋Š” Single์ด๋ผ๋Š” ์˜ต์ €๋ฒ„๋ธ”์„ ์•Œ๊ฒŒ ๋๋Š”๋ฐ, success, error ์ด๋ฒคํŠธ๋งŒ ๋ฐฉ์ถœํ•ด ๋„คํŠธ์›Œํฌ์— ์ ํ•ฉํ•˜๋‹ค๊ณ  ์ƒ๊ฐ๋ผ์„œ ๊ณต๋ถ€ ํ›„ ์ ์šฉํ•ด๋ณด๊ณ  ์‹ถ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๊ฒŒ ๋๋‹ค.

  1. ๋ทฐ๋ชจ๋ธ์—์„œ ๋„คํŠธ์›Œํฌ ์ฒ˜๋ฆฌ ํ›„ ํ™”๋ฉด์ „ํ™˜ ์ฒ˜๋ฆฌํ•ด์ค„ ๋•Œ ์ฝ”๋””๋„ค์ดํ„ฐ ํŒจํ„ด์˜ ํ•„์š”์„ฑ์„ ๋Š๋ผ๊ฒŒ ๋˜์—ˆ์œผ๋‚˜, ์ œํ•œ๋œ ์‹œ๊ฐ„์œผ๋กœ ์ธํ•ด ๊ณต๋ถ€ํ•  ์‹œ๊ฐ„์ด ์—†์–ด ์ ์šฉํ•˜์ง€ ๋ชปํ–ˆ๋‹ค.

  1. ์ฑ„ํŒ…์„ ๊ตฌํ˜„ํ•  ๋•Œ ์–ด๋ ค์› ๋˜ ์ ์ด BehaviorSubject๋ฅผ ํ†ตํ•ด chatResponse๋ฅผ ๋ฐ›์•„์„œ postChat ์„œ๋ฒ„ํ†ต์‹  ์‹œ ๋ณด๋‚ด๋Š” ์ฑ„ํŒ… ๋ฐ์ดํ„ฐ๋ฅผ onNext๋ฅผ ํ†ตํ•ด ๋„ฃ๊ณ , ๋ทฐ์— ์ฑ„ํŒ… ๋‚ด์šฉ์„ ๋ฟŒ๋ ค์ฃผ๋Š”๋ฐ ์ด๋•Œ ์ฑ„ํŒ… ๋ฐฐ์—ด์ด ์•„๋‹Œ ๋ณด๋‚ธ ์ฑ„ํŒ… ๋‚ด์šฉ๋งŒ ํ•˜๋‚˜์”ฉ ๋“ค์–ด๊ฐ€ ์–ด๋ ค์›€์„ ๊ฒช์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ [Chat] ํƒ€์ž…์„ ๊ฐ€์ง„ chatList๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค๊ณ  ์ด chatList๋ฅผ onNext๋ฅผ ํ†ตํ•ด ๊ฐ’์„ ๋„ฃ์–ด์คฌ๋‹ค. ์ด ๋ถ€๋ถ„์—์„œ ๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์ ์šฉํ•จ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ๋ถˆํ•„์š”ํ•œ ๊ณผ์ •์ด ์ค‘๊ฐ„์— ๋“ค์–ด๊ฐ„ ์ ์ด ์•„์‰ฝ๊ณ , ๊ฐœ์„ ์ ์„ ๊ณ ๋ฏผํ•ด๋ด์•ผ ํ•  ๊ฒƒ ๊ฐ™๋‹ค.
/// ChatViewModel

var chatList = [Chat]()
    
let chatResponse = BehaviorSubject<[Chat]>(value: [])
    
func postChat(_ chat: String, to: String, completion: @escaping ((Int) -> Void)) {
    APIManager.shared.request(Chat.self, ChatRouter.sendChat(chat: chat, to: to)) { [weak self] data, status, error in
        guard let self = self else { return }
        if let data = data {
            self.chatList.append(
                Chat(id: data.id,
                     to: data.to,
                     from: data.from,
                     chat: data.chat,
                     createdAt: data.createdAt.toDate().toString(format: .special)))
            
            RealmRepository.shared.addChatData(
                chat: RealmChat(id: data.id,
                                from: data.from,
                                to: data.to,
                                chat: data.chat,
                                createdAt: data.createdAt.toDate()))
            
            self.chatResponse.onNext(self.chatList)
            completion(self.chatList.count)
        }
        if let error = error {
            ErrorManager.handle(with: error, vc: ChatViewController(ChatViewModel()))
        }
    }
}

  1. ๊ฒ€์ƒ‰๋ทฐ๋ฅผ ๊ตฌํ˜„ ์‹œ ๋ณต์žกํ•œ ๋กœ์ง ์ฒ˜๋ฆฌ๋ฅผ ์„ค๊ณ„ํ•˜๋Š” ๋ฐ์— ์–ด๋ ค์›€์„ ๊ฒช์—ˆ๊ณ , ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ œ๋Œ€๋กœ ๋ทฐ๋ชจ๋ธ๋กœ ๋ถ„๋ฆฌ์‹œํ‚ค์ง€ ๋ชปํ•œ ์ ์ด ๊ฐœ์„ ํ•  ๋ถ€๋ถ„.