Skip to content

WooseokJ/ServiceLevelProject

Repository files navigation

ServiceLevelProject

2022.11.07 ~ 2022.12.07 (30일, 약 4주)

앱 아이콘 스크린샷
개요 디자이너, 서버개발자가 있는 프로젝트 로 새싹 스터디원을 찾고 채팅할수있는 앱 프로젝트
디자인 패턴 MVC, Repository, Singleton
화면 UIKit, SnapKit, AutoLayout
의존성 관리 도구 CocoaPod , Swift Package Manager
서버 Firebase Auth, Firebase Cloud Messaging
데이터베이스 Realm
네트워크 Alamofire, async/await
디버깅 스킬 LLDB
라이브러리 socket.io, NMapsMap, Tabman, Rxswift, RxCocoa
협업 Confluence, Swagger, Github, Figma
참고 회고

🟢 기술 명세

  • 프로토콜 에서 네트워크 로직where Self 를 활용하여 채택할 ViewController들 제약
  • 프로토콜로 네트워크 로직들을 모듈화하여 비지니스 로직과 분리 하여 뷰 컨트롤러 역할과 분리
  • 정규표현식으로 문자나 특수문자 입력시 인증요청이 안되게 함으로서 불필요한 요청 문제 해결
  • Alamofire URLRequestConvertible로 네트워크 Router 모듈화로 매 통신에필요한 파라미터,해더,HTTP 메서드 캡슐화
  • await/async 를 활용하여 매개변수 @escaping 구문 및 콜백함수로 부터 코드 가독성을 개선
  • Socket.io로 1:1채팅 구현시 DB 에 채팅내역 저장하여 네트워크 중복 호출 방지, 소켓 연결 및 해제 시점에 대해 대응
  • Firebase Auth휴대폰 인증 을 받고 상태코드를 통해 로그인 성공 화면 혹은 회원가입 화면 분기처리
  • Firebase Cloud Messaging 에서 발급받은 FCM token 으로 멀티디바이스 대응
  • Confluence 기획안을 보면서 요구사항 필수 기능 구현 및 화면 로직 설계 (화면 로직 링크)
  • Timer를 활용하여 5초마다 매칭상태 확인하는 API 반복호출
  • LLDB 명령어를 활용한 변수추적여러 네트워크 상황에대한 대처

🔴 고민한 내용 및 트러블 슈팅

이슈 1. 프로토콜 채택할수있는 ViewController 들을 제약
🔴 고민한 영역

프로토콜로 네트워크 로직을 만든후 아래와같이 UIViewController 로 제약을 걸면 studyPostDodge 메소드가 어느 ViewController에서 채택해서 사용이되는지 알기가 어렵고 네트워크로직에 import UIKit을 해주는게 적합하지 않다고 보인다.

스크린샷 2022-12-22 오후 3 12 58

🔵 해결

아래와같이 특정 뷰컨트롤러(ChattingViewController)에서만 studyPostdodge메소드를 사용한다고 제약을 주어서 어느 뷰컨트롤러에서 사용하는 메소드인지 명시!

// DodgeProtocol.swift
import Foundation

protocol DodgeProtocol: APIProtocol, APIQueueProtocol  {
    func studyPostDodge(otheruid: String)
}

extension DodgeProtocol where Self: ChattingViewController {

    
    func studyPostDodge(otheruid: String) {
        apiQueue.studyPostDodge(otheruid: otheruid) { [weak self] data in
            do {
                switch data {
                case .success:
                    print(data)
                    self?.view.makeToast("스터디 취소 성공")
                case .failure(.otherUIDError):
                    self?.view.makeToast("잘못된 otheruid 요청")
 
                case .failure(.notUserError):
                    self?.view.makeToast("미가입 회원")
                case .failure(.tokenErorr):
                    self?.view.makeToast("토큰 만료")
                    self?.refreshIdToken { [weak self] in
                        self?.studyPostDodge(otheruid: otheruid)
                    }
                case .failure(.serverError):
                    self?.view.makeToast("서버 에러")
                case .failure(.clientError):
                    self?.view.makeToast("클라이언트 에러")
                }
            }
            catch{
                print("에러야")
                return
            }
        }
    }
}
이슈 2. 토큰이 만료되는 상황에서 토큰 갱신이 무한으로 호출되는 문제
🔴 이슈

토큰갱신하는게 비동기적으로 처리되기떄문에 토큰갱신하고 다시 네트워크 요청메소드 호출시 무한으로 호출되는 문제가 발생

🔵 해결

completion Handler로 토큰갱신후 네트워크 요청메소드 호출하는 방식으로 해결

func studyPostRequest(otheruid: String) {
        apiQueue.studyPostRequest(otheruid: otheruid) { [weak self] data in
            do {
                switch data {
                case .success:
                    self?.view.makeToast("스터디 요청을 보냈습니다")
                case .failure(.alreadyTomeRequest):
                    self?.view.makeToast("상대방이 이미 나에게 스터디 요청한 상태")
                    self?.studyPostAccept(otheruid: otheruid)

                case .failure(.oppnentStopRequest):
                    self?.view.makeToast("상대방이 스터디찾기를 그만두었습니다.")
                case .failure(.notUserError):
                    self?.view.makeToast("미가입 회원")
                case .failure(.tokenErorr):
                    self?.view.makeToast("토큰 만료")
                    self?.refreshIdToken { [weak self] in
                        self?.studyPostRequest(otheruid: otheruid)
                    }
                case .failure(.serverError):
                    self?.view.makeToast("서버 에러")
                case .failure(.clientError):
                    self?.view.makeToast("클라이언트 에러")

                }
            }
            catch let error{
                print(error)
            }
        }
    }
}

//APIProtocol.swift
@objc protocol APIProtocol {
    @objc optional func presentVC()
    @objc optional func refreshIdToken()
}

extension APIProtocol where Self: UIViewController {
    
    typealias completion = ( () -> Void)
    
    func refreshIdToken(completion: @escaping completion) {
        let currentUser = Auth.auth().currentUser
        currentUser?.getIDTokenForcingRefresh(true) { idToken, error in
            if error != nil {
                self.view.makeToast("토큰 갱신 에러")
                return;
            }
            print("갱신한 idToken",idToken as Any)
            UserDefaults.standard.set(idToken!, forKey: "token")
            completion() 
        }
    }
}
이슈 3. 휴대폰 번호 입력부분에서 숫자이외 한글 복사붙여넣기 허용 문제
🔴 이슈

휴대폰 인증 요청시 문자열 입력시에도 Firebase Auth로 인증을 요청이 되는 문제가있었다.

🔵 해결

정규표현식으로 유효성검사를 통해 유효하지않은 숫자포맷과 문자열이 TextField에 입력시 인증요청을 보내지않음.

// 정규표현식으로 유효성검사
extension LoginViewController {
    func isValidPhone(phone: String?) -> Bool {
        guard phone != nil else { return false }
        let phoneRegEx = #"^\(?\d{3}\)?[ -]?\d{3,4}[ -]?\d{4}$"#
        guard let valid = phone.range(of: phoneRegEx, options: .regularExpression) != nil
        return valid
    }
   
      
      //MARK: textfield의 text가 변경이있을떄
      loginView.phoneNumberTextField.rx.text.orEmpty
          .observe(on: MainScheduler.asyncInstance)
          .withUnretained(self)
          .bind { (vc,textfield) in
              
              /// 글자수 제한
              guard vc.checkMaxLength(textField: vc.loginView.phoneNumberTextField, maxLength: 13) else {return}
              
              // 유효한 숫자형태면 색상변환
              guard !self.isValidPhone(phone: textfield) {
                 vc.loginView.phoneButton.backgroundColor = BrandColor.green
                  return
              }
              vc.loginView.phoneButton.backgroundColor = Grayscale.gray6
              let test = vc.loginView.phoneNumberTextField
              switch test.text?.count {
                  case 4:
                  if test.text?.filter({$0 == "-"}).count == 1 {
                          test.deleteBackward()
                      } else {
                          test.text?.insert("-", at: test.text?.index(test.text?.startIndex, offsetBy: 3))
                      }
                      vc.loginView.phoneNumberTextField.text? = test.text?
                  case 9:
                  if test.text?.filter({$0 == "-"}).count == 2 {
                          test.deleteBackward()
                      } else {
                          test.text?.insert("-", at: test.text?.index(test.text?.startIndex, offsetBy: 8))
                      }
                      vc.loginView.phoneNumberTextField.text? = test.text?
                  default:break
              }
          }
          .disposed(by: disposeBag)
    }
}
이슈 4. 채팅 대화목록을 불러오는 부분에서 채팅할때 마다 계속해서 호출 되는 문제
🔴 이슈

채팅메세지를 보낼떄마다 채팅대화목록을 불러오는방식은 과도한 호출이 되는 문제가 발생할것같다.

🔵 해결

처음 채팅목록을 Realm DB에 저장하고 이후의 채팅을 보내고 보낸내용은 Local Realm DB에서 저장하고 불러오는형태로 서버에 Send만 보내는 request만 보내고 chatPostList요청은 처음 viewDidload시 한번만 불러온다!

// chatPostList를 통해 서버에서 대화목록을 가져와서 Realm에 저장한다.
class ChattingViewController {
    override func viewDidLoad() {
        chatPostList(lastchatDate: lastchatDate, from: UserDefaults.standard.string(forKey: "otheruid")!) { [weak self] data in
            self?.recentChattingInfo = data
            do {
                print(self?.repository.localRealm.configuration.fileURL! as Any)
                try self?.repository.localRealm.write {
                    self?.repository.localRealm.deleteAll()
                    self?.test = (data?.payload.count)!
                    data?.payload.forEach{
                        let value = ChatData(to: $0.to!, from: $0.from!, chat: $0.chat!, createdAt: $0.createdAt!)
                        self?.repository.localRealm.add(value)
                    }
                }
                self?.configureTableView()
                self?.collectionViewConfigure()
            }catch let error {
                 print(error)
              }
        }
}

extension ChattingViewController {
    func bind() {
        // 메세지 전송전송후 DB와 chatPostSend메소드로 네트워크 요청
        chattingView.sendButton.rx
            .tap
            .map {self.chattingView.sendTextView.text}
            .withUnretained(self)
            .bind { (vc,val) in
                vc.chatPostSend(chat: val!, to: UserDefaults.standard.string(forKey: "otheruid")!)
                vc.chattingView.sendTextView.text = ""
                let chatContent = ChatData(to:UserDefaults.standard.string(forKey: "otheruid")! , from: UserDefaults.standard.string(forKey: "Myuid")!, chat: val!, createdAt: "")
                self.repository.addChat(item: chatContent)
                
                vc.chattingView.tableView.reloadData()
            }
            .disposed(by: disposeBag)
      }
}
이슈 5. 네트워크 요청시 배열로 요청할떄 bracket 문제
🔴 이슈

alamofire는 request 배열을 보낼떄 [](브라켓)을 달고 보내지는데 이 때문에 서버쪽에서 거부하는 상황

🔵 해결

Alamofire 공식문서를 보고 참고하여 브라켓을 제거하는 방법을 시도

/func queueRequest(lat: Double, long: Double, studylist: [String], completionHandler: @escaping QueuPostHandler ) {
        let api = APIHeader.queue(lat: lat, long: long, studylist: studylist)
        AF.request(api.url, method: api.method, parameters: api.parameters , encoding: URLEncoding(arrayEncoding: .noBrackets), headers: api.headers).validate().responseData { response in
            switch response.result {
            case .success(let data):
                completionHandler(.success(data))
            case .failure :
                guard let customError = queuePostErorr(rawValue: response.response!.statusCode) else{return}
                completionHandler(.failure(queuePostErorr(rawValue: customError.rawValue)!))
            }
        }
    }

🟢 참고

Alamofire 링크
스크린샷 2022-12-23 오후 3 10 22

🔵 회고

회고
이번 SLP 프로젝트에서는 서비스 레벨수준의 디자인,기획,서버가 있었습니다. Confluence를 통해 요구사항들을 보면서 적합한 기술스택을 정하고 Service Flow를 그려보면서 개발을 직접적으로 해보기전 방향성을 설정해 보았습니다. 기술스택의경우 선정시 회사마다 다르겠지만 그래도 현업에서 많이사용하는 라이브러리로 해보려고 시도하였고 화면마다 네트워크호출되는 부분에대한 로직처리를 Figma로 그려 보고 로직에맞게 개발의 방향성을 잡아가면서 개발하였습니다. 이전 출시프로젝트에서는 빠른개발을위해 로직적인부분의 고려가 덜된상태에서 개발을 진행하여서 변동사항에대해 대처하기가 힘들었습니다. 하지만 이번 SLP에서도 서버가 변경되기도하고 기획안이 추가되기도하는 유연한 상황에대해 대처하기가 수월했습니다. 이번에 네트워크 구조에대해 Protocol 과 Enum을 통해 모듈화와 추상화를 잘해두었기에 화면별로 중복되서 호출되는 네트워크 메서드에대한 대응도 쉽게할수있었고 firebase auth를 통해 휴대폰인증에따른 화면분활처리와 WebSocket을 이용한 1:1 실시간 채팅, 불필요한 네트워크호출로인한 서버부하를 줄이는 생각같은 요구사항에 없지만 고려해야할 사항들에대해 생각해보고 구현해보는 경험을할수있어서 좋았습니다.

About

서비스 레벨 프로젝트(새싹 ios 11월)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages