Post

TCA + Clean Architecture 프로젝트 구조 - 트렌디

TCA + Clean Architecture 핵심 요약

1. 폴더 구조

AppStore/
├── App/
│ ├── AppFeature.swift
│ └── AppStoreApp.swift

├── Domain/ // 비즈니스 로직
│ ├── Models/ // Entity (비즈니스 모델)
│ │ ├── User.swift
│ │ ├── Place.swift
│ │ └── YTVisitedPlace.swift
│ │
│ └── Services/ // UseCase + Repository 역할
│ ├── AuthService.swift
│ └── PlacesService.swift

├── Data/ // Infrastructure
│ ├── Clients/ // 외부 시스템 접근
│ │ ├── AppSupabaseClient.swift
│ │ ├── KeychainClient.swift
│ │ ├── LocationClient.swift
│ │ └── UserDefaultsClient.swift
│ │
│ └── DTOs/ // API 응답 모델 (필요시만)
│ └── UserDTO.swift

├── Features/ // Presentation
│ ├── Login/
│ │ ├── LoginFeature.swift
│ │ └── LoginView.swift
│ └── HomeMap/
│ ├── HomeMapFeature.swift
│ └── HomeMapView.swift

├── Shared/
│ ├── Components/
│ ├── Extensions/
│ └── Utils/

└── Config/
├── Debug.xcconfig
├── Release.xcconfig
└── Secrets.xcconfig

2. 레이어별 역할

Layer역할예시
Domain/Models비즈니스 엔티티User, Place
Domain/Services/비즈니스 로직 + 데이터 접근AuthService, PlacesService
Data/Clients/외부 시스템 접근AppSupabaseClient, LocationClient
Data/DTOs/API 응답 구조 (필요시만)UserDTO
Features/화면 로직 (Reducer + View)LoginFeature, LoginView

3. Client vs Service 구분

Client (Infrastructure)

1
2
3
4
// 외부 시스템/하드웨어 접근만
KeychainClient          // iOS Keychain 접근
LocationClient          // GPS 접근
AppSupabaseClient       // Supabase SDK 래핑

Service (Business Logic)

1
2
3
// 여러 Client 조합 + 비즈니스 로직
AuthService             // Supabase + Keychain + 인증 로직
PlacesService           // Location + Supabase + 거리 계산

4. Entity vs DTO

Entity (Domain 모델)

1
2
3
4
5
6
7
8
9
10
11
12
// Domain/Models/User.swift
struct User: Equatable, Identifiable {
    let id: String
    let email: String
    let name: String
    let createdAt: Date

    // 비즈니스 로직 포함
    var displayName: String {
        name.isEmpty ? email : name
    }
}

DTO (API 모델)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Data/DTOs/UserDTO.swift
struct UserDTO: Codable {
    let user_id: String
    let email_address: String
    let full_name: String
    let created_at: String  // API는 String

    // Entity 변환
    func toDomain() -> User {
        User(
            id: user_id,
            email: email_address,
            name: full_name,
            createdAt: ISO8601DateFormatter().date(from: created_at) ?? Date()
        )
    }
}

API와 도메인 모델이 같으면 DTO 생략 가능!

5. Service 구현 패턴 (추천)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Domain/Services/AuthService.swift
struct AuthService {
    var signIn: (String, String) async throws -> User
}

extension AuthService: DependencyKey {
    static let liveValue: AuthService = {
        @Dependency(\.supabaseClient) var client
        @Dependency(\.keychainClient) var keychain

        return AuthService(
            signIn: { email, password in
                // 1. API 호출 (Repository 역할)
                let dto: UserDTO = try await client
                    .from("users")
                    .select()
                    .execute()
                    .value

                // 2. DTO → Entity 변환 (Repository 역할)
                let user = dto.toDomain()

                // 3. 비즈니스 로직 (UseCase 역할)
                if !user.isVerified {
                    throw AuthError.emailNotVerified
                }

                try keychain.save("token", ...)

                return user
            }
        )
    }()
}

extension DependencyValues {
    var authService: AuthService {
        get { self[AuthService.self] }
        set { self[AuthService.self] = newValue }
    }
}

Service가 Repository + UseCase 역할 모두 수행 ✅

6. Feature에서 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Features/Login/LoginFeature.swift
@Reducer
struct LoginFeature {
    @ObservableState
    struct State {
        var user: User?  // Entity만 사용
    }

    @Dependency(\.authService) var authService  // Service만 의존

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            case .loginButtonTapped:
                return .run { send in
                    // DTO? 몰라도 됨. Entity만 받음
                    let user = try await authService.signIn(email, password)
                    await send(.loginResponse(.success(user)))
                }
        }
    }
}

7. 데이터 흐름

```plain text Feature ↓ (Entity) Service (Repository + UseCase 역할) ↓ (DTO) Client ↓ (JSON) API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22


### 8. 핵심 원칙

1. **Feature는 Entity만 알면 됨** - DTO 몰라도 OK
2. **Service가 DTO → Entity 변환** - Repository 역할 포함
3. **Client는 순수 Infrastructure** - 외부 접근만
4. **API 같으면 DTO 생략** - Entity에 Codable 추가
5. **Service = Repository + UseCase** - 한 파일에 통합 (실용적)


### 9. DependencyValues 등록


```swift
// ❌ 틀림
@Dependency(\.AuthService) var auth

// ✅ 맞음
@Dependency(\.authService) var auth
//            ↑ property 이름 사용

10. 파일 위치 체크리스트

  • ✅ Entity → Domain/Models/
  • ✅ Service → Domain/Services/ (Repository 역할 포함)
  • ✅ DTO → Data/DTOs/ (필요시만)
  • ✅ Client → Data/Clients/
  • ✅ Feature → Features/{FeatureName}/
  • ✅ View → Features/{FeatureName}/

끝! 🎯

This post is licensed under CC BY 4.0 by the author.