출시 직후부터 자잘한 문제부터 발생하면 안 될 문제까지 정말 많은 피드백을 받게 되었는데, 한 두 개가 아니라 팀원들과 회의를 통해 모든 문제를 한 번에 다잡기보다는 먼저 가장 급한 문제부터 잡아나가자는 결론이 났다.
피드백 리스트
회원가입
1. 텍스트 필드를 순서대로 입력하지 않고 반대로 입력할 경우 예러처리가 제대로 진행되지 않고 있음
2. 개인정보 처리방침을 동의 했으나 텍스트 필드가 모두 입력되지 않은 상태에서도 회원가입 버튼이 활성화되고 있음
→ 예외처리 재구현 필요 & 개인정보 처리방침 동의 후 활성화 예외처리 필요
메인(타이머)
1. 앱 실행 중 중단 버튼이 안 눌릴 때가 있음
2. 탭바 이동 후 다시 메인페이지로 왔을 때 마리모 애니메이션 실행 안 됨
→ 전면 리팩토링 필요
캘린더
1. 화살표로 달 이동이 안 됨 & 셀 클릭 안 됨(상세 페이지 이동)
→ 하이라키 확인 시 네비게이션 바가 활성화되어 버튼을 덮고 있음
투두
1. 셀 살제 안 됨, 편집 버튼 안 사라짐(편집이 중복 처리 됨)
2. 투두 연필 버튼 누르니까 앱이 종료됨
마이페이지
1. 최근 성장한 마리모 보기가 버튼처럼 안 보임
2. 대체적으로 어떻게 사용하는지 모르겠음
→ 마리모 보기 버튼 수정 및 회원가입 시 온보딩 페이지 추가
설정
1. 프로필 수정이 왜 설정에 있는지 모르겠다, 사용하기 어렵다
→ 회원가입 시 온보딩 페이지 추가
프로필 수정
1. 닉네임 수정과 회원가입 시 닉네임 입력과 유효성 검사가 일치하지 않음
2. 디데이 오늘 날짜 포함 시 하루가 포함되어 나옴
→ 닉네임은 회원탈퇴와 동일한 유효성 검사 진행 / D-Day는 내 정보 수정 페이지 오늘 포함 수정 필요
회원 탈퇴
- 데이터가 일괄적으로 안 지워지고 있음(내부 확인)
→ 투두리스트가 삭제가 안 되고 있음, 회원 탈퇴 재구현 필요
날짜 선택 모달
1. 취소 안 됨, 선택 후 확인 눌러도 안 내려감
위젯
1. 왜 설치하자마자 D-Day야? 라는 질문이 많았음...
→ 프로그래스바 날짜 설정하고 뜨도록 수정 예정
튜터님 피드백 : 앱 처음 시작 시 온보딩이 있어야 할 것 같다
일단 내가 진행했던 페이지인 로그인/회원가입 & 메인페이지를 전면적으로 수정해야 할 필요성을 느꼈는데 일단 코드 베이스로 처음 구현하기도 했고, 빠른 시간 내로 구현하려다 보니 코드가 깔끔하지 않아 어딘가 한 군데를 수정하는데 너무 많은 시간이 소요될 것 같았다. 그래서 주변에 조언을 구해 Then과 SnapKit을 사용해 코드를 줄이고 View와 Controller를 분리하며 피드백 내용을 수정해 나가기로 결정했다.
회원 탈퇴
먼저 가장 급하다고 판단된 회원탈퇴부터 수정하기 시작했는데 계속 시도를 해도 실패를 반복했다. 결국 이건 다른 문제가 있다는 판단이 들어, 구글링을 미친 듯이 한 결과 데이터 구조 자체에서 문제가 있음을 깨달았다.(...)
바로 Firestore에서 하나의 Document를 삭제한다고 해도 하위 Collection까지 함께 삭제해주지는 않는다는 문제였다.
[ 변경 전 데이터 구조 ]
User 정보(Collection) [
UserUUID(DocumentID)[
user 문서(필드),
Timer Data(Sub-Collection) [
Date(DocumentID) [
Timer Data(필드)
]
],
Todo Data(Sub-Collection) [
Date(DocumentID) [
Sub-Collection(Sub-Collection) [
Sub-CollectionID(DocumentID) [
Todo Data(필드)
]
]
]
]
]
]
이러니까 어떻게 Timer Data에 대한 것까지는 삭제되도록 했는데도 불구하고 Todo Data가 죽어도 삭제가 되지 않는 거다. 그래서 밤새 고민하다 Todo 데이터만 CoreData로 저장하거나, 저장하는 로직을 변경하는 방법 둘 중 하나를 선택해야겠다는 결론에 도달했다. 바로 팀원들한테 의견을 물어보니 '다른 데이터는 전부 서버에 저장되는데 Todo만 내부저장소를 활용하는 건 아닌 것 같다' 라는 의견이 나와서 바로 서버 로직을 변경하는 걸로 진행하게 되었다.
[ 변경된 데이터 구조 ]
User 정보(Collection) [
UserUUID(DocumentID)[
user 문서(필드),
Timer Data(Sub-Collection) [
Date(DocumentID) [
Timer Data(필드)
]
],
Todo Data(Sub-Collection) [
Date(DocumentID) [
Todo Data(필드) >> Array로 변경
]
]
]
]
Sub-Collection의 문서까지는 삭제가 되니까 Todo도 Timer와 동일하게 하위 컬렉션이 아니라 문서로 저장되게 하는 방법으로 재구현을 진행했고, 결과는 성공적이었다!

메인(타이머 및 애니메이션)
[ Timer를 시작하면 Background에 버블 애니메이션이 무수히 많아지는 문제 ]

메인 페이지를 View와 Controller로 분리하며 구현하는 도중 Timer를 시작하면 Background 애니메이션인 버블이 엄청나게 늘어나는 문제가 발생했다.
처음에는 MainViewController에서 바로 setupBubbleEmitter를 호출했는데 이게 문제였다.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
stopwatchView.setupBubbleEmitter()
}
왜냐하면, viewDidLayoutSubviews는 ViewController가 레이아웃을 다 그린 후에 호출되는데 이때 setupBubbleEmitter를 호출하게 되면 그 시점에 View의 레이아웃이 완전히 완료되지 않기 때문에 View의 크기나 위치등이 올바르게 설정되지 않았던 것 같다. 그래서 View를 layoutSubviews를 통해 개별적으로 MainViewController에 호출하는 식으로 변경하니 문제가 해결되었다.
override func layoutSubviews() {
super.layoutSubviews()
backgroundLayer.frame = bounds
bubbleEmitter.emitterPosition = CGPoint(x: bounds.width / 2, y: bounds.height * 0.85)
bubbleEmitter.emitterSize = CGSize(width: bounds.width, height: 1)
}
[ 다른 페이지 이동 후 마리모 애니메이션이 실행되지 않는 문제 ]
이 문제로 구현 초기부터 애를 먹었는데 해결하고자 온갖 생명주기를 다 사용해보고(...) 튜터님도 찾아가 보고 했지만 해결되지 않고 있었다. 이후, 같이 공부를 하는 분께 이 문제에 대해 조언을 구했는데 애니메이션을 별도의 파일로 분리해서 시작점과 끝점을 맞추어 구현해 보라는 조언을 듣고 이번 피드백 수정 시 반영해 보았다.
기존 애니메이션 코드
private func updateImageView() {
if currentGroup < 5 {
imageView.image = UIImage(named: "Group \(currentGroup)")
} else {
imageView.image = UIImage(named: profileImageName)
}
addBouncingAnimation(targetView: imageView)
}
private func addBouncingAnimation(targetView: UIView) {
let moveDistance: CGFloat = 30 // 이동 거리
let duration: TimeInterval = 1.6
let damping: CGFloat = 1
let velocity: CGFloat = 0
UIView.animate(
withDuration: duration,
delay: 0,
usingSpringWithDamping: damping,
initialSpringVelocity: velocity,
options: [.autoreverse, .repeat],
animations: {
targetView.transform = CGAffineTransform(translationX: 0, y: -moveDistance)
},
completion: nil
)
}
1차 시도
MainViewController.swift
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
stopwatchView.successView.isHidden = true
stopwatchView.cheeringLabel.isHidden = false
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 애니메이션 추가
if !timerIsCounting {
AnimationHelper.addBouncingAnimation(to: imageView)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 애니메이션 제거
AnimationHelper.removeBouncingAnimation(from: imageView)
}
AnimationHelper.swift
class AnimationHelper {
static func addBouncingAnimation(to targetView: UIView) {
let moveDistance: CGFloat = 30 // Movement distance
let duration: TimeInterval = 1.6
let damping: CGFloat = 1
let velocity: CGFloat = 0
UIView.animate(
withDuration: duration,
delay: 0,
usingSpringWithDamping: damping,
initialSpringVelocity: velocity,
options: [.autoreverse, .repeat],
animations: {
targetView.transform = CGAffineTransform(translationX: 0, y: -moveDistance)
},
completion: nil
)
}
static func removeBouncingAnimation(from targetView: UIView) {
UIView.animate(withDuration: 0.3) {
targetView.transform = .identity
}
}
}
2차 시도(성공)
MainViewController.swift
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
AnimationHelper.addBouncingAnimation(to: imageView)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
AnimationHelper.removeBouncingAnimation(from: imageView)
}
AnimationHelper.swift
class AnimationHelper {
static func addBouncingAnimation(to targetView: UIView) {
let moveDistance: CGFloat = 30 // 이동 거리
let duration: TimeInterval = 1.6
let damping: CGFloat = 1
let velocity: CGFloat = 0
let animationKey = "bouncingAnimation"
if targetView.layer.animation(forKey: animationKey) == nil {
let animation = CABasicAnimation(keyPath: "transform.translation.y")
animation.fromValue = 0
animation.toValue = -moveDistance
animation.duration = duration
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.autoreverses = true
animation.repeatCount = .greatestFiniteMagnitude
animation.isRemovedOnCompletion = false
targetView.layer.add(animation, forKey: animationKey)
}
}
static func removeBouncingAnimation(from targetView: UIView) {
let animationKey = "bouncingAnimation"
if targetView.layer.animation(forKey: animationKey) != nil {
targetView.layer.removeAnimation(forKey: animationKey)
}
}
}
2차 시도에서 애니메이션이 성공한 이유는 UIView.animate를 사용한 방식에서 CABasicAnimation을 사용한 방식으로 변경했기 때문인데, 두 가지 방법의 차이점과 왜 2차 시도가 성공했는지를 설명해보자면,
일단 1차(기존 코드), 2차 시도에는 UIView.animate 메서드를 사용하여 애니메이션을 추가했는데 UIView.animate은 UIView의 애니메이션 API로 애니메이션 상태를 관리가 적절하지 않을 경우 중간에 View가 사라지거나, 상태가 초기화되지 않는 문제가 발생할 수 있다는 문제점이 있었다.
그래서 이를 CABasicAnimation으로 변경하여 애니메이션을 추가해 봤다. CABasicAnimation에 경우 독립적인 애니메이션 처리가 가능해서 애니메이션이 UIView의 상태와 별개로 작동하게 되고, 키를 사용해서 애니메이션을 추가/삭제할 수 있다. 그렇기 때문에 애니메이션 상태를 보다 명확하게 관리 할 수 있게 되어 다른 페이지를 이동했다 돌아와도 애니메이션이 멈추지 않게 구현할 수 있었다.
※ 참고한 블로그
Firebase Firestore 컬렉션의 문서를 삭제할 시 하위 컬렉션 삭제는 불가능한 문제에 대하여
현재 이 사람은 채팅 기능이 있는 앱을 만들고 있다. Realtime Database 사용을 위해 Firebase를 사용하고 있다. 그리고 현재 당면한 과제는 채팅방에서 모든 인원이 나가게 되면 이 채팅방을 어떻게 처
pongdangovo.tistory.com
'개발자가 상팔자 > [ Team ] 리모리모, 집중하란 마리모' 카테고리의 다른 글
| Team Project : 리모리모 2차 ~ 3차 업데이트 - 피드백 수정 및 기능 추가 (0) | 2024.07.04 |
|---|---|
| Team Project : 리모리모의 리젝 수난기 (1) | 2024.07.01 |
| Team Project : 리모리모, 집중하란 마리모 (2) | 2024.06.24 |