기분이 축 쳐질 때, 열일하기 위한 자극이 필요할 때,누군가 당신의 곁에서 어떠한 말이라도 해주길 바란 적이 있나요?
당신이 원하는 타입의 응원으로, 하루에 한 번쯤은 미소지어 보세요.
왈 iOS 개발자 | 기능 담당 |
---|---|
김루희 | 로그인/온보딩/설정/푸시 |
김소연 | 메인 |
배은서 | 왈소리 만들기 |
최이준 | 왈소리 히스토리 |
사용 기술 스택 | 이유 |
---|---|
UIKit |
iOS Framework |
RxSwift RxCocoa |
비동기 처리 |
SnapKit |
코드 기반 오토레이아웃 |
Then |
간결한 인스턴스 선언 |
Moya |
서버통신 |
WALKit |
디자인시스템 및 컴포넌트 SPM으로 관리 |
Lottie Gifu |
런치스크린 및 로딩뷰 |
KakaoSDK Apple Login |
소셜로그인 |
FCM |
푸시알림 구현 |
SPM Cocoapods |
의존성 관리 도구 |
사용자 액세스 토큰 및 온보딩 처리 상태에 따라 첫 시작화면 분기처리 고민
-
다양한 경우들이 존재
- 자동로그인 → 액세스토큰 저장
- 로그아웃 → 액세스토큰 삭제
- 액세스 토큰 만료 → 재발급 API 호출 로직
- 재발급 토큰 만료 → 로그아웃 로직
- 탈퇴 → 사용자 정보 및 토큰 삭제
-
사용자가 왈 서비스에 가입했는지 (액세스토큰 발급)
-
온보딩 과정을 완료했는지 (완료했다면 닉네임이 로그인 API 응답값으로 옴)
위 과정들에 따라 첫 진입 화면이 달라서 그에 따른 경우의 수가 다르다. 온보딩 완료까지 해서 메인 뷰에 진입한 유저를 기존 유저라고 할 때,
-
닉네임 O → 메인
-
닉네임 X, 액세스토큰 O → 온보딩
-
닉네임 X, 액세스토큰 O → 로그인
SceneDelegate와 LoginViewController에서 액세스토큰과 닉네임을 UserDefaults
에 저장해 해당 값을 기준으로 분기처리해서 첫 진입 시점에 보여줄 시작 화면을 결정해줬다.
SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
// 닉네임O, 액세스토큰 O -> 자동로그인 -> 메인
guard let nickname = UserDefaultsHelper.standard.nickname else { return }
guard let accesstoken = UserDefaultsHelper.standard.accesstoken else { return }
if nickname != "" {
window?.rootViewController = UINavigationController(rootViewController: MainViewController())
window?.makeKeyAndVisible()
} else {
// 닉네임X, 액세스토큰 O -> 온보딩
if accesstoken != "" {
window?.rootViewController = UINavigationController(rootViewController: OnboardingViewController())
window?.makeKeyAndVisible()
} else { // 닉네임X, 액세스토큰 X -> 로그인
window?.rootViewController = LoginViewController()
}
}
window?.makeKeyAndVisible()
}
LoginViewController
private func pushToHome() {
guard let nickname = UserDefaultsHelper.standard.nickname else { return }
if nickname == "" {
transition(OnboardingViewController(), .presentFullNavigation)
} else {
// 로그인 -> 완료버튼을 눌러서 서버통신 성공인 경우에 -> 메인화면으로 이동
transition(MainViewController(), .presentFullNavigation)
}
}
동적 레이아웃(메인화면의 경우 사용자에 따라서, 상황에 따라서 여러 UI가 보여지게 되었다.)의 대응
- 아침, 점심, 저녁 왈소리를 어떻게 선택했는가?
- 예약한 왈소리가 있는가?
- 현재 확인할 수 있는 왈소리/확인한 왈소리는 몇개인가?
- 오늘 도착한 왈소리를 모두 읽었는가?
위의 상황들을 포함한 여러 상황에 동적으로 UI가 변경되는 화면이었기에, 이를 enum의 case로 관리
- 왈소리 데이터 타입 (아침/점심/저녁/사용자 직접 작성)
enum WALDataType {
case morning
case lunch
case evening
case special
var image: UIImage {
switch self {
case .morning, .lunch, .evening:
return WALIcon.imgPawActive.image
case .special:
return WALIcon.imgPawSpecial.image
}
}
var color: UIColor {
switch self {
case .morning, .lunch, .evening:
return UIColor.orange100
case .special:
return UIColor.mint100
}
}
}
- 왈소리 컨텐츠의 타입 (드립/주접/위로/꾸중)
enum WALContentType: Int {
case special = -1
case fun = 0
case love = 1
case cheer = 2
case angry = 3
var walImage: UIImage {
switch self {
case .special:
return WALIcon.imgWalbbongSpecial.image
case .fun:
return WALIcon.imgWallbbongFun.image
case .love:
return WALIcon.imgWallbbongLove.image
case .cheer:
return WALIcon.imgWallbbongCheer.image
case .angry:
return WALIcon.imgWallbbongAngry.image
}
}
}
- 메인 화면 캐릭터/타이틀 분기처리
enum WALStatus {
case sleeping
case arrived
case checkedAvailable
case checkedAll
var subTitle: String {
switch self {
case .sleeping:
return "왈뿡이가 자는 시간이에요. 아침에 만나요!"
case .arrived, .checkedAvailable, .checkedAll:
return "다들 밥 잘 먹어! 난 뼈다구가 젤루 좋아"
}
}
var content: String {
switch self {
case .sleeping:
return ""
case .arrived:
return "왈소리가 도착했어요\n발바닥을 눌러 확인해주세요"
case .checkedAvailable:
return "왈소리가 열심히 달려오고 있어요"
case .checkedAll:
return "오늘의 왈소리가 전부 도착했어요"
}
}
var walImage: UIImage {
switch self {
case .sleeping:
return WALIcon.imgWalBBongSleeping.image
case .arrived:
let walArrivedImageList: [UIImage] = [WALIcon.imgWalBBongArrive1.image,
WALIcon.imgWalBBongArrive2.image,
WALIcon.imgWalBBongArrive3.image]
return walArrivedImageList.randomElement() ?? WALIcon.imgWalBBongArrive1.image
case .checkedAll, .checkedAvailable:
return WALIcon.imgWalBBongWaiting.image
}
}
}
오늘의 왈소리 중 확인할 수 있는/확인한 왈소리의 분기
PATCH
를 통해 확인한 경우, 서버의 DB에서 확인한 왈소리로 업데이트 구현
var path: String {
switch self {
case .main:
return "/main"
case .mainItem(let param):
return "/main/\(param)"
}
}
var method: Moya.Method {
switch self {
case .main:
return .get
case .mainItem:
return .patch
}
}
-
인증이라는 한 사이클을 체험해본 경험이 없었는데, 실제 서비스에서 인증이 중요하고 고려할 부분이 많은 단계라는 걸 알 수 있었다.
- 사용자의 정보를 어떻게 저장하고 관리해야 하는지
- 자동 로그인을 구현하기 위한 과정
- 보안을 위해서 액세스 토큰 만료 기간의 적정기간과 그에 따른 로그아웃 처리 등을 배웠다.
-
특히, 종종 어떤 서비스를 사용하다가 일정 기간 앱을 들어가지 않으면 화면에 데이터가 뜨지 않는 경우를 보았는데 이 경우 사용자로서 느끼는 경험이 부정적이고, 의문이 들 수 있다는 걸 바탕으로 왈을 개발하면서 고려하고자 노력했다.
-
추후 개선하고 싶은 부분은 UserDefaults에 저장한 정보들을 Keychain을 통해 저장해보자는 것이다. 보안에 있어 민감한 액세스 토큰, 리프레시 토큰을 저장하기 적합하기 때문이다.
-
코드를 개선하는 과정 속에서 enun을 사용하게 되었다.
- enum으로 나타날 수 있는 경우의 수를 case로 나눠서 그에 맞는 Image, Color, Constraints 등을 관리하는 방법에 대해 알게 되었다.
-
QA를 진행하면서 기획, 디자인에 대해 꼼꼼하게 이해하고 정리한 뒤, 개발을 진행할 때 보다 많은 경우의 수를 고려하면서 구현할 수 있다는 것을 알게 되었다.
-
꼼꼼한 확인이 보다 유연한 사고의 확장으로 이어진다는 것을 배웠다.
-
개선하고 싶은 부분은 아직까지도 로우한 값으로 관리되고 있는 부분들에 대해서 좀 더 추상화된 방법으로 코드를 작성하고 싶다.
-
또한, 기획이 변경될 경우에 대비해서 enum의 case로 관리하는 것보다 더 유연한 대처 방법을 찾고 싶다. (아침, 점심, 저녁이 아니라 다른 상황에 왈소리가 추가되는 경우 등이 있을 수 있으므로)