코드베이스의 파일 구조화 원칙과 타입 분리 기준을 정의합니다.
기본 규칙: 하나의 파일에는 하나의 주요 타입만 정의합니다.
목적:
- 파일 이름만으로 내용 파악 가능
- 코드 탐색 및 유지보수 용이
- Git 충돌 감소
- 명확한 책임 분리
Private helper types는 owner와 함께 유지합니다.
잘못된 예:
// ❌ AuthEndpoint.swift
enum AuthEndpoint: Endpoint {
// ...
}
// ❌ Configuration.swift - 불필요한 분리
private enum Configuration { // AuthEndpoint에서만 사용
// ...
}올바른 예:
// ✅ AuthEndpoint.swift
enum AuthEndpoint: Endpoint {
// ...
}
// Private helper는 함께 유지
private enum Configuration {
static var apiBaseURL: String? { ... }
}
private struct SignInRequest: Encodable {
// ...
}TCA의 State, Action은 Reducer와 함께 유지합니다.
// ✅ AppRootReducer.swift
@Reducer
struct AppRootReducer {
@ObservableState
struct State { // ← Reducer와 함께 유지
var isLoggedIn: Bool
}
enum Action { // ← Reducer와 함께 유지
case login
case logout
}
var body: some ReducerOf<Self> {
// ...
}
}이유: TCA 표준 패턴이며, State/Action/Reducer는 하나의 단위로 이해되어야 함
예제: SignInResponse 분리
Before:
// ❌ AuthEndpoint.swift (143줄, 4개 타입)
enum AuthEndpoint: Endpoint { ... }
private enum Configuration { ... }
private struct SignInRequest: Encodable { ... }
struct SignInResponse: Decodable { ... } // ← AuthClient+Live.swift에서 사용After:
// ✅ AuthEndpoint.swift (111줄, 3개 타입)
enum AuthEndpoint: Endpoint { ... }
private enum Configuration { ... }
private struct SignInRequest: Encodable { ... }
// ✅ DTO/SignInResponse.swift (32줄, 1개 타입)
struct SignInResponse: Decodable { ... }판단 기준:
SignInResponse는AuthClient+Live.swift에서 사용됨 (외부 의존성)Configuration과SignInRequest는AuthEndpoint내부에서만 사용 (private)- DTO는 엔드포인트와 별개의 관심사
예제: NetworkProviderProtocol 분리
Before:
// ❌ NetworkProviderProtocol.swift (70줄, 3개 타입)
public protocol NetworkProviderProtocol: Sendable {
func request<T: Decodable>(endpoint: Endpoint) async throws -> T
}
public struct NetworkClient: Sendable { // ← TCA 전용 래퍼
// ...
}
extension NetworkClient: TestDependencyKey { ... }
extension DependencyValues { ... }After:
// ✅ NetworkProviderProtocol.swift (26줄, 1개 타입)
public protocol NetworkProviderProtocol: Sendable {
func request<T: Decodable>(endpoint: Endpoint) async throws -> T
}
// ✅ NetworkClient.swift (44줄, 3개 타입)
public struct NetworkClient: Sendable { ... }
private struct UnimplementedNetworkProvider: NetworkProviderProtocol { ... }
extension NetworkClient: TestDependencyKey { ... }
extension DependencyValues { ... }판단 기준:
NetworkProviderProtocol: 순수 인터페이스 정의NetworkClient: TCA 전용 의존성 클라이언트- 서로 다른 책임 → 분리
예제: ShakeDetector 분리
Before:
// ❌ ShakeDetector.swift (130줄, 7개 타입)
extension Notification.Name { ... }
class ShakeDetectingViewController: UIViewController { ... }
struct ShakeDetectingView: UIViewControllerRepresentable { ... }
struct ShakeDetectorModifier: ViewModifier { ... }
struct ShakeDetectingHostingView: UIViewControllerRepresentable { ... }
class ShakeDetectingHostingController: UIViewController { ... }
extension View { ... }After:
// ✅ ShakeDetectingUIKit.swift (55줄, 3개 타입)
#if DEBUG
import UIKit
import Foundation
extension Notification.Name {
static let deviceDidShake = Notification.Name("deviceDidShake")
}
class ShakeDetectingHostingController: UIViewController { ... }
class ShakeDetectingViewController: UIViewController { ... } // deprecated
#endif
// ✅ ShakeDetectorModifier.swift (75줄, 4개 타입)
#if DEBUG
import SwiftUI
import UIKit
struct ShakeDetectorModifier: ViewModifier { ... }
struct ShakeDetectingHostingView: UIViewControllerRepresentable { ... }
struct ShakeDetectingView: UIViewControllerRepresentable { ... } // deprecated
public extension View {
func detectShakeForPulse(label: String = "Global") -> some View { ... }
}
#endif판단 기준:
- UIKit 관련 타입:
ShakeDetectingUIKit.swift - SwiftUI 관련 타입 + Public API:
ShakeDetectorModifier.swift - 레이어별 분리로 의존성 명확화
// ✅ AuthEndpoint.swift
enum AuthEndpoint: Endpoint {
// ...
}
// Private helper는 함께 유지
private enum Configuration {
static var apiBaseURL: String? { ... }
}
private struct SignInRequest: Encodable {
// AuthEndpoint에서만 사용되는 request DTO
}이유: 응집도 유지, 구현 디테일 숨김
// ✅ ShakeDetectorModifier.swift
struct ShakeDetectorModifier: ViewModifier {
// ...
}
// 6줄짜리 helper는 함께 유지
struct ShakeDetectingHostingView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ShakeDetectingHostingController {
ShakeDetectingHostingController()
}
func updateUIViewController(_ uiViewController: ShakeDetectingHostingController, context: Context) {}
}이유: 과도한 파일 분리 방지 (6줄을 별도 파일로 만들 필요 없음)
// ✅ NetworkClient.swift
public struct NetworkClient: Sendable {
// ...
}
// Stub은 클라이언트와 함께 유지
private struct UnimplementedNetworkProvider: NetworkProviderProtocol {
func request<T: Decodable>(endpoint: Endpoint) async throws -> T {
assertionFailure("NetworkClient.request is unimplemented")
throw NetworkError.unknownError
}
}
extension NetworkClient: TestDependencyKey {
public static let testValue = Self(provider: UnimplementedNetworkProvider())
}이유: TCA 패턴에서 stub은 클라이언트 정의의 일부
// ✅ AppRootReducer.swift
@Reducer
struct AppRootReducer {
@ObservableState
struct State { // ← 분리하지 않음
var isLoggedIn: Bool
}
enum Action { // ← 분리하지 않음
case login
}
var body: some ReducerOf<Self> {
// ...
}
}이유: TCA 표준 패턴, Reducer/State/Action은 하나의 단위
여러 DTO가 있을 경우 DTO/ 서브디렉토리 생성:
Projects/Domain/Auth/Sources/
├── AuthEndpoint.swift
├── AuthClient.swift
└── DTO/
├── SignInResponse.swift
├── SignUpResponse.swift
└── RefreshTokenResponse.swift
UIKit과 SwiftUI가 혼재된 경우 파일명으로 구분:
Projects/Core/Logging/Sources/
├── ShakeDetectingUIKit.swift # UIKit 관련
├── ShakeDetectorModifier.swift # SwiftUI 관련
└── PulseNetworkLogViewProvider.swift
이미 적용된 Interface/Implementation 분리:
Projects/Core/Network/
├── Interface/
│ └── Sources/
│ ├── NetworkProviderProtocol.swift # Protocol 정의
│ └── NetworkClient.swift # TCA Client
└── Sources/
└── NetworkProvider.swift # 실제 구현
파일 분리 시 기존 코드가 깨지지 않도록 보장:
✅ 같은 모듈 내에서만 분리
import DomainAuth로SignInResponse접근 가능 (파일 위치 무관)- Swift 컴파일러가 자동으로 파일 순서 처리
✅ Access level 유지
- Internal 타입은 internal로 유지
- Public 타입은 public으로 유지
❌ Module 경계를 넘는 분리는 신중하게
- 다른 모듈로 이동 시 import 수정 필요
- 순환 의존성 발생 가능
즉시 삭제하지 말고 deprecated 표시:
@available(*, deprecated, message: "Use ShakeDetectorModifier directly")
class ShakeDetectingViewController: UIViewController {
// ... legacy implementation
}이유:
- 다른 브랜치에서 사용 중일 수 있음
- 점진적 마이그레이션 가능
- 명확한 deprecation 경로 제공
파일 분리 후 불필요한 import 제거:
Before:
// ❌ NetworkProviderProtocol.swift
import Foundation
import ComposableArchitecture // ← NetworkClient 분리 후 불필요
public protocol NetworkProviderProtocol: Sendable { ... }After:
// ✅ NetworkProviderProtocol.swift
import Foundation // ← 필요한 것만 import
public protocol NetworkProviderProtocol: Sendable { ... }분리된 파일에도 동일한 조건 유지:
// ✅ ShakeDetectingUIKit.swift
#if DEBUG
import UIKit
import Foundation
// ... 모든 코드 ...
#endif파일 분리 시 다음을 확인하세요:
- 파일이 여러 타입을 포함하는가?
- 각 타입의 책임과 의존성을 분석했는가?
- Private vs Internal/Public 타입을 구분했는가?
- 외부 의존성이 있는 타입을 식별했는가?
- Private 타입은 owner와 함께 유지했는가?
- TCA State/Action은 Reducer와 함께 유지했는가?
- 작은 helper 타입(<10줄)은 함께 유지했는가?
- 파일 이름이 내용을 명확히 나타내는가?
- Access level을 동일하게 유지했는가?
- 불필요한 import를 제거했는가?
-
tuist generate성공하는가? -
tuist build성공하는가? - 기존 코드가 정상 작동하는가?
- Public API가 변경 없이 작동하는가?
- Git status로 의도한 파일만 변경되었는지 확인했는가?
상황: 143줄 파일에 4개 타입 혼재
분석:
AuthEndpoint(enum): 메인 타입Configuration(private enum): AuthEndpoint에서만 사용SignInRequest(private struct): AuthEndpoint에서만 사용SignInResponse(struct): AuthClient+Live.swift에서 사용 ← 외부 의존성
결정:
- ✅
SignInResponse→DTO/SignInResponse.swift로 분리 - ✅
Configuration,SignInRequest→AuthEndpoint.swift에 유지 (private)
결과:
AuthEndpoint.swift: 111줄, 3개 타입DTO/SignInResponse.swift: 32줄, 1개 타입- DTO 패턴 확립 (향후 SignUpResponse 등 추가 용이)
상황: 70줄 파일에 3개 타입, 서로 다른 책임
분석:
NetworkProviderProtocol(protocol): 순수 인터페이스NetworkClient(struct): TCA 의존성 래퍼UnimplementedNetworkProvider(private struct): Test stub
결정:
- ✅
NetworkClient+ stub + extensions →NetworkClient.swift로 분리 - ✅
NetworkProviderProtocol→NetworkProviderProtocol.swift에 유지
결과:
NetworkProviderProtocol.swift: 26줄, 1개 타입 (protocol만)NetworkClient.swift: 44줄, 3개 타입 (TCA 관련만)- TCA 패턴 명확화, import 최소화 (ComposableArchitecture 분리)
상황: 130줄 파일에 7개 타입, UIKit/SwiftUI 혼재
분석:
- UIKit:
Notification.Name,ShakeDetectingHostingController,ShakeDetectingViewController - SwiftUI:
ShakeDetectorModifier,ShakeDetectingHostingView,ShakeDetectingView,Viewextension
결정:
- ✅ UIKit 관련 →
ShakeDetectingUIKit.swift - ✅ SwiftUI 관련 + Public API →
ShakeDetectorModifier.swift - ✅ 레거시 타입 → deprecated 표시로 유지
결과:
ShakeDetectingUIKit.swift: 55줄, 3개 타입ShakeDetectorModifier.swift: 75줄, 4개 타입- 레이어 분리로 의존성 명확화
- ✅ 파일 이름만으로 내용 파악 가능
- ✅ 파일당 평균 줄 수 감소 (100줄 이하 유지)
- ✅ 타입별 책임 명확화
- ✅ 변경 시 영향 범위 명확
- ✅ Git conflict 감소
- ✅ Code review 범위 축소
- ✅ 패턴 확립 (DTO/, 레이어 분리)
- ✅ 일관된 코드 구조
- ✅ 새로운 타입 추가 시 명확한 위치
- ✅ 파일 탐색 용이 (Xcode navigator)
- ✅ 병렬 작업 시 충돌 감소
- ✅ 코드 이해도 향상
❌ 하지 마세요:
// 6줄짜리 helper를 별도 파일로
ShakeDetectingHostingView.swift // 6줄
✅ 대신:
// ShakeDetectorModifier.swift
struct ShakeDetectorModifier: ViewModifier { ... }
// 작은 helper는 함께 유지
struct ShakeDetectingHostingView: UIViewControllerRepresentable {
// 6줄
}❌ 하지 마세요:
Configuration.swift // private enum을 별도 파일로
✅ 대신:
// AuthEndpoint.swift
enum AuthEndpoint: Endpoint { ... }
private enum Configuration { // owner와 함께
// ...
}❌ 하지 마세요:
AppRootState.swift
AppRootAction.swift
AppRootReducer.swift
✅ 대신:
// AppRootReducer.swift
@Reducer
struct AppRootReducer {
struct State { ... }
enum Action { ... }
var body: some ReducerOf<Self> { ... }
}- One Type Per File (예외: private helpers, TCA nested types, 작은 helpers)
- 응집도 우선 (긴밀하게 결합된 타입은 함께 유지)
- 레이어 분리 (UIKit vs SwiftUI, Protocol vs Implementation)
- Backward Compatibility (같은 모듈 내 분리, access level 유지)
- 과도한 분리 지양 (100줄 이하는 분리 고려, 10줄 이하는 유지)
파일에 여러 타입이 있는가?
↓ Yes
타입이 private인가?
↓ Yes → 함께 유지
↓ No
TCA Reducer의 State/Action인가?
↓ Yes → 함께 유지
↓ No
10줄 이하의 작은 helper인가?
↓ Yes → 함께 유지
↓ No
외부에서 사용되는가 OR 서로 다른 책임/레이어인가?
↓ Yes → 분리
↓ No → 함께 유지
문서 버전: 1.0 작성일: 2026-01-19 기반: AuthEndpoint, NetworkProviderProtocol, ShakeDetector 분리 사례