refactor: 모듈 경계와 앱 내비게이션 정리 - #43
Conversation
계정 이관에 따라 팀을 N94CS4N6VR 에서 3UFKCXVTN4 로 바꾸고, Xcode 가 개발 인증서를 찾지 못하던 문제를 함께 수정한다. - ProjectConfig: teamId 를 새 팀으로 교체 - Project+Target: 타깃 레벨 recommended 기본값이 CODE_SIGN_IDENTITY 에 "iPhone Developer" 를 심어 프로젝트 설정을 덮었다. match 가 발급하는 건 Apple Development 이므로 이 키만 제외한다 - SettingDictionary: setCodeSignIdentity 기본값을 Apple Development 로 - Project+Settings: Stage 컨피그가 개발용 프로파일을 쓰도록 변경. fastlane 은 세 레인 모두 Release 컨피그로 빌드하므로 배포에는 영향이 없다 - Matchfile: API 키 환경변수명이 .env 와 달라 Apple ID 로그인으로 폴백하던 문제 수정
앱 업데이트 조회 코드가 모듈 정의 없이 소스만 존재해 다른 Data 모듈처럼 독립적으로 빌드/의존할 수 없었다. 모듈 목록과 Tuist 프로젝트를 추가한다. - DataFeatureModule 에 AppUpdate 케이스 추가 - Projects/Data/AppUpdate/Project.swift 신설(AppUpdateDomain 인터페이스, Model, NetworkModule 에만 의존)
세션 인스턴스를 Data/Auth 의 OptimizedSessionManager 가 소유해 NetworkModule 이 상위 모듈을 거꾸로 참조해야 했고, 요청 지연·실패를 관측할 지점도 없었다. 세션 보관은 NetworkModule 쪽 레지스트리로 옮기고 provider 가 요청 결과를 직접 기록하게 한다. - NetworkSessionRegistry: 인증/비인증 세션을 주입받아 보관, provider 는 여기서 세션을 꺼낸다. OptimizedSessionManager 는 configureNetworkSessions() 로 자신이 만든 세션을 등록만 한다 - NetworkTelemetry: source·method·url·statusCode·duration·성공여부 이벤트 기록 - AlamofireNetworkProvider: .value 대신 .response 를 받아 텔레메트리를 남기고 결과를 반환하도록 변경(동작 동일) - URLSession 을 직접 쓰는 두 경로도 같은 이벤트를 남긴다: 앱스토어 lookup(app_store_lookup), 투표 공유 이미지(vote_share_image)
Repository 모듈은 provider 타입을 재노출하는 것 외에 자체 책임이 없으면서 Storage·Domain 인터페이스·AudioPlayer 까지 끌고 다녀, 모든 Data 모듈이 필요 없는 의존성을 함께 가져가고 있었다. 각 모듈이 NetworkModule 만 직접 의존하도록 바꾸고 모듈을 삭제한다. - ModulePath.Datas 에서 Repository 케이스 제거, Projects/Data/Repository 삭제 - Data 9개 모듈(Attendance/Auth/Battle/Comment/Home/Notification/Perspective/ Profile/Search)과 Service/Device 의 의존성·import 를 NetworkModule 로 교체 - 각 모듈 TestSupport 의 StubNetworkProvider import 도 동일하게 교체 - DataExported: Repository·Service 재노출 제거, 중복 CommentData 정리, AppUpdateData·AttendanceData 추가 - PerspectiveService: Repository 를 통해 딸려오던 Remote 를 명시적으로 import
- Domain/Auth, Domain: AudioPlayer 서비스 의존 제거 - Domain/Home: Notification 도메인 의존 제거 - Presentation/Profile: Profile 도메인 인터페이스를 interfaceDependencies 로 이동
Kingfisher 는 자체 다운로더를 쓰기 때문에 provider 경로의 계측에 잡히지 않았다. ImageDownloaderDelegate 로 시작/완료 시각을 재 kingfisher source 이벤트로 남긴다.
의존성이 WeaveDI 컨테이너에 문자열 없이 타입으로 숨어 있어, 화면이 무엇을 필요로 하는지 코드에서 보이지 않고 resolve 실패는 런타임에만 드러났다. TCA DependencyValues 에 명시적으로 넣고 컨테이너는 인터셉터 브리지에만 남긴다. - DiRegister 삭제, AppDependencyFactory 신설. 화면 단위 Factory(Auth/Home/ Chat/Profile/Search/AppService)가 각자 필요한 의존성을 configure(&values) 로 주입 - Alamofire 인터셉터는 Store 수명 밖에서 동작하므로 KeychainManaging 과 AuthInterface 만 WeaveDI 에 등록하고, 세션·Kingfisher 설정을 configureNetworkInfrastructure() 에서 함께 수행 - UnifiedDI.resolve 호출부(푸시 토큰, 코디네이터, 루트 뷰)를 직접 참조로 교체 - App/Project.swift: 위 주입에 필요한 Domain 10종, AppUpdateData, NetworkToken, PickeStorage, Device, Analytics 의존성 추가 - AppDelegate: 텔레메트리 이벤트를 Mixpanel network_request 로 보내는 싱크 연결
동작 변경 없음. 2-space 들여쓰기와 파일 끝 개행만 맞춘다.
Project.Environment.projectName 이 static let 이라 매니페스트 파일마다 한 번씩 평가되면서, 모듈 수만큼(50여 줄) 같은 print 가 generate 로그를 덮었다. 아래 bundleIdPrefix·teamId 와 동일하게 환경변수 우선 대입만 남긴다.
DDDAttendance 처럼 Project 생성 시 IDETemplateMacros 를 심어 파일명·모듈명·생성일이 자동으로 채워지게 한다. 작성자는 기여자별로 갈리지 않도록 Picke 로 고정한다.
Interface 가 UnifiedDI 컨테이너를 조회해 실패하면 no-op 구현으로 조용히 대체하던 폴백을 걷어낸다. 주입 누락이 런타임 빈 화면이 아니라 링크 단계에서 드러난다. - Interface 는 TestDependencyKey 만 노출하고 testValue 로 Mock 을 준다 - Repository live 는 AttendanceData, UseCase live 는 AttendanceDomain 이 등록 - 한 키에 별칭으로 묶여 있던 repository·useCase 를 별도 키로 분리 - 중앙 등록 목록에서 Attendance 두 줄 제거
Attendance 에 적용한 패턴을 나머지 10개 도메인으로 확장한다. - Interface 의 DependencyKey 를 TestDependencyKey 로 낮춰 계약과 testValue 만 남긴다. - liveValue 는 구현을 소유한 Domain/Data 모듈의 <X>LiveDependencies.swift 에서 등록한다. - Default<X>RepositoryImpl 을 Mock<X>Repository 로 바꿔 testValue 전용임을 이름으로 드러낸다. - UnifiedDI.resolve(...) ?? Default...() 폴백을 제거한다. 주입 누락이 런타임 무동작으로 묻히지 않고 컴파일 단계에서 드러나야 한다. - Repository 와 UseCase 가 같은 키를 공유하던 도메인은 키를 둘로 분리한다. 중앙 팩토리가 같은 키에 서로 다른 구현을 대입해 나중 등록이 이기던 문제를 없앤다. - 중앙 집중식 AppDependencyFactory 의 도메인 등록을 걷어낸다. - AuthPresentationContextProvider 를 App 에서 AuthData 로 옮긴다. 구현을 쓰는 모듈이 소유한다. - GoogleOAuthRepositoryImpl / KakaoOAuthRepository 의 init 을 nonisolated 로 바꾼다. @mainactor configure 대신 nonisolated liveValue 에서 생성되며, init 은 저장 프로퍼티만 대입한다. - Dependencies+OAuth.swift 를 제거한다. AuthLiveDependencies 가 DependencyKey 준수까지 포함해 대체한다.
…bly 로 분리한다 레이어 이름을 참조 프로젝트(DDDAttendance)와 맞춘다. - Projects/Presentation 을 Projects/Feature 로 옮긴다. - 모든 피처를 묶던 Presentation 엄브렐러 모듈을 FeatureAssembly 로 바꾼다. - PresentationFeatureModule 을 FeatureModule 로 바꾸고 케이스를 lowerCamelCase + 명시 rawValue 로 정리한다. rawValue 가 실제 타깃명이라 오타로 깨지지 않는다. - 엄브렐러는 피처가 아니므로 카탈로그에서 빼고 .featureAssembly 헬퍼로 분리한다. - TargetDependency+Presentation.swift 를 TargetDependency+Feature.swift 로 바꾸고 .Presentation(implements:) 를 .feature(implements:) 로 바꾼다. - App 은 .featureAssembly 하나만 의존한다. - import Presentation 을 import FeatureAssembly 로 바꾼다. - 파일 헤더에 모듈명으로 박혀 있던 Presentation 을 갱신한다. PresentationAction(TCA) 과 UNNotificationPresentationOptions(UIKit) 는 외부 API 라 건드리지 않는다.
조립 경계 이름을 참조 프로젝트(DDDAttendance)와 맞춘다. - Projects/Domain/Domain 을 DomainAssembly 로, Projects/Data/Data 를 DataAssembly 로 옮긴다. - ModulePath.Domains / Datas 카탈로그에서 엄브렐러 case 를 뺀다. 엄브렐러는 모듈이 아니라 조립 경계라 .domainAssembly / .dataAssembly 전용 헬퍼로만 접근한다. FeatureAssembly 와 같은 처리다. - import Domain 을 import DomainAssembly 로 바꾼다. - 파일 헤더에 모듈명으로 박혀 있던 Domain / Data 를 갱신한다.
…vice 의존을 하나로 묶는다 참조 프로젝트(DDDAttendance)의 5개 조립 경계 중 빠져 있던 둘을 채운다. - Projects/Core/CoreAssembly 를 만든다. PickeCore·PickeFoundation·PickeStorage 구현을 묶는다. - Projects/Service/ServiceAssembly 를 만든다. CoreAssembly 위에 Ad·Analytics· AudioPlayer·Device 구현을 묶는다. SDK 링크는 이 경계 안에서만 일어난다. - TargetDependency 에 .coreAssembly / .serviceAssembly 헬퍼를 더한다. - App 이 개별로 걸고 있던 Core 2개와 Service 4개를 .serviceAssembly 하나로 바꾼다. App 의존성 29개에서 24개로 줄었다. Domain·Data·Network 는 App 소스가 개별 Interface 를 직접 import 하고 있어 이번 단계에서 건드리지 않는다.
DomainAssembly 가 AppUpdate 를 포함해 도메인 10개를 모두 재노출하도록 맞추고, App 매니페스트에서 .Domain 10개와 .Data(.AppUpdate) 를 걷어낸다. App 소스의 개별 모듈 import 도 DomainAssembly·DataAssembly 로 정리한다. 빌드 검증은 사용자 요청으로 취소했다.
모듈·피처 프로젝트가 Project.Options 를 주지 않아 자동 생성 스킴에 커버리지가 꺼져 있었고 개발 리전도 비어 있었다. DDD 와 동일하게 채운다. Demo 가 있는 모듈은 자동 스킴이 구현과 Demo 를 한 BuildAction 에 묶으므로, 자동 스킴을 끄고 Scheme.module / Scheme.demo 로 목적별 스킴을 나눈다. 검증: tuist generate 성공, PickeDesignKit 스킴이 구현·Demo 둘로 분리됨, AuthDomain 자동 스킴에 codeCoverageEnabled=YES 적용 확인.
API 는 베이스 URL 과 도메인 경로만 담는 서버 계약이라 Repository 구현이 사는 Data 레이어가 아니라 Service 레이어에 속한다. DDD 의 Service/API 배치에 맞춘다. 모듈 카탈로그의 Datas 에서 API 를 빼고 TargetDependency.api 접근자를 두었으며, 조립 경계도 DataAssembly 에서 ServiceAssembly 로 옮긴다. 검증: tuist generate 성공.
각 Data 모듈이 들고 있던 *API enum 은 서버 경로 문자열만 담고 있어 Repository 구현과 함께 둘 이유가 없다. 파일 헤더도 이미 API 를 가리키고 있었다. DDD 의 Service/API/Sources/<Domain> 배치에 맞춰 9개를 옮긴다. AuthApI.swift 는 이동하면서 AuthAPI.swift 로 바로잡는다. 검증: tuist generate 성공.
이 모듈이 담고 있던 건 디바이스 등록 요청 정의와 Encodable 확장으로, Repository 구현이 아니라 서버 요청 정의다. 이름과 위치를 실제 내용에 맞춘다. DDD 의 Service/APIEndpoint 와 같은 자리다. 모듈 카탈로그의 Datas 에서 Remote 를 빼고 TargetDependency.apiEndpoint 를 두었다. 매니페스트 10곳과 import 9곳을 함께 옮겼다. 검증: tuist generate 성공.
각 Data 모듈이 들고 있던 *Service TargetType 과 요청 DTO 20개를 Service/APIEndpoint/Sources/<Domain> 으로 옮긴다. 서버와의 통신 계약이라 Repository 구현과 같은 모듈에 있을 이유가 없다. Auth 엔드포인트가 경로에 SocialType 을 쓰므로 APIEndpoint 는 Auth 도메인 인터페이스만 의존한다. DDD 의 APIEndpoint 도 동일하게 필요한 도메인 인터페이스만 좁게 의존한다. 참조하는 Repository·테스트 23개 파일에 import APIEndpoint 를 추가했고, Search 매니페스트에 빠져 있던 apiEndpoint 의존을 채웠다. 검증: tuist generate 성공. APIEndpoint 스킴 빌드는 진행 중.
여러 feature 가 공유한다는 이유로 CommonDomain 에 모아둔 횡단 타입들은 소유자가 불분명해 도메인 경계를 흐렸다. 각 타입을 실제로 계약을 정의하는 모듈로 내려보내고 모듈 자체를 없앤다. - BattleTag·TagType → HomeDomainInterface 홈 섹션 4종이 직접 보유하는 값이다. Battle → Home 의존이 이미 있어 Battle 도 그대로 쓸 수 있다. 반대로 Battle 에 두면 Home → Battle 이 생겨 순환한다. - BattlePerspective 외 4종 → BattleDomainInterface BattleInterface.fetchBattlePerspectives 의 반환 타입이다. Perspective 에 두면 Comment → Perspective → Comment 순환이 생긴다. - CommentLikeResult·CommentError → CommentDomainInterface - ShareContent·ShareItem·PickeDeeplink → PickeFoundation 도메인 규칙이 아니라 Feature·App 이 함께 쓰는 순수 Foundation 값이다. 새로 생기는 모듈 간선은 Comment → Battle, Perspective → Battle 둘뿐이고 Battle → Home 이 유일한 하위 간선이라 순환은 없다. tuist generate 가 성공하는 것으로 그래프에 순환이 없음을 확인했다. Swift 36개 파일의 import 를 소유 모듈로 치환하고, 매니페스트 20곳에서 .Domain(.Common) 을 제거했으며, 카탈로그의 DomainFeatureModule.Common 과 DataFeatureModule.Common 케이스를 삭제했다. 빌드 검증은 완료 전이다. FeatureAssembly 스킴 빌드가 진행 중인 상태에서 사용자 요청으로 먼저 커밋했다.
DDD 참조 프로젝트의 Core 레이어 명명(DDDCoreUtility)에 맞춘다. "Foundation" 은 애플 프레임워크와 이름이 겹쳐 모듈 성격이 드러나지 않는다. 표준 타입 확장·딥링크·공유 같은 순수 유틸만 담는다는 뜻이 이름에 드러나도록 CoreUtility 로 바꿨다. - 디렉터리와 테스트 타깃 이름 변경 (PickeFoundationTests → PickeCoreUtilityTests) - CoreFeatureModule 카탈로그, CoreAssembly 매니페스트·재노출, PickeCore 의존성 갱신 - import 문 32개 파일 일괄 치환 검증: tuist generate 성공. 빌드 검증은 아직 하지 않았다.
날짜는 이미 DDD 와 동일하게 ___DATE___ (Xcode 가 파일 생성 시점을 채움) 를 쓰고 있었고, 빠져 있던 Copyright 줄만 채웠다. 검증: tuist generate 후 생성된 IDETemplateMacros.plist 에 반영 확인.
…tility 로 옮긴다 DDD 의 DDDThirdParty 와 역할을 맞춘다. 이 모듈의 실체는 외부 SPM 라이브러리를 한데 묶어 노출하는 우산이었고, 안에 들어 있던 유일한 자체 코드가 FeatureFlag 였다. "Core" 라는 이름이 기반 계층 전체를 가리키는 것처럼 읽혀 실제 역할과 어긋났다. - FeatureFlag 은 순수 유틸이므로 PickeCoreUtility 로 이전 - FeatureFlag 을 쓰는 3파일은 import 를 PickeCoreUtility 로 교체 - FeatureFlag 을 쓰지 않으면서 PickeCore 를 import 하던 3파일의 죽은 import 제거 (AppReducer, HomeFeature, NotificationFeature) - 테스트 타깃 이름과 모듈 카탈로그·매니페스트·CoreAssembly 재노출 갱신 - DDD 에서 복사돼 남아 있던 Base.swift 헤더를 이 저장소 형식으로 교체 검증: tuist generate 성공. 빌드 검증은 아직 하지 않았다.
Swift 명명 규약과 DDD 참조 프로젝트(`.core(.logger)`, `.domain(.auth, .interface)`)에 맞춘다. 타입만 대문자로 시작하고 케이스·함수·프로퍼티는 소문자로 시작해야 하는데, `.Core(.PickeThirdParty)` 는 둘 다 어겨 타입 참조처럼 읽혔다. - 모듈 enum 케이스를 lowerCamelCase 로 바꾸고 타깃 이름은 rawValue 로 명시 (예: `case thirdParty = "PickeThirdParty"`) — 생성되는 타깃·경로는 그대로다 - 의존성/경로 접근자를 소문자로: `.Domain/.Data/.Core/.Service/.Network/.DesignSystem` → `.domain/.data/.core/.service/.network/.designSystem` - 매니페스트 호출부 40개 파일 일괄 갱신 검증: tuist generate 성공(타깃 구성 동일).
`.data(implements: .model)` 은 마이크로피처 재편 전의 레이어 단일 모듈 DSL 잔재다. 같은 레이어를 가리키는 접근자가 `.data(.battle)` 과 둘로 갈려 있어, 어느 쪽을 써야 하는지가 호출부마다 달랐다. - Data 레이어 공용 DTO 모듈은 `.model` 접근자 하나로 노출 (호출부 12곳 교체) - ModulePath 의 Datas·Domains 카탈로그와 `.data(implements:)`·`.domain(implements:)`, 대응 Path 접근자 제거 — dataTesting·domainTesting 케이스는 참조가 0이었다 - Networks 는 P7(Network 모듈 통합)에서 정리하므로 이번엔 남긴다 검증: tuist generate 성공(타깃 구성 동일).
DDD 의 DDDCoreLogger 와 같은 구조로 로그 레벨(PickeLogLevel)·카테고리 (PickeLogCategory)·진입점(PickeLogger)을 나눠 담는다. Bundle identifier 를 subsystem 으로 쓰고 파일·함수·라인을 함께 남기며, 메시지 본문은 DEBUG 에서만 공개(privacy: .public)한다. CoreAssembly 가 이 모듈을 물고 재수출하므로 소비자는 Core 만 import 하면 된다. 외부 LogMacro 호출부(126곳)를 이 모듈로 옮기는 작업은 후속 단계에서 한다. 빌드 검증은 아직 하지 않았다. tuist generate 는 통과했다.
새 프로젝트 부트스트랩 잔재(newproject·generateProjectWithSettings·updateProjectConfig· XConfig 갱신)를 걷어내고, Command enum 과 단일 execute switch 로 진입점을 정리한다. graph·graph:prod·setup 명령과 레이어별 모듈 scaffold 를 추가한다. Workspace 는 Project.Environment.appName 하나만 쓰게 해 중복 분기와 로그를 없앤다.
legacyData 훅으로 기존 UserDefaults 값을 첫 조회 때 이관해 값 유실이 없다.
배치 - App/Sources: Reducer/Coordinator 와 View/Coordinator 로 갈려 있던 코디네이터를 Navigation/<피처>/ 한곳으로 모으고, Root 계층과 Di 디렉토리를 없앤다 - Tests: flat 배치 25개 모듈을 Tests/Sources/ 로 통일한다 - Interface: Interface 안에 또 Interface 가 겹쳐 있던 7군데를 펴고, Domain Interface 10개에 Sources/ 계층을 넣는다 - 매니페스트가 참조하지 않는 잔재 테스트 디렉토리 5개와 리소스 선언이 없는 모듈에 남아 있던 번들 Info.plist 를 지운다 의존성 - 도메인마다 되풀이하던 .service(.api)·.apiEndpoint·.core(.network)·.core(.coreUtility) 나열을 .serviceAssembly 한 줄로 바꾼다. ServiceAssembly 에 .apiEndpoint 를 넣어 묶음이 실제로 같은 범위를 덮게 한다 - App 은 조립 경계 셋과 직접 쓰는 SPM 둘만 남긴다. PickeCoreUI 는 CoreAssembly 로 옮겨 다른 모듈처럼 조립 경계를 통해 들어온다 - Core·Service 9개 모듈을 staticFramework 에서 framework 로 바꿔 중복 링크 경고를 27건에서 13건으로 줄인다 - Project.swift import 를 Foundation / 플러그인 / ProjectDescription 세 묶음으로 정렬한다
두 레이어는 모든 모듈이 Interface 를 갖췄다. 구현을 링크해야 하는 조립 레이어와 Ad 처럼 상대 View 를 직접 쓰는 곳만 .implementation 을 명시한다. Core·Service·UI 는 Interface 가 없는 모듈이 남아 기본값을 유지한다.
PickeLogger 는 os.Logger 기반으로 이미 완성돼 있었는데 아무도 쓰지 않고 53개 파일이 LogMacro 를 import 하고 있었다. 그중 실제 호출은 17곳뿐이라 나머지 36개는 쓰지도 않는 import 였다. 로거 - 호출 17곳을 PickeLogger 로 옮기고 카테고리를 붙인다 (푸시 서버 등록 network, 딥링크 navigation, 로그인 auth, 나머지 app) - 실제로 쓰는 7개 파일만 PickeCoreLogger 를 import 하고 나머지 46개는 지운다 - AppleOAuthRepositoryImpl 의 LogMacro.Log 래퍼도 없앤다 - 매니페스트 12개의 .SPM.logMarco 를 .core(.logger) 로 바꾼다 중복 의존성 - 도메인 9개의 .core(.logger) 는 .serviceAssembly 로 이미 들어와 지운다 - 피처 9개에서 .core(.thirdParty) 를 뺀다. PickeThirdParty 는 템플릿 뷰만 든 껍데기이고 SPM 을 흘려보내는 통로로만 쓰였다. 대신 각 피처가 실제 import 하는 .SPM.composableArchitecture·.SPM.tcaFlow 만 명시한다 - Hifi 는 코디네이터를 지우며 TCAFlow 를 쓰지 않게 돼 함께 뺀다
configure(moduleType:) 는 ModuleType enum 으로 분기하면서 .microModule 이면 Interface·구현·Testing·Tests 4타깃을 무조건 만들었다. 그 결과 아무도 쓰지 않는 Testing 스텁이 23개 쌓였다. - Project+Template.swift 를 makeModule 하나로 합치고 타깃을 불리언으로 켠다 (hasInterface·hasTesting·hasDemo·hasTests). makeAppModule 도 함께 노출한다 - ModuleType·configureModule·configureFeature 를 지운다 - 매니페스트 40개를 새 시그니처로 옮긴다. module 계열의 sources 인자는 buildableFolders 가 대신하므로 뺀다 - Testing 은 실제 목이 있는 AuthDomain·HomeDomain·PickeAuth 만 켜고 빈 스텁 23개를 지운다 - 소스도 매니페스트도 없이 생성물만 남아 있던 Domain/DomainTesting 을 지운다
- Interface/ 와 UseCase 파일의 여러 줄 /// 블록을 첫 줄만 남긴다 (32개 파일) - Xcode 템플릿이 남긴 'Add your test logic here'·'Put setup code here' 8줄과 코드를 그대로 되풀이하는 'Track call'·'Apply delay' 6줄을 지운다 왜를 설명하는 인라인 주석 421줄은 남겼다. 서버 진실값을 쓰는 이유나 Figma 좌표처럼 코드만 봐서는 알 수 없는 것들이라 지우면 근거가 사라진다.
Splash - SplashFeature·SplashView 를 App/Sources/Navigation/Splash 로, 테스트를 App/Tests 로 옮기고 모듈과 빈 SplashInterface 를 지운다. 카탈로그 case·조립 의존·재노출도 함께 끊는다 - SplashView 가 애니메이션 에셋을 직접 쓰므로 App 에 .ui(.animation) 을 넣는다 FeatureSharedUI - 피처 넷이 Ad 구현의 View 를 직접 물고 있었다. AdFitBannerView·AdFitNativeAdView· AdBannerSkeletonView 를 새 모듈로 옮겨 피처끼리 서로의 구현을 참조하지 않게 한다 - Profile 은 리워드 광고 계약도 쓰므로 Ad 는 Interface 로만 남긴다 세션 캐시 - PickeStorage Interface 에 SessionCacheInvalidating 계약을 두고 DomainAssembly 가 live 를 등록한다. 로그아웃·탈퇴 경로에서 부른다. 비울 로컬 캐시가 아직 없어 지금은 Noop 이고, LocalDataSource 가 생기면 여기에 엮는다 Workspace - 커버리지 대상을 스킴 관련 타깃으로 좁히는 generationOptions 를 넣는다. Stage 스킴은 App 프로젝트의 appSchemes 가 이미 만들어 워크스페이스에서 또 만들지 않는다
isLoading: Bool 은 화면이 스켈레톤을 보일지 콘텐츠를 보일지 가르는 값인데
이름만으로는 그 의미가 드러나지 않고, 로딩이 아닌 상태가 무엇인지도 모호했다.
- 리듀서 13개의 State 에 ViewState { loading, loaded } 를 두고 viewState 로 바꾼다
- 뷰 12개의 분기를 store.viewState == .loading 으로 옮긴다
- 페이지네이션 플래그(isLoadingMore 등)는 뜻이 다르므로 Bool 로 둔다
- 기존 의미를 그대로 옮겼다. 기본값 false 는 .loaded 에 대응한다
로거를 PickeCoreLogger 로 옮길 때 #log 매크로 17곳만 보고 Log.error 처럼 직접 부르는 자리를 놓쳤다. import 는 지웠는데 호출은 남아 컴파일이 깨졌다. - Log 직접 호출 64건(33파일)을 PickeLogger 로 옮기고 모듈에 맞는 카테고리를 붙인다 (PickeNetwork network, Auth 계열 auth, BattleDomain battle, Coordinator navigation, Feature ui, 나머지 app) - 피처 여덟과 PickeAnalytics 에 .core(.logger) 를 넣는다. 전에는 .core(.thirdParty) 를 통해 전이로 받고 있었는데 그 껍데기 의존을 걷어내며 끊겼다 - ViewState 치환이 !state.isLoadingMore 의 앞부분까지 먹어 없는 case 를 만든 곳 넷을 되돌린다
매니페스트 인자를 다시 배치하는 스크립트가 주석이 앞에 붙은 인자를 통째로 삼켜 의존성이 조용히 사라졌다. tuist generate 는 문법만 보므로 이걸 잡지 못하고 빌드에서야 드러났다. - PickeAnalytics 의 network·Firebase·Mixpanel·Sentry 일곱, PickeStorage 의 composableArchitecture·sqliteData, PickeNetwork 의 alamofire, AudioPlayerService 의 composableArchitecture 를 되살린다 - APIErrorDTO 를 PickeNetworkInterface 에 둔다. Data 레이어를 걷어낼 때 옮기지 않아 LogOutDTO·WithdrawDTO 가 없는 타입을 참조하고 있었다 - Log 를 옮기며 인자 두 개짜리 호출을 그대로 둬 시그니처가 어긋난 곳 여덟을 문자열 보간으로 합친다 - 그 치환이 개행을 넘어 매치되며 깨뜨린 문자열 리터럴 스물넷(일곱 파일)을 되돌린다
AppReducer - 자식 리듀서를 손으로 reduce 하던 reduceChild 를 ifCaseLet 합성으로 바꾼다. 상태 일치 검사는 handleScopeAction 한 곳으로 모으고 isValidAction 을 없앤다 - 비어 있던 NavigationAction 과 쓰이지 않던 isSplashState 를 지운다 (362→313줄) - LogMacro 를 걷어내며 전이로 받던 Foundation 을 직접 import 한다 기동 설정 - configurePushNotifications 과 requestTrackingAuthorizationWhenActive 를 AppDelegate+Configure 로 모은다. Push 파일에는 델리게이트 콜백만 남고 Tracking 파일은 비어 지운다 - 폰트를 Info.plist 등록이 아니라 PretendardFontFamily.registerFonts() 로 올린다 SplashFeature - run 클로저가 self 를 잡지 않도록 clock·openURL 을 명시적으로 캡처한다 KingfisherConfigurator - try? 가 옵셔널을 평탄화하므로 중복된 조건부 바인딩을 없앤다
변경 Swift 파일 포맷 검사와 diff 검사를 통과했다. 새 DerivedData에서 전체 시뮬레이터 검증을 진행 중이다. Rejected: 테스트 기대값만 제거 | 네트워크 에러와 실제 인증 인터셉터 계약을 검증한다
알림 목록 루트와 dismiss 전달 회귀 테스트를 포함한다. Screen enum의 별도 extension 매크로 구조를 유지한다. Rejected: Feature 계층에서 Coordinator 소유 | 앱의 화면 전환 조립 책임과 충돌한다
상세 투표 화면의 제목 높이가 줄어들지 않게 하고 넘치는 본문을 스크롤한다. 리스트 화면은 변경하지 않는다. SwiftFormat 검사를 통과했다. Rejected: 제목 폰트 축소 | 기존 타이포그래피를 유지하며 전체 제목을 표시한다
Fastlane 생성은 --no-binary-cache를 사용한다. 자동화 시나리오와 포맷·Ruby 문법 검사를 통과했다. 캐시 빌드는 통과했으며 CLI 저장 단계 오류를 추가 확인 중이다. Rejected: CI 대시보드와 캐시 연결 추가 | 로컬에서만 사용한다는 요청을 따른다
빌드 로그 파서 수정이 포함된 Tuist 4.207.0으로 실행 버전을 통일한다. 기존 Bitrise 버전 핀도 맞추되 Dashboard와 캐시 연동은 로컬에만 둔다. 독립 Alamofire fixture의 캐시 저장 성공을 확인했고 전체 앱 테스트 348개가 통과했다. 프로젝트 전체 캐시 저장 검증은 진행 중이다. Rejected: 캐시 생성에만 별도 Tuist 버전 사용 | 생성과 캐시의 실행 버전을 일치시키기 위해 제외
TuistSpider에서 Picke와 각 Assembly의 내부 의존성을 직접 내보내고 사용자가 지정한 docs/grpah 경로로 이미지와 README 링크를 맞췄다. 로컬 Dashboard와 캐시, Fastlane의 바이너리 캐시 제외 설정을 문서화했다. PNG 7장과 README 로컬 링크, diff 형식을 확인했다. Rejected: 이전 모듈 구조의 그래프 재사용 | 현재 실제 의존성을 설명해야 하므로 제외
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4b4461991b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| func handleAuthFailure() { | ||
| signOut() | ||
| // 화면 전환은 App 계층의 책임이므로 서비스는 세션 정리 후 이벤트만 알린다. | ||
| NotificationCenter.default.post(name: .pickeAuthSessionDidExpire, object: nil) |
There was a problem hiding this comment.
Post the session-expiration event observed by AppReducer
When token refresh is rejected, this posts .pickeAuthSessionDidExpire (pickeauth.session.didExpire), but AppReducer.setupRefreshTokenExpiredListener() subscribes to the unrelated name RefreshTokenExpired. The service therefore clears the credential without transitioning the main-tab UI to authentication, leaving the user on protected screens while subsequent requests run without a token; publish and observe the same notification constant.
Useful? React with 👍 / 👎.
| let dataRequest = try makeDataRequest(request) | ||
| let startedAt = Date() | ||
| let response = await dataRequest.serializingData(emptyResponseCodes: Self.emptyResponseCodes).response | ||
| Self.recordTelemetry(response: response.response, request: response.request, startedAt: startedAt, isSuccess: response.error == nil) |
There was a problem hiding this comment.
Classify HTTP failures correctly in raw-response telemetry
sendResponse intentionally skips validate() so logout and withdrawal can inspect non-2xx response bodies, which means a received 4xx/5xx response normally has no serialization error and is recorded here with isSuccess: true. Since these events are forwarded to Mixpanel as network_request, backend failures for both operations corrupt the success metric; derive success from the HTTP status range as well as the serialization result.
Useful? React with 👍 / 👎.
변경 내용
분산된 모듈 의존성과 앱 화면 전환 조립을 정리하고, 최신 TCA 환경에서 개발·검증할 수 있도록 맞춥니다.
Presentation/Data/Shared/Network구조를Feature/Domain/Service/Core/UI로 재편하고, 각 Assembly가 구현체를 조립하도록 정리했습니다.swift-dependencies,PickeCoreLogger, 저장소 인터페이스로 의존 경계를 통일했습니다.1.26.2, TCAFlow1.1.8로 갱신하고 실제 resolve 결과를 잠금 파일에 반영했습니다.docs/grpah/에 담았습니다../make setup이 로컬 Tuistpicke2026/picke인증 확인, 의존성 설치, 외부 모듈 캐시 준비, 프로젝트 생성을 순서대로 수행합니다. 캐시 준비 프로세스만 Dashboard handle을 비워 로컬 저장소에 저장하고, 일반 로컬 생성은 Dashboard 연결을 유지합니다. CI에서는 대시보드 인증과 캐시 준비를 건너뜁니다.--no-binary-cache --no-open을 적용했습니다.swift-dependencies를 1.12.0으로 고정했습니다. Xcode 컴파일 캐시는 기존 Explicit Modules 설정과 호환되지 않아 비활성화하고, 모듈 바이너리 캐시를 사용합니다.검증
확인 범위
실기기 화면 전환·공유 시트·광고 노출과 App Store 배포는 별도 검증이 필요합니다.