개인공부

WanderBoard _ 최종프로젝트 회고

h_luz 2024. 7. 12. 00:23

 

WanderBoard _ 여행 기록 어플

 

초기 기획

맨 처음 아이디어를 냈을 때는 여행을 계획하고 기록하는 어플을 제작하고자 했다

당시 생각했던 주요 기능은 맵으로 경로를 표시할 수 있고, 캘린더로 여행일정과 여행 계획을 기록하여 여행 카운트 다운,, 꼭 하고 싶었다...

여행에 맞는 시원한 색감으로 생각했음!

 

그러나 6주라는 시간 안에 이렇게 많은 기능을 구현하기는 어려울 것이라고 생각해서 우선 기록에 중점을 두고, 시작하기로 하고, 계획 부분은 나중에 시간이 남으면 추가하기로 하고, 우선은 여행 기록 어플로 결정했다!

 

초기 화면 흐름도와 와이어프레임!

 

 

최종 기획

 

여행 사진과 사진 촬영 장소를 지도에 보여주고, 글로도 기록하고, 지출까지 함께 기록할 수 있는!

다른 사람들의 여행을 구경하며, 다음엔 어디로 여행 갈까? 아이디어를 얻을 수 있는 앱. 그런 기획으로 탄생됐다 ㅎㅎ

바로 WanderBoard :)

 

게시글에 이목을 집중시키기 위해 블랙 앤 화이트를 메인 컬러로, 다양한 명도의 그레이를 서브 컬러로 선정.

앱 로고는 다양한 지형의 자연경관을 상징하고자 구불거리는 결 모양을 중첩하여 표현했고, 기록은 중점으로 하기로 해서 여행의 마침표를 완더보드를 찍자는 의미로 마침표 같은 맵 아이콘을 넣은 로고!

그리고 최대한 아이폰에서 기본으로 제공하는 것들을 사용해서 만들기로 했다

 

 

한 주 동안은 계속 회의를 하면서 디자인을 했는데 다들 열정이 넘치시고, 창의력도 풍부하시고, 최종 프로젝트인 만큼 적극적이셔서 머리가 과부하될 때도 많고, 서로 하고자 하는 것을 오해할 때도 많았지만 그래도 건강한 토론들이 오갔고, 재밌었다! 

 

무엇보다 나는 정말 나쁜 의견이 아니면 반대하지도 않는 성격이고, 자존감도 낮고,,,

생각하는 것이나 선택하는 것에 신중하고, 느린 편이라서,,

내 의견을 잘 말하지 못하는 경우가 많은데 우리 팀원들은 정말 잘 들어주시기도 하고, 기다려주시고,,

서로 이해가 안 되면 자유롭게 되물어보시고, 또 물어보면 잘 대답해 주시고 그러셔서

내 의견을 이야기하고, 다른 사람의 의견을 듣고, 또 그것에 대해서 이야기하는 부분에도 많이 성장한 거 같다..

(그래도 아직 많이 부족하고 경험이 더 필요하다는 생각이 든다)

 

여하튼 이번에 사람들을 잘 만나서 좋은 토론 경험을 쌓았다고 생각한다! 앞으로도 이런 토론은 많이 해보면 좋을 것 같다는 생각이 들었다

 

 

프로젝트에서 맡은 부분

 

난 메인페이지와 마이페이지를 맡았는데

초기에는 메인페이지랑 마이페이지 UI를 동일하게 가려고 했었다

그런데 디자이너 튜터님이 다른 역할을 하는 페이지에서는 다른 게 확실히 보이는 게 좋다고 하셔서 변경되었다!

 

지금은 이렇게 간단하게 말할 수 있지만 당시에는,, 튜터님 피드백이 들어오면 그때부터,,, 회의가 끝나질 않았던 거 같다...

 

이건 최종 와이어프레임 정리본이고, 나는 초반에는 Explore 페이지와 My trips 페이지를 맡았다! (+ Search 페이지)

사실 초반에는 Explore / My trips / Search 모두 같은 화면을 쓰기로 했어서.. 이렇게 맡았는데 점점 회의하고, 바뀌면서 구현해야 할 기능이 많아져서 정말.. 쉽지 않았다... 사실 UI적인 부분은 그리 어렵지 않았다! 내가 맡은 부분은 어려운 기능도 많이 없고,,

그래도 나는 실력이 부족해서,, 주말 없이 밤잠 설쳐가며 달렸다.....

 

어려웠던 부분

 

무한 스크롤 ( 30개씩 무한 스크롤 )

진짜 이랬다... ㅠㅠ

: 전 프로젝트 마지막 단계 과제였는데 내가 거기까지 못했어서.. ㅠ 처음 해보는 부분이라서 어렵기도 했고,

메인페이지를 구현할 때 테이블뷰에 컬렉션뷰를 넣어놨는데, 그래서 뭔가 무한 스크롤하는 시점을 잡는 것도 어려웠다..

그래서 컬렉션 뷰 하나로 구성된 Search페이지부터 무한 스크롤을 구현하고, 넘어왔는데 더 헷갈렸다ㅜ

(성공했을 때 매우 기뻤음 ㅜ 구현하고 나면 별 거 아닌 거 같아 보이는데 ,, 왜 이걸로 시간을 이만큼 썼지.. 하는 생각이 들지만 그때는 진짜 왜 안되지.. 하면서 머리를 부여잡았다... 눈물 줄줄 흘리며 코딩하던 기억이... )

extension ExploreViewController: RecentTableViewCellDelegate {
    func recentTableViewCell(_ cell: RecentTableViewCell, didSelectItemAt indexPath: IndexPath) {
        let selectedPinLogSummary = cell.recentLogs[indexPath.item]
        
        let detailVC = DetailViewController()
        detailVC.pinLogId = selectedPinLogSummary.id
        detailVC.delegate = self
        let navController = UINavigationController(rootViewController: detailVC)
        navController.modalPresentationStyle = .fullScreen
        present(navController, animated: true)
    }
    
    func loadMoreRecentLogs() {
        guard let lastSnapshot = lastSnapshot, !isLoading else {
            return
        }
        
        isLoading = true
        
        pinLogManager.fetchMoreData(pageSize: 30, lastSnapshot: lastSnapshot) { result in
            self.isLoading = false
            switch result {
            case .success(let (newLogs, newSnapshot)):
                let filteredNewLogs = self.filterBlockedAndHiddenLogs(from: newLogs)
                self.recentLogs.append(contentsOf: filteredNewLogs)
                self.recentLogs.sort { $0.createdAt > $1.createdAt }
                self.calculateRecentCellHeight()
                self.lastSnapshot = newSnapshot
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                    if let cell = self.tableView.cellForRow(at: IndexPath(row: 1, section: 0)) as? RecentTableViewCell {
                        cell.updateItemCount(self.recentLogs.count)
                    }
                }
            case .failure(let error):
                print("Failed to fetch more data: \(error.localizedDescription)")
            }
        }
    }
}

fetchMoreData와 애증의 관계가 되었다...

 

라지 타이틀 (내비게이션 다루기)

쉽지 않았지만 너무 예뻐서 포기할 수가 없었따... 안되면 되게해야죠... 그쵸...

: 악... 라지 타이틀.. 우리 팀의 트러블 슈팅에서 빼놓을 수 없는 존재인 거 같다..

이거 때문에 팀원들과 라지 타이틀을 뺄지 말지 엄청 고민하고, 회의했다.. ㅜ

우선 라지타이틀은 기본적으로 제공하는 기능이기 때문에 커스텀이 쉽지 않았다.. ㅠ  ( 양 옆에 여백 주고 싶어서 엄청 고민했다.. )

 

그리고 내비게이션 바에 버튼 넣기..

처음에 버튼을 구현할 때도 어디 블로그에서 본 대로 따라 했었다.

그 버튼을 커스텀했었는데 내가 내비게이션바에 뷰를 만들고, 거기에 버튼을 얹었는데

다음 페이지 넘어갈 때 버튼만 hidden 했더니 그 뷰가 다음 페이지까지 따라가서  다른 버튼을 가리고 난리도 아니었다... 

 

그리고 라지 타이틀 내비게이션바와 또 다른 내비게이션바가 겹쳐져서.. 또 문제를 맞이하고,, 

결국 다 해결했지만! ㅠ 정말 쉽지 않았다... 어디서부터 문제인지 찾는 것도 너무 어려웠던 거 같음..

그래도 예쁘지요..? 우하하...

 

 

검색 기능

: 전에 개인 과제할 때 해봤어서 그래도 조금 괜찮았는데,, 그래도,, 검색 기능 어려웠다..

그리고 현재 검색 기능 잘 작동하기는 하는데,,

그리고 현재는 지역으로만 검색가능한데 주소로도 검색할 수 있으면 좋겠다고 생각하는 중...

 

페이지 컨트롤러 버튼을 이용한 화면 전환 연결 (SwiftUI로 구현된 버튼 UIHosting 해서 사용)

 

UIKit과 SwiftUI를 연결하는 게 쉽지 않았다... ㅜ

클로저를 사용해서 버튼에 따라서 페이지가 전환되는 부분은 구현했는데

페이지가 전환되면 버튼이 이동되는 부분은 클로저로 구현이 안 돼서.. delegate로도 시도해 보고, 하다가,,

결국 페이지 전환 시 버튼 이동하는 부분은 NotificationCenter 사용해서 연결해 줌 ㅠ

 

var pageControlButton: UIHostingController<PageControlButton>!

func setupPageControlButton() {
    let pageControlButtonView = PageControlButton(onIndexChanged: { [weak self] index in
        self?.navigateToPage(index: index)
    })
    pageControlButton = UIHostingController(rootView: pageControlButtonView)
    addChild(pageControlButton)
    view.addSubview(pageControlButton.view)
    pageControlButton.didMove(toParent: self)

    pageControlButton.view.backgroundColor = .clear

    pageControlButton.view.snp.makeConstraints {
        $0.leading.trailing.equalToSuperview().inset(90)
        $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(15)
        $0.height.equalTo(70)
    }
}

 

 

그라데이션 뷰

private func setGradient() {
    let maskedView = UIView()
    maskedView.backgroundColor = view.backgroundColor
    view.addSubview(maskedView)

    maskedView.snp.makeConstraints {
        $0.bottom.leading.trailing.equalToSuperview()
        $0.height.equalTo(40)
    }

    let gradientLayer = CAGradientLayer()
    gradientLayer.frame = view.bounds
    gradientLayer.colors = [UIColor.clear.cgColor, UIColor.white.withAlphaComponent(0.98).cgColor, UIColor.white.cgColor, UIColor.white.cgColor]
    gradientLayer.locations = [0, 0.05, 0.8, 1]

    maskedView.layer.mask = gradientLayer
    maskedView.isUserInteractionEnabled = false
}

 

필터 기능 ( 내가 쓴 글 / 남이 나를 태그 한 글 / 내가 핀으로 저장한 글)

 

핀 버튼 기능

: 누르면 pinCount 하고, 다시 들어왔을 때도 핀 버튼이 누른 상태가 유지되도록 구현하는 게 어려웠다...

 

회원 탈퇴 기능

: 탈퇴할 때 계정 인증이 다시 필요하다고 하고,, 내 모든 기록을 지워야 하고 (내가 쓴 글, 태그 된 글에 내 정보, 내가 핀으로 저장한 정보, 로그인 정보 등등..) 어려웠다.. 

 

파이어 베이스 연동 (프로필 사진 가져오기)

: 파이어베이스는 처음이라.. 공부하면서 기간 안에 구현하려고 하니까 어려웠 다...

 

기본 프로필 만들기
(닉네임 설정 시 랜덤 배경 색에 닉네임에 앞 두 글자를 따서 기본 프로필을 만들어주는 기능 / 프로필 이미지도 원하는 사진으로 변경 가능)

: 경우의 수가 진짜 많았다.. 이럴 때는 되고, 이럴 때는 안되고,, 그래서 경우의 수를 찾아가면서 팀원과 함께 구현했다. (UIGraphicsImageRenderer 사용)

 

인상 깊었던 점

 

화면 크기에 맞춰서 글씨가 작아지는 기능

private let localLabel = UILabel().then {
    $0.font = UIFont.systemFont(ofSize: 20)
    $0.textColor = .white
    $0.textAlignment = .center
    $0.numberOfLines = 2
    $0.adjustsFontSizeToFitWidth = true
    $0.minimumScaleFactor = 0.7
}

$0.adjustsFontSizeToFitWidth = true
$0.minimumScaleFactor = 0.7

좀 더 복잡할 줄 알았는데 간단해서 신기했다..

 

SwiftUI

: 화면전환 버튼도 그렇고, 애니메이션 구현 할 때 SwiftUI를 사용 -> 예쁘기도 하고, 간단해 보여서 배워보고 싶다고 생각했다

 

전체적으로 탭바를 넣지 않고, 구현했다.

: 실제로 최종발표 때 인상 깊었다고 칭찬 많이 들었다! 우리는 이미지를 중요하게 생각하는 앱의 특성만큼 화면을 가리는 부분을 최소화하기 위해서 탭바를 없앴고, 탭바를 없앤 만큼 간편하게 화면 전환할 수 있게 페이지 컨트롤러 버튼에 신경을 많이 썼다ㆅㆅ

 

이것들 외에도 정말 많은 일이 있었지만! 이만 줄여보려고 합니다..

 

무엇보다 기획 디자인 코딩해서 앱 출시까지 한 멋진 경험!

(완성된 어플도 너무 마음에 든다! 물론 오류가 없진 않지만... )

 

완성해 보니 이걸 어떻게 했지.. 하는 생각이 들고, 너무 뿌듯하다,,

 

-

팀원들과 추려본 주요 트러블 슈팅들

 

1. DetailView, InputView 리뉴얼

 

초기 디자인은 게시글을 클릭하면 대표 이미지, 지역, 날짜, 기록이 먼저 보이고, 모달 뷰를 스크롤하여 사진, 맵, 가계부 기능, 태그 된 여행 메이트를 확인하는 구조. But! 주요 기능들이 분산되어 한눈에 보이지 않고, 구성이 깔끔하지 않다는 내부 의견으로 시작해서 회의를 통해 리뉴얼 진행.

우선 주요 기능들을 한 화면에 밀집시켜 사용자가 쉽게 접근할 수 있도록 했다.

특히, 사진 속 장소를 언제든 확인할 수 있도록 맵 기능 버튼을 상단 핀 버튼 옆에 배치

 

2. 프로필 UI 설정

초기엔 프로필 사진을 설정하지 않은 유저는 사람 아이콘 -> 많은 유저가 프로필을 설정하지 않을 경우 닉네임이 없으면 구별에 어려움

해결 -> 랜덤 그레이베이스 컬러와 닉네임의 앞 두 글자를 혼합한 이미지가 기본 프로필이 되도록 구성 

+) 프로필 이미지 클릭 시 프로필 이미지와 닉네임을 제공한 뷰가 나와서 프로필을 더 자세히 보고자 하는 사용자들의 요구 반영

 

3. 리젝 1

애플 정책을 살펴보면 로그인이 되지 않아도 사용될 수 있도록 앱이 구성되는 게 좋고, 앱 내 계정 삭제도 제공되어야 한다는 정책이 있었고, 이를 바탕으로 로그인 정보 없이 앱 접속이 되지 않은 점으로 리젝 -> 로그인 없이도 메인 페이지를 구경할 수 있고, 로그인이 필요한 부분에서 로그인을 유도하도록 앱 플로우 변경

 

4. 리젝 2

아이폰 정책 상 폰트는 11 이상 이어야 하는데 그거보다 작은 글씨들이 존재해서 리젝을 당했다

이것 외에도 버튼의 크기가 44*44 이상 이어야 하는데 작은 버튼들도 존재해서 버튼들을 대폭 수정했다.

 

* 나중에 현직자님께 피드백을 들었을 때, 사용자 경험을 높이기 위해서 사용자가 설정에서 폰트의 사이즈를 변경할 수 있도록 구현하는 것도 좋은 방법이 될 것이라고 말씀해 주셨다.

 

5. 비효율적인 모델링 구조

메인 페이지에 필요한 데이터 정보는 대표이미지, 프로필 이미지, 지역명칭, 날짜 등 10가지가 되지 않는 정도이다.

그러나 firebase에서 데이터를 불러오는 모델링이 저장에서 사용한 모델을 그대로 사용하게 되면서 불필요한 데이터까지 모드 불러오는 방식이었다. 데이터가 많아질 경우 문제가 될 수도 있어서 PinLogSummary를 만들어 필요한 데이터만 불러올 수 있도록 별도의 데이터 모델링을 설정하고 해당 내용을 호출하는 방식으로 변경. -> 최적화 후 처음 접속 했을 때 사용하는 데이터의 양이 1.14기가바이트에서 380메가바이트로 효율성을 높일 수 있었다.

 

6. 강한 참조 순환으로 인한 Memory Leak 발생

핀로그로 이동하고 나오면 메모리가 자동적으로 해제되지 않고 쌓이는 현상이 발생했고 다른 핀로그를 확인할 때마다 그 메모리가 쌓여 앱을 사용할수록 굉장히 많은 메모리 사용량이 누적 ->  메모리가 누적되는 시점을 추적한 결과 핀로그 내 제어 메뉴에서 클로저를 활용해 메서드를 호출하고 있지만 강하게 참조되어 메모리가 해제되고 있지 않은 점을 파악 -> weak self를 추가해 참조를 약하게 수정해서 해결

강한 참조 순환이 일어난다는 점은 이론적으로 많이 접해봤고 유의해야 한다는 점은 배웠지만 직접 프로젝트에서 해당 내용을 겪으니

메모리 누수를 구현 단계에서 신경 쓰면서 해야 하는지 실제로 몸으로 느낄 수 있는 좋은 경험이었습니다. (메모리 누수가 난 곳을 찾기 어려웠다..)

 

 

물론 프로젝트를 진행하면서 더 많은 일이 있었겠지만 여기까지 ~~


 

 

그리고 별 건 아니지만 프로젝트하면서 적었던 TodoList 모음본..

 

2

 

3

 

프로젝트를 정말 끝까지 수정하고, 보완하느라 유저피드백도 받을지 말지 고민 많이 했었는데

결국 유저피드백까지 완료한 우리 팀원들 진짜.. 다들 너무 고생하셨다는 생각이 들고, 감사하기도 하고 ㅠ

절대 잊지 못할 경험이었다.. ㅠ 우리 팀원들 다 취업하게 해 주세요.. 다들 너무 대단하신 분들이니까ㅠㅠ

 

이것 외에도 미처 적지 못하고 진행된 부분들이 많은데 진짜 고생했다는 생각이 든다...

그래도 저거 체크할 때 쾌감이 진짜.. 잊을 수 없다... 성취감 무슨 일..

 

하면 할수록 오류나 생각해야 할 부분들이 많은 것 같다.. 개발의 길은 끝이 없구나...

'개인공부' 카테고리의 다른 글

SwiftUI _ @State, @Binding  (1) 2024.07.22
SWiftUI _ MapKit  (1) 2024.07.20
SwiftUI  (0) 2024.07.07
UIHostingController _ 페이지 컨트롤러 호스팅  (0) 2024.07.06
이미지 캐싱 _ Kingfisher  (0) 2024.07.06