Loading the catalog…
Loading the catalog…
DAY 36 | 개인 프로젝트 — viewmodel & riverpod (4) 작업 일정 #35 [state] ProfileNotifier 및 프로필/스케치 아카이브 구현 #36 [state] SettingsNotifier 및 계정/앱 설정 관리 구현 #35 [state] ProfileNotifier 및 프로필/스케치 아카이브 구현 프로필 화면까지 하다 보니 친구 목록 화면이랑 사용자 검색 화면이랑 프로필 화면에서 코드 중복이 발생하는 것을 발견했다. 하는 일이 완전히 동일하지는 않지만 거의 비슷한 녀석들. Gemini를 통해 비교해 보자면 다음과 같다. ViewModel 들고 있는 상태 (State) 친구 수락/취소 시 일어나는 일 FriendsViewModel List<UserSummary> friends List<UserSummary> receivedRequests 받은 요청 리스트에서 제거하고, 친구 리스트에 추가 UserSearchViewModel Set<String> friendIds Set<String> sentRequestIds Set<String> receivedRequestIds ID Set에서 add / remove UserProfileViewModel Friendship? friendship List<SketchPost> sketches 단일 관계 객체 변경 및 상대방 스케치 목록 즉시 조회 FriendsViewModel , UserSearchViewModel , UserProfileViewModel 세 곳을 비교해 보면 겉모습은 똑같이 friendshipRepository.acceptFriendRequest(...) 등을 호출합니다. 하지만 호출 결과를 자기 State에 반영하는 방식(State Mutation) 이 화면마다 완전히 다릅니다. 즉, "화면마다 보여줘야 하는 데이터 형태가 다르기 때문에, 각 ViewModel이 자기 화면의 상태를 즉시 갱신(낙관적 UI 업데이트)하느라" Repository를 부르는 코드가 복붙처럼 중복된 것입니다. 현재 방식의 잠재적 문제점: "상태 불일치 (State Inconsistency)" 이 방식은 코드가 중복될 뿐만 아니라 더 큰 문제가 있습니다: 프로필 화면( UserProfileScreen )에서 친구를 맺거나 차단했는데, 뒤로가기를 눌러 검색 화면이나 친구 목록 화면으로 돌아갔을 때, 만약 깜빡하고 ref.invalidate(...) 를 호출하지 않으면 화면마다 친구 상태가 제각각 다르게 보이는 '상태 불일치' 버그가 생깁니다. Flutter 및 Riverpod 실무에서 이 중복을 말끔히 해결하는 대표적인 방법은 "조회(Query)와 변경(Command)의 분리 (CQS 패턴)" 입니다. 관련 개념 단일 진실 공급원 (Single Source of Truth, SSOT) 원칙 관심사의 분리 (Separation of Concerns)와 계층화 (Layering) 단방향 데이터 흐름 (Unidirectional Data Flow, UDF) 및 CQS 패턴 코드 유지보수성 및 실무 규모 확장성 (Scalability) 따라서 친구 요청을 보내고 취소하고 수락하고 거절하는 등의 기능은 ViewModel에서 개별적으로 작성하는 게 아니라 UseCase로 따로 빼기로 했다. "1 UseCase = 1 Action" 원칙에 따라 친구 요청, 친구 요청 취소, 친구 요청 수락, 친구 요청 거절, 친구 삭제, 차단, 차단 해제에 대한 UseCase를 작성한다. 상대와의 관계에 따라 화면에 보이는 게 달라져 조건문 분기를 사용했다. 한 줄 소개 및 스케치 게시물은 친구 공개이므로 친구가 아닌 자에게는 보여주지 않는다. 한 줄 소개 위치에 친구가 아닌 경우에는 친구 요청 버튼이 뜬다. 요청 상태에 따라 친구 요청, 수락 및 거절, 요청 취소 버튼이 뜬다. 친구일 경우에는 실수로 친구 삭제를 누르지 않도록 친구 삭제 버튼은 메뉴 안에 숨겨져 있다. route가 꼬인 부분이 있어서 살펴 보니 검색해서 얻은 자료에서 사용하던 방식이 내 프로젝트와 차이가 있는데 제대로 검토하지 않고 사용한 여파였다. 어디에는 UID 기반으로 되어 있고 어디에는 username 기반으로 되어 있는 것을 정리해 주니 문제가 해결되었다. 하는 김에 나중에 리팩토링 하려고 했던 route 작업도 같이 처리했다. #36 [state] SettingsNotifier 및 계정/앱 설정 관리 구현 Mock model이 남아 있는 [차단한 사용자 괸리]부터 작업하였다. 여기서도 다른 곳처럼 ViewModel에서 사용할 State class를 만들고 작업을 하다가 엎엇다. 차단 해제에 대한 것도 FriendshipActionHandler 에 묶어 버리는 게 나을 것 같다. 당장은 차단된 사용자는 검색에서 빠져 있기에 [차단한 사용자 관리]에서밖에 차단 해제를 안 하겠지만, 딥링크로 사용자 프로필에 접속하는 경우가 발생할 경우 차단된 사용자 프로필 메뉴에서도 차단 해제를 할 수 있도록 하는 게 UX적으로 더 나으니까. ViewModel에서 차단 해제를 처리했다면 차단한 사용자 목록 blockedUsers 와 작업 진행 중 여부 isProcessing 을 담는 State class가 필요했겠지만 작업 수행을 FriendshipActionHandler 에서 하면 blockedUsers 만 있으면 되므로 따로 State class를 생성하지 않고 List<UserSummary> 를 사용하기로 했다. [차단된 사용자 관리] 페이지를 작업하다 보니 검색에서는 차단된 사용자를 제외했지만 차단된 사용자가 친구의 스케치에 댓글을 달았을 때에 대한 처리가 되어 있지 않다는 것을 인지했다. 삭제된 댓글과 마찬가지로 답글이 없다면 화면에서 완전히 가리고 답글이 있다면 차단된 사용자의 댓글이라고 표시되도록 수정하였다. 기본적으로 모든 View가 ViewModel과 연결되고 ViewModel이 Repository와 연결되며 Repository가 Service로 연결되는 구조가 MVVM 아키텍처의 흐름이다. 경우에 따라서는 UseCase가 끼기도 하지만. 하여간 기본적으로는 이를 따르되, 설정 화면에서는 예외를 두기로 했다. 독자적인 비즈니스 상태를 갖는 게 아니라 이미 메모리에 존재하는 전역 로그인 세션의 단일 속성을 변경하는 역할을 수행하므로, 설정 화면마다 ViewModel을 두면 사용자 세션에 대한 상태의 이중화 및 동기화 오버헤드가 발생할 수 있다는 이슈가 있다. 교과서적인 아키텍처 100%보다는 상황에 따라 적절한 trade-off를 고려하여 조정하는 것이 낫다는 판단으로, MVVM 구조의 완성보다는 단일 진실 공급원 원칙을 우선시하기로 했다. 일단은 소셜 로그인을 배제한 상태로 설정 기능을 마무리하고, 소셜 로그인 기능을 추가할 때 설정 화면의 기능 구현을 마무리하도록 하겠다. 최초로 소셜 로그인을 하였을 때 필요한 페이지도 새로 만들어야 하고 처리할 게 좀 있으니 설정 화면 기능 구현과 별개로 따로 빼는 게 나을 것 같다. 이용약관 및 개인정보 처리방침도 그 이후로 넘긴다. >>> GitHub Repository at this point (111b9df)
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
DAY 36 | 개인 프로젝트 — viewmodel & riverpod (4). DAY 36 | 개인 프로젝트 — viewmodel & riverpod (4) 작업 일정 #35 [state] ProfileNotifier 및 프로필/스케치 아카이브 구현 #36 [state] SettingsNotifier 및 계정/앱 설정 관리 구현 #35 [state] ProfileNotifier 및 프로필/스케치 아카이브 구현 프로필 화면까지 하다 보니 친구 목록 화면이랑 사용자 검색 화면이랑 프로필 화면에서 코드 중복이 발생하는 것을 발견했다. 하는 일이 완전히 동일하지는 않지만 거의 비슷한 녀석들. Gemini를 통해 비교해 보자면 다음과 같다. ViewModel 들고 있는 상태 (State) 친구 수락/취소 시…
Open source