{"id":24281556,"url":"https://github.com/roy-wonji/devlopgram","last_synced_at":"2026-05-19T09:07:43.094Z","repository":{"id":38194547,"uuid":"485753208","full_name":"Roy-wonji/DevlopGram","owner":"Roy-wonji","description":"개발그램","archived":false,"fork":false,"pushed_at":"2022-07-03T12:09:38.000Z","size":63639,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-23T20:06:37.867Z","etag":null,"topics":["clean-architecture","clean-code","codebase","ios13","mvvm","mvvm-pattern","objective-c","swift","xcode"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Roy-wonji.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-04-26T11:18:12.000Z","updated_at":"2023-01-30T14:53:14.000Z","dependencies_parsed_at":"2022-08-18T18:00:36.882Z","dependency_job_id":null,"html_url":"https://github.com/Roy-wonji/DevlopGram","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Roy-wonji/DevlopGram","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Roy-wonji%2FDevlopGram","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Roy-wonji%2FDevlopGram/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Roy-wonji%2FDevlopGram/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Roy-wonji%2FDevlopGram/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Roy-wonji","download_url":"https://codeload.github.com/Roy-wonji/DevlopGram/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Roy-wonji%2FDevlopGram/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264795838,"owners_count":23665241,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clean-architecture","clean-code","codebase","ios13","mvvm","mvvm-pattern","objective-c","swift","xcode"],"created_at":"2025-01-16T03:00:15.954Z","updated_at":"2026-05-19T09:07:38.069Z","avatar_url":"https://github.com/Roy-wonji.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"## ⛺️개발그램(출시 예정)\n\n### 목차\n- [프로젝트](#projects)\n- [UML](#uml)\n- [실행 화면](#실행-화면)\n- [키워드](#키워드)\n- [구현 내용](#구현내용)\n    - [고민했던점 \u0026\u0026 로직구현](#고민했던점--로직구현)\n- [배운개념](#배운개념)\n\n\n### 개발환경 및 라이브러리\n[![swift](https://img.shields.io/badge/swift-5.0-orange)]()\n[![xcode](https://img.shields.io/badge/Xcode-13.0-blue)]()\n\n### Projects\n### 개발자를 위한 인스타그램 프로젝트  \n🗓 프로젝트 소개 :개발자를 위한 인스타그램 프로젝트 !\u003c/br\u003e\n🗓 기간 : 2022.04.26 ~   \u003c/br\u003e\n🗓 팀원: [로이](https://github.com/Roy-wonji) \u003c/br\u003e\n🗓 리뷰어: [릴리](https://github.com/yeahg-dev) \u003c/br\u003e\n🗓 리뷰: [PR](https://github.com/Roy-wonji/DevlopGram/pulls)\n\n\n### UML\n\n![Group 2225-min](https://user-images.githubusercontent.com/75601594/172851770-976b2531-eb77-403b-bd59-a3aba18a760e.jpg)\n\n\u003c/br\u003e\n\n### UI \n\n![Group 5](https://user-images.githubusercontent.com/75601594/172876715-8473e30a-5933-4740-b376-affcc355f0c0.jpg)\n\n\n\n### 실행 화면\n\n![KakaoTalk_Video_2022-06-04-14-46-18](https://user-images.githubusercontent.com/75601594/172851799-a3100b59-8418-47fe-bb95-8085c17ea82b.gif)\n\n\u003c/br\u003e\n\n### 키워드 \n - `코드베이스UI`\n - `UICOLLECTIONVIEW`\n - `UINAVIGATION`\n - `비동기 , 동기`\n - `UINAVIGATION`\n - `MVVM 디자인 패턴 `\n - `Firebase 로그인 `\n - `Firebase 사용방법 `\n - `Firebase 로그인한 유저의 정보 저장 `\n - `custom button , textfield`\n - `확장자로 빼서 레이아웃 `\n - `GCD`\n     - `DispatchQueue`\n     - `DispatchGroup`\n- `searchbar`\n- `로그인 / 로그아웃`\n- `다크모드`\n\n### 구현내용\n- 코드 베이스로 UI구현\n- 앱을 비동기 동기 과정을 사용해서 구현 \n- MVVM 디자인 패턴을 사용해서 앱을 최대한 클린 아키텍쳐로 구현\n- UICollectioview를 코드로 구현 \n- 파이어 베이스 연동 및  podfile을 사용해서 앱에 추가하는 걸 구현 \n- 검색창에 등록한 프로필 구현 \n- 프로필을 누르면 업로드한 사진 별로 올라가게 구현 \n- 검색할때 이름 순으로 필터를 걸어서 구현 \n- 다크모드 구현 \n- 로그인 한 계정이 다른경우 프로필 사진및 이름이 변경하게 구현\n- 피드를 올리면 올린 계정에 이름이랑 피드 사진및 글이 올라가게 구현\n- 댓글을 달면 댓글 단 계정 및 이름이 timestamp로 나오게 구현\n- 좋아요를 누리면 계시글에 좋아요 카운트가 증가하면서 버튼 색상이 바뀌게 구현\n\n### 고민했던점 \u0026\u0026 로직구현 \n - 처음으로 mvc 말고 mvvm 디자인 패턴으로 구현을 하려고 하니까 view model안에는 어떤 로직을 구현을 해야 하는 어려웠습니다 . ㅠㅠㅠㅠ\n - 앱을 동작을 했을때 로그인이 되면 메인 화면으로 보이고 아니면 로그인 화면으로 구현 하는 방식을 어떨게 할지 고민을 했습니다.\n - 메인쓰레드에서 앱에 정보가 많아지면  앱이 버벅일수 있어서  버벅이는 걸  방지 하기 위해서 비동기 동기 프로그램을 사용해서 앱이 버벅이지 않고 구현을 하도록 했습니다.\n - 파이어베이스 연동및 로그인했을때 api를 연동을 하면서 데이터를 받아오고 넘겨주는 부분은  네트워킹에  api 라는 파일  구현 해서 최대한 분리 하면서 구현을 했습니다 .\n - 최대한 한 파일안에  한 구조체 및 클래스가 사용하려고 노력을 하면서 , 하드코딩을 피하면서 최대한 클린 아키텍쳐로 구현을 했습니다.\n - 프로필로 가서 사진 이 다르게 나오게 구현을 할려면  사진이 조금 늦게 올라오는건 사진이 업로드 하는과정에서 비동기적이 작업을 해야되서 비동기적인 작업을 구현을 했습니다.\n - table뷰에 가입한 계정및 이름이 순서대로 나오게 구현 및 이름을 순서대로 구현하면서 map을 핕터를 걸면서 구현을 했습니다.\n - 다크 모드를 구현을 할때 각 컬러의 set을 설정해주면서 컬러를 구현을 했습니다 \n - 다른계정으로 로그인을 하면 로그인 한 계정이름 , 사진 이 업로드 하게 되게 구현및 사진이 업로드 할때는 비동기 처리로 구현을 했습니다. \n - 좋아요를 눌었을때 색상을 변경하고 카운트 수 증가를 뷰델에서 구현을 했습니다\n\n### 배운개념\n#### DispatchQueue를 사용하면서  작업량이 많은 코드는 GCD로 구현을 했습니다 \n```swift\n func checkIfUserIsLoggedIn() {\n        if Auth.auth().currentUser ==  nil  {\n            DispatchQueue.main.async {\n                let controller = LoginController()\n                let  navigation = UINavigationController(rootViewController: controller)\n                navigation.modalPresentationStyle = .fullScreen\n                self.present(navigation, animated: true, completion:  nil)\n            }\n        }\n    }\n```\n\n#### 코드로 tababrcontroller 구현을 할때  tabbar를 눌렀을떄와 안눌렀을때 아이콘을 다르게 구현을 했습니다\n```swift\n private func configureViewControllers() {\n        view.backgroundColor = .white\n        let layout =  UICollectionViewFlowLayout( )\n        let feed = tempateNavigationController(unselectedImage: #imageLiteral(resourceName: \"home_unselected\"),  selectedImage:  #imageLiteral(resourceName: \"home_selected\") , rootViewController: FeedController(collectionViewLayout: layout))\n        let search = tempateNavigationController(unselectedImage: #imageLiteral(resourceName: \"search_selected\"),  selectedImage:  #imageLiteral(resourceName: \"search_selected\") , rootViewController: SearchController())\n        let imageSelector = tempateNavigationController(unselectedImage: #imageLiteral(resourceName: \"plus_unselected\"),  selectedImage:  #imageLiteral(resourceName: \"plus_unselected\") , rootViewController: ImageSelectorController())\n        let notifications = tempateNavigationController(unselectedImage: #imageLiteral(resourceName: \"like_unselected\"),  selectedImage:  #imageLiteral(resourceName: \"like_selected\") , rootViewController: NotificationController())\n        let profile = tempateNavigationController(unselectedImage: #imageLiteral(resourceName: \"profile_unselected\"),  selectedImage:  #imageLiteral(resourceName: \"profile_selected\") , rootViewController: ProfileController())\n        viewControllers = [feed , search, imageSelector, notifications, profile]\n        tabBar.tintColor = .black\n        tabBar.backgroundColor = .white\n    }\n    //MARK: - tabbar 의 이미지가 선택 되었을때랑 안선택 되었을때 이미지 선택 해주는 함수\n    private func tempateNavigationController(unselectedImage: UIImage, selectedImage: UIImage , rootViewController: UIViewController) -\u003e UINavigationController {\n        let navigation = UINavigationController(rootViewController: rootViewController)\n        navigation.tabBarItem.image = unselectedImage\n        navigation.tabBarItem.selectedImage = selectedImage\n        navigation.navigationBar.tintColor = .black\n        return navigation\n    }\n```\n#### 파이어베이스 로그인을 했을때 로그아웃을 했을떄서버와 통신이 안되면 에러 처리를 프린트를 해주었습니다  \n\n```swift\n @objc func handleLogin() {\n        guard let email = emailTextField.text else  { return }\n        guard let password = passwordTextField.text else  { return }\n        \n        AuthService.logUseIn(withEmail: email, password: password) { (result,  error ) in\n            if let error = error {\n                print(\"DEBUG: Falied to log user in  \\(error.localizedDescription)\")\n                return\n            }\n            self.dismiss(animated: true, completion: nil)\n        }\n    }\n\n @objc func handleSignUp() {\n        guard let email = emailTextField.text else  { return }\n        guard let password = passwordTextField.text else  { return }\n        guard let fullname = fullNameTextField.text else  { return }\n        guard let username = userNameTextField.text?.lowercased() else  { return }\n        guard let profileImage = self.profileImage else { return }\n\n        let crendentials = AuthCredentials(email: email, password: password,\n                                         fullname: fullname, username: username,\n                                         profileImage: profileImage)\n        \n        AuthService.registerUser(withCredential: crendentials) { error in\n            if let error = error {\n                print(\"DEBUG: Falied to register user \\(error.localizedDescription)\")\n                return\n            }\n            print(\"DEBUG:Sucessfully registered user with firestore...\")\n            self.dismiss(animated: true, completion: nil)\n        }\n    }\n```\n## Step2\n### 유저 업데이트 및 데이터 받아오기 \n```swift\nstruct UserService {\n    static func fetchUser(completion: @escaping(User) -\u003e Void) {\n        guard let uid = Auth.auth().currentUser?.uid else { return }\n        COLLECTION_USERS.document(uid).getDocument { snapshot, error in\n            print(\"\\(String(describing: snapshot?.data()))\")\n            guard let dictionary = snapshot?.data() else { return }\n            let user = User(dictionary: dictionary)\n            completion(user)\n        }\n    }\n}\n```\n\n### 로그인 했을때  사용자가 다르면  프로필에  업데이트 하게 구현\n```swift\n\nprotocol AuthenticationDelegate: class {\n    func authenticationDidComplete()\n}\n\nextension MainTabViewController: AuthenticationDelegate {\n    func authenticationDidComplete() {\n        fetchUser()\n        self.dismiss(animated: true, completion: nil)\n    }\n}\n\n```\n\n###  검색  탭에  searchCell 구현\n```swift\nfinal class SearchController:  UITableViewController {\n    //MARK: - Properties\n    \n    //MARK:  - Lifecycle\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        view.backgroundColor = .lightGray\n        configureTableView()\n        \n    }\n    \n    private func configureTableView() {\n        view.backgroundColor = .backgroundColor\n        tableView.register(UserCell.self, forCellReuseIdentifier: CellIdentifier.searchReuseIdentifier)\n        tableView.rowHeight = 64\n    }\n}\n\n//MARK: - UITableViewDataSource\nextension SearchController {\n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -\u003e Int {\n        return 5\n    }\n    \n    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -\u003e UITableViewCell {\n        let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier.searchReuseIdentifier, for:  indexPath)\n        cell.backgroundColor = .backgroundColor\n        return cell\n    }\n}\n\n```\n### 사용자들 수 만큼 테이블뷰에 나오게 구현\n\n```swift\n//MARK: - firebase 에서 사용자 정보 받아 오기\n    static func fetchUsers(completion: @escaping ([User]) -\u003e Void) {\n        COLLECTION_USERS.getDocuments { (snapshot, error) in\n            guard let snapshot = snapshot else { return }\n            \n            let users = snapshot.documents.map({ User (dictionary: $0.data() )  })\n            completion(users)\n        }\n    }\n\n```\n\n### 테이블 뷰에 현재 등록 되있는 계정 사진및 정보 구현\n```swift\nstruct UserCellViewModel {\n    private let user: User\n    \n    var profileImageUrl: URL? {\n        return URL(string: user.profileImageUrl)\n    }\n    \n    var username: String {\n        return user.username\n    }\n    \n    var fullname: String {\n        return user.fullname\n    }\n    \n    init(user: User) {\n        self.user = user\n    }\n}\n\n```\n\n### 검색창 구현\n```swift\nprivate var inSearchMode: Bool {\n        return searchController.isActive \u0026\u0026 !searchController.searchBar.text!.isEmpty\n    }\n```\n## STEP3\n### 댓글창 구현 \n```swift\n//MARK: - API\n    func fetchComments( ) {\n        DispatchQueue.main.async {\n            CommentService.fetchComments(forPost: self.post.postId) { comments in\n                self.comments = comments\n                self.collectionView.reloadData()\n            }\n        }\n    }\n    \n    //MARK: - UI\n    private func configureUI() {\n        configureCollectionView()\n    }\n    \n    private func configureCollectionView() {\n        navigationItem.title = \"Comment\"\n        collectionView.backgroundColor = .backgroundColor\n        self.navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.textColorAsset ?? CommentUIText.colorWrongInput]\n        collectionView.register(CommentCell.self, forCellWithReuseIdentifier: CellIdentifier.commentResueIdentifier)\n        collectionView.alwaysBounceVertical = true\n        collectionView.keyboardDismissMode = .interactive\n    }\n}\n```\n\n### 사진이 업로드 한만큼 반환\n```swift\n//MARK: - UICollectionViewDataSource\nextension ProfileController {\n    //MARK: - collectionView셀 구현\n    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -\u003e Int {\n        return posts.count\n    }\n    //MARK: - collectionView  ProfileCell 셀 등록\n    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -\u003e UICollectionViewCell {\n        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellIdentifier.profileCellIdentifier, for: indexPath) as! ProfileCell\n        cell.viewModel = PostViewModel(post: posts[indexPath.row])\n        return cell\n    }\n    //MARK: - collectionView  ProfileHeader 셀 등록\n    override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -\u003e UICollectionReusableView {\n        \n        let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CellIdentifier.headerIdentifier, for: indexPath) as! ProfileHeader\n        header.delegate = self\n        header.viewModel = ProfileHeaderViewModel(user: user)\n        return header\n    }\n}\n//MARK: - UICollectionViewDelegate\nextension ProfileController {\n    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {\n        let controller = FeedController(collectionViewLayout: UICollectionViewFlowLayout() )\n        controller.post = posts[indexPath.row]\n        navigationController?.pushViewController(controller, animated: true)\n    }\n}\n\n//MARK: - UICollectionViewDelegateFlowLayout\nextension ProfileController: UICollectionViewDelegateFlowLayout {\n    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -\u003e CGFloat {\n        return 1\n    }\n    \n    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -\u003e CGFloat {\n        return 1\n    }\n    \n    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -\u003e CGSize {\n        let width = (view.frame.width - 2) / 3\n        return CGSize(width: width, height: width)\n    }\n    \n    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -\u003e CGSize {\n        return CGSize(width: view.frame.width, height: 240)\n    }\n}\n//MARK: - ProfileHeaderDelegate\nextension ProfileController: ProfileHeaderDelegate {\n    func header(_ profileHeader: ProfileHeader, didTapActionButton user: User) {\n        if user.isCurrentUser {\n            print(\"DEBUG: Show edit profile here\")\n        } else if user.isFollowed {\n            UserService.unfollowUser(uid: user.uid) { error in\n                self.user.isFollowed = false\n                self.collectionView.reloadData()\n            }\n        } else {\n            UserService.followUser(uid: user.uid) { error in\n                self.user.isFollowed = true\n                self.collectionView.reloadData()\n            }\n        }\n    }\n}\n```\n### 파일업로드 및 팔로우수 구현 \n```swift\n    private func updateAPI() {\n        DispatchQueue.main.async {\n            self.checkIfUserIsFollowed()\n            self.fetchUserStatus()\n            self.fetchPosts()\n        }\n    }\n    \n    private func checkIfUserIsFollowed() {\n        UserService.checkUserIsFollowed(uid: user.uid) { isFollowed in\n            self.header.viewModel?.isFollwed = isFollowed\n        }\n    }\n    \n    private func fetchUserStatus() {\n        UserService.fetchUserStats(uid: user.uid) { stats in\n            self.user.stats = stats\n            self.collectionView.reloadData()\n        }\n    }\n    \n    private func fetchPosts() {\n        PostService.fetchPost(forUser: user.uid) { posts in\n            self.posts = posts\n            self.collectionView.reloadData()\n        }\n    }\n```\n\n\n## 고민했던점 \u0026\u0026 궁금한점 \n\nTabBarController 설정관련 코드는 어디에서 호출해야하는지\n\u003e 음... 이 부분에 대해선 로이의 생각이 먼저 궁금한데요~\nTabBar의 설정 함수의 호출 위치를 SceneDelegate와 TabBarController 두 군데로 고민하셨는데, SceneDelegate를 고민하신 이유는 무엇이고, TabBarController에서 호출한 이유는 무엇인지 궁금합니다!\n\u003e TabbarController를 SceneDelegate 에서 호출 하는 이유는 어떤 scene을 rootviewcontroller로 지정할때 같이 tabbarcontroller를 할수 있어서 SceneDelegate에서 바로 호출 한다는생각 때매 SceneDelegate에서 호출 한다는 생각을 했습니다\n\nTabBarControlle에서 하면 viewdidload에서 호출을 하면서 할수 있다고 생각이 들었습니다\n\n\n메인스레드에서 API를 호출하고 있는 것 같습니다.\n통신은 글로벌 큐로 보내어서 메인 큐는 UI업데이트 처리에 집중할 수 있도록 하는 건 어떨까요?\n\n\nViewModel의 역할\n\u003e 클린아키텍쳐에서 뷰는 뷰모델을 소유합니다. 뷰모델은 뷰로부터 이벤트를 받고, 유스케이스에서 API를 호출하여 전달받은 데이터를 뷰에 바로 뿌려줄 수 있는 형식으로 포맷팅을 하는 역할을 담당한다고 생각합니다. 특정 뷰에 보여지는 데이터를 제공하기 때문에, 뷰모델은 뷰에 종속적이지만, 뷰와 모델의 의존성을 없애주기 때문에 UI와 비지니스 로직을 분리 할 수 있고 유연한 설계를 가능하게 하고요.\n제가 생각하는 뷰모델의 역할인 2가지가 로이만의 방법으로 달성되었다고 생각합니다.\n뷰에 보여질 데이터를 처리하는 역할\n바인딩을 통해 수동적인 뷰를 만든다\n다만 개선되었으면 하는 부분이 있다면, 라인별 코멘트에도 남겼지만 뷰가 API를 호출하는 경우입니다. 뷰모델을 거치지 않고 뷰가 직접 API를 참조하게 되면 뷰와 API사이에는 의존성이 생깁니다. 뷰모델을 만들어서 의존성을 낮추고 책임을 분리해주세요!\n\n\nUI처리시 main.async를 사용하는 것\n```swift\n private func configure( ) {\n        DispatchQueue.main.async {\n            guard let viewModel = self.viewModel else { return }\n            self.captionLabel.text = viewModel.caption\n            self.postImageView.sd_setImage(with: viewModel.imageUrl)\n            self.profileImageView.sd_setImage(with: viewModel.userProfileImageUrl)\n            self.userNameButton.setTitle(viewModel.username, for: .normal)\n            self.likesLabel.text = viewModel.likesLabelText\n            self.likeButton.tintColor = viewModel.likeButtonTintColor\n            self.likeButton.setImage(viewModel.likeButtonImage, for: .normal)\n        }\n    }\n    configure가 호출되는 스레드가 메인스레드가 아닌 글로벌스레드라면 DispatchQueue.main.async로 감싸주시면 작성하신 것이 맞습니다:)\n```\n\n클린아키텍처\n\u003e 제가 공부했던 클린아키텍처 레퍼런스들에서는 API Service는 Usecase(비지니스로직)가 소유했었습니다. 그래서 뷰에서 API를 들고 호출하는 구조가 어색하게 느껴졌던 것 같아요.\n현재 소프트웨어의 각 객체들이 클린아키텍쳐의 Entity, Usecase, Presentaion 어떤 레이어에 속하나요?\n각 레이어간의 의존성에 대해 고민하고 Usecase를 추가해서 역할을 분리해보면 더 클린아키텍쳐스러워질 것 같습니다!\n\n파일구조\n\u003e 파일 구조가 현재는 Model / ViewModel / Controller.. 로 크게 MVVM으로 나뉘어져있는데요. \n각 Scene과 연관된 뷰 컨트롤러와 뷰모델, 뷰 컴포넌트가 분리되어 있어서 찾기가 조금 어려운 것 같네요🤔\nSceme별로 연관된 뷰와 뷰모델을 모아두는 방법도 있으니 참고해보시는 것도 좋을 것 같습니다~ \u003c/br\u003e\n\n\n\n\n\n## Commit 규칙\n\u003e 커밋 제목은 최대 50자 입력 \u003c/br\u003e\n본문은 한 줄 최대 72자 입력 \u003c/br\u003e\nCommit 메세지 \u003c/br\u003e\n\n🪛[chore]: 코드 수정, 내부 파일 수정. \u003c/br\u003e\n✨[feat]: 새로운 기능 구현. \u003c/br\u003e\n🎨[style]: 스타일 관련 기능.(코드의 구조/형태 개선) \u003c/br\u003e\n➕[add]: Feat 이외의 부수적인 코드 추가, 라이브러리 추가 \u003c/br\u003e\n🔧[file]: 새로운 파일 생성, 삭제 시 \u003c/br\u003e\n🐛[fix]: 버그, 오류 해결. \u003c/br\u003e\n🔥[del]: 쓸모없는 코드/파일 삭제. \u003c/br\u003e\n📝[docs]: README나 WIKI 등의 문서 개정. \u003c/br\u003e\n💄[mod]: storyboard 파일,UI 수정한 경우. \u003c/br\u003e\n✏️[correct]: 주로 문법의 오류나 타입의 변경, 이름 변경 등에 사용합니다. \u003c/br\u003e\n🚚[move]: 프로젝트 내 파일이나 코드(리소스)의 이동. \u003c/br\u003e\n⏪️[rename]: 파일 이름 변경이 있을 때 사용합니다. \u003c/br\u003e\n⚡️[improve]: 향상이 있을 때 사용합니다. \u003c/br\u003e\n♻️[refactor]: 전면 수정이 있을 때 사용합니다. \u003c/br\u003e\n🔀[merge]: 다른브렌치를 merge 할 때 사용합니다. \u003c/br\u003e\n✅ [test]: 테스트 코드를 작성할 때 사용합니다. \u003c/br\u003e\n\n### Commit Body 규칙\n\u003e 제목 끝에 마침표(.) 금지 \u003c/br\u003e\n한글로 작성 \u003c/br\u003e\n브랜치 이름 규칙\n\n- `STEP1`, `STEP2`, `STEP3`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froy-wonji%2Fdevlopgram","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froy-wonji%2Fdevlopgram","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froy-wonji%2Fdevlopgram/lists"}