Tuist도 Scaffold CLI도 만들지 않는다: SwiftPM + AI Agent로 iOS Clean Architecture 모듈 자동 생성하기

이경규·2026년 8월 14일

Tuist도 Scaffold CLI도 만들지 않는다: SwiftPM + AI Agent로 iOS Clean Architecture 모듈 자동 생성하기

iOS 프로젝트에서 Tuist를 빼려고 하면 보통 다음 문제가 바로 나온다.

Tuist가 모듈을 만들어줬는데
이제 Feature를 추가할 때마다 직접 폴더를 만들어야 하나?

Package.swift도 매번 수정해야 하나?

Xcode Target도 사람이 연결해야 하나?

결국 별도의 Scaffold CLI를 또 만들어야 하나?

예전이라면 실제로 다음과 같은 도구를 하나 만들었을 가능성이 높다.

make feature name=Profile

# 또는

./scripts/create_feature.sh Profile

조금 더 발전하면 Swift Argument Parser로 사내 CLI까지 만든다.

myapp module create Profile

하지만 2026년에는 여기까지 할 필요가 없는 프로젝트가 꽤 많다.

새 iOS 프로젝트를 만든다면 오히려 다음 구조를 먼저 검토한다.

SwiftPM
→ 실제 모듈과 Dependency 관리

Xcode
→ App / Widget / Extension 같은 Application Target만 관리

AI Coding Agent
→ 모듈 생성과 Package.swift 수정

AGENTS.md
→ Architecture 규칙

.ai/templates/
→ Clean Architecture 템플릿

xcodebuild / SwiftPM
→ 실제 검증

개발자가 사용하는 인터페이스는 자연어다.

FeatureProfile 모듈 만들어줘.

Clean Architecture 템플릿을 사용하고

Domain은 Core,
Data는 Network,
Presentation은 DesignSystem을 사용해.

AppComposition에도 연결하고
빌드까지 검증해줘.

Agent가 뒤에서 알아서 처리한다.

Template 확인

↓

폴더 생성

↓

Swift 파일 생성

↓

Package.swift 수정

↓

Dependency 연결

↓

순환 참조 확인

↓

Build

↓

Test

↓

변경 결과 보고

중요한 점이 하나 있다.

AI Agent가 Swift Package Manager를 대체하는 것은 아니다.

구조는 여전히:

SwiftPM
=
Package Manager / Build Graph

AI Agent
=
SwiftPM을 조작하는 개발 인터페이스

다.

즉 사용자 입장에서는 CLI를 거의 사용하지 않지만, 실제 검증은 여전히 결정론적인 SwiftPM과 Xcode가 담당한다.

이 글에서는 Tuist를 완전히 제거한 프로젝트에서 이 구조를 처음부터 만들어본다.


1. 먼저 Tuist가 하던 일을 분해한다

Tuist가 편했던 이유는 여러 역할을 한곳에서 처리했기 때문이다.

Project.swift

↓

Target 생성

Dependency 연결

Scheme

Configuration

Project 생성

그리고 다음과 같은 Scaffold도 만들 수 있었다.

FeatureHome

FeatureLogin

FeatureProfile

Tuist를 제거하면서 이 모든 것을 다른 Project Generator 하나로 그대로 옮길 필요는 없다.

역할을 나누면 된다.

Dependency / Module Graph
→ SwiftPM

App Target
→ Xcode

Source Folder
→ Xcode Folder + SwiftPM

Environment
→ xcconfig

Module Scaffold
→ AI Agent

Architecture Rule
→ AGENTS.md

Validation
→ SwiftPM + xcodebuild

이렇게 하면 별도의 Project Generator 자체가 필요 없어지는 경우가 많다.


2. 2026년에는 Xcode Project를 최대한 작게 만든다

Apple도 Local Swift Package를 앱 내부 코드의 모듈화 수단으로 공식 지원한다.

그리고 최근 Xcode의 folder 기반 프로젝트 구조에서는 디스크의 파일 추가·삭제를 프로젝트 파일에 하나씩 기록하는 방식보다 훨씬 작은 Project File 표현을 사용할 수 있다.

파일을 추가할 때마다:

project.pbxproj

+ PBXFileReference
+ PBXBuildFile
+ PBXGroup

가 계속 바뀌던 문제를 줄일 수 있다.

그래서 목표를 다음처럼 잡는다.

MyApp.xcodeproj

담당:

MyApp
Widget
NotificationExtension
ShareExtension
Signing
Capability
Scheme
Build Settings

반대로 일반 Swift 코드는 대부분 SwiftPM으로 내린다.


3. Package.swift도 Feature마다 만들지 않는 쪽부터 시작한다

처음에는 이런 구조가 자연스럽게 보인다.

Packages/
├── Core/
│   └── Package.swift
├── Network/
│   └── Package.swift
├── FeatureHome/
│   └── Package.swift
├── FeatureLogin/
│   └── Package.swift
└── FeatureProfile/
    └── Package.swift

이것도 좋은 구조다.

특히 각 Package를 독립적으로 배포하거나 팀별 Ownership을 강하게 나눠야 한다면 적합하다.

하지만 하나의 iOS 앱 안에서만 사용하는 Module이라면 먼저 하나의 Local Package + 여러 Target 구조를 검토한다.

Packages/
└── AppModules/
    ├── Package.swift
    ├── Sources/
    └── Tests/

그 안에 Target을 여러 개 만든다.

Core

Network

DesignSystem

FeatureHomeDomain
FeatureHomeData
FeatureHomePresentation
FeatureHomeComposition

FeatureProfileDomain
FeatureProfileData
FeatureProfilePresentation
FeatureProfileComposition

AppComposition

이 구조에는 큰 장점이 있다.

Xcode App Target이 Local Package 하나만 알면 된다.


4. Xcode Project와 SwiftPM 사이의 접점을 하나로 줄인다

최종 구조를 이렇게 만든다.

MyApp.xcodeproj
      │
      │ dependency
      ▼
AppModules
      │
      ▼
AppComposition
      │
 ┌────┴──────────┐
 │               │
 ▼               ▼
FeatureHome   FeatureProfile

App Target은:

import AppComposition

정도만 알고 있다.

새로운 Feature가 생겨도:

FeatureSettings

FeatureCamera

FeatureSearch

Xcode Project에 새로운 Package Reference를 계속 추가하는 대신 AppModules/Package.swift 안에서 Target Graph만 수정한다.

따라서 Agent가 .pbxproj를 직접 수정할 일이 크게 줄어든다.

이 부분이 Tuist 제거 구조에서 꽤 중요하다.


5. 전체 프로젝트 구조

처음 프로젝트를 다음처럼 만든다.

MyApp/
├── App/
│   ├── MyApp.xcodeproj
│   ├── MyApp/
│   │   ├── MyApp.swift
│   │   └── Resources/
│   └── Extensions/
│
├── Packages/
│   └── AppModules/
│       ├── Package.swift
│       ├── Sources/
│       │   ├── Core/
│       │   ├── Network/
│       │   ├── DesignSystem/
│       │   ├── FeatureHomeDomain/
│       │   ├── FeatureHomeData/
│       │   ├── FeatureHomePresentation/
│       │   ├── FeatureHomeComposition/
│       │   └── AppComposition/
│       │
│       └── Tests/
│
├── Config/
│   ├── Dev.xcconfig
│   ├── Stage.xcconfig
│   └── Prod.xcconfig
│
├── .ai/
│   └── templates/
│       └── clean-feature/
│
└── AGENTS.md

이 구조에서 Xcode Project는 매우 작다.

실제 애플리케이션 코드는 대부분:

Packages/AppModules

안에 존재한다.


6. Clean Architecture Dependency부터 고정한다

Agent에게 가장 먼저 알려줘야 하는 것은 Folder 이름이 아니다.

Dependency Direction이다.

Feature 단위 Clean Architecture를 만든다면 다음처럼 한다.

                         App
                          │
                          ▼
                  AppComposition
                          │
                          ▼
             FeatureProfileComposition
                   /             \
                  ▼               ▼
        Presentation             Data
              │                   │
              │                   ▼
              └──────────────► Domain
                                  │
                                  ▼
                                Core

그리고 공통 Infra는:

Data
 │
 └────► Network

Presentation
 │
 └────► DesignSystem

로 연결한다.

중요한 Rule:

Domain
→ UIKit 금지
→ SwiftUI 금지
→ Network 구현체 금지

Presentation:

Domain O

Data X

Data:

Domain O

Presentation X

Feature 간 직접 참조:

FeatureHome
      ↓
FeatureProfile

X

가 기본이다.

Feature 간 연결은:

AppComposition

또는

Shared Domain

에서 해결한다.


7. 왜 Composition Target을 따로 둘까

Clean Architecture를 만들다 보면 흔히 이런 문제가 생긴다.

Presentation이 실제 Repository 구현체를 만들기 시작한다.

let repository = LiveProfileRepository()
let useCase = LoadProfileUseCase(repository: repository)

그러면:

Presentation

↓

Data

Dependency가 생긴다.

이를 막기 위해 Composition을 따로 둔다.

FeatureProfileComposition

이 Target만:

Data

Presentation

Domain

을 모두 알고 있다.

Composition Root가 실제 Dependency를 조립한다.


8. 첫 Package.swift 만들기

Packages/AppModules/Package.swift

// swift-tools-version: 6.3

import PackageDescription

let package = Package(
    name: "AppModules",
    platforms: [
        .iOS("18.0")
    ],
    products: [
        .library(
            name: "AppComposition",
            targets: ["AppComposition"]
        )
    ],
    targets: [
        .target(
            name: "Core"
        ),

        .target(
            name: "Network",
            dependencies: [
                "Core"
            ]
        ),

        .target(
            name: "DesignSystem"
        ),

        .target(
            name: "FeatureHomeDomain",
            dependencies: [
                "Core"
            ]
        ),

        .target(
            name: "FeatureHomeData",
            dependencies: [
                "FeatureHomeDomain",
                "Network"
            ]
        ),

        .target(
            name: "FeatureHomePresentation",
            dependencies: [
                "FeatureHomeDomain",
                "DesignSystem"
            ]
        ),

        .target(
            name: "FeatureHomeComposition",
            dependencies: [
                "FeatureHomeDomain",
                "FeatureHomeData",
                "FeatureHomePresentation"
            ]
        ),

        .target(
            name: "AppComposition",
            dependencies: [
                "FeatureHomeComposition"
            ]
        ),

        .testTarget(
            name: "FeatureHomeDomainTests",
            dependencies: [
                "FeatureHomeDomain"
            ]
        )
    ]
)

실제 Deployment Target은 앱 정책에 맞춰 바꾸면 된다.

Swift 6.3은 2026년 3월 정식 출시됐고, SwiftPM에서는 Swift Build 통합 Preview도 제공하기 시작했다.

중요한 것은 특정 버전보다 Package.swift가 Module Graph의 Source of Truth가 되는 구조다.


9. App Target은 AppComposition만 본다

MyApp.swift

import SwiftUI
import AppComposition

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            AppRootFactory.make()
        }
    }
}

App Target의 역할이 매우 작아진다.

App 시작

Signing

Capability

Lifecycle

AppComposition 호출

정도만 담당한다.


10. 이제 Clean Architecture Template을 만든다

여기가 이번 구조의 핵심이다.

예전에는:

Tuist Scaffold

또는

사내 CLI

가 Template을 가지고 있었다.

이제 Template을 그냥 Repository에 둔다.

.ai/
└── templates/
    └── clean-feature/
        ├── template.json
        │
        ├── Domain/
        │   ├── __FEATURE__.swift
        │   ├── __FEATURE__Repository.swift
        │   └── Load__FEATURE__UseCase.swift
        │
        ├── Data/
        │   └── Live__FEATURE__Repository.swift
        │
        ├── Presentation/
        │   ├── __FEATURE__View.swift
        │   └── __FEATURE__ViewModel.swift
        │
        ├── Composition/
        │   └── __FEATURE__FeatureFactory.swift
        │
        └── Tests/
            └── Load__FEATURE__UseCaseTests.swift

__FEATURE__가 Placeholder다.


11. Template Metadata도 하나 둔다

.ai/templates/clean-feature/template.json

{
  "template": "clean-feature",
  "placeholder": "__FEATURE__",
  "layers": [
    "Domain",
    "Data",
    "Presentation",
    "Composition"
  ],
  "defaultDependencies": {
    "Domain": [
      "Core"
    ],
    "Data": [
      "Domain",
      "Network"
    ],
    "Presentation": [
      "Domain",
      "DesignSystem"
    ],
    "Composition": [
      "Domain",
      "Data",
      "Presentation"
    ]
  }
}

Agent가 Template 구조를 쉽게 이해할 수 있다.


12. Domain Template

__FEATURE__.swift

public struct __FEATURE__: Sendable, Equatable {
    public let id: String

    public init(id: String) {
        self.id = id
    }
}

Repository:

public protocol __FEATURE__Repository: Sendable {
    func load() async throws -> __FEATURE__
}

UseCase:

public struct Load__FEATURE__UseCase: Sendable {
    private let repository: any __FEATURE__Repository

    public init(
        repository: any __FEATURE__Repository
    ) {
        self.repository = repository
    }

    public func execute() async throws -> __FEATURE__ {
        try await repository.load()
    }
}

Domain에는 UI와 Network 구현이 없다.


13. Data Template

import Feature__FEATURE__Domain
import Network

public struct Live__FEATURE__Repository:
    __FEATURE__Repository,
    Sendable {

    private let client: any HTTPClient

    public init(
        client: any HTTPClient
    ) {
        self.client = client
    }

    public func load() async throws -> __FEATURE__ {
        let response: __FEATURE__Response =
            try await client.get("/profile")

        return __FEATURE__(
            id: response.id
        )
    }
}

실제 Endpoint까지 Template에서 고정하면 좋지 않다.

Agent가 Feature 요구사항을 보고 수정하게 한다.


14. Presentation Template

Swift Observation을 사용한다면:

import Observation
import Feature__FEATURE__Domain

@MainActor
@Observable
public final class __FEATURE__ViewModel {

    public private(set) var state: State = .idle

    private let loadUseCase: Load__FEATURE__UseCase

    public init(
        loadUseCase: Load__FEATURE__UseCase
    ) {
        self.loadUseCase = loadUseCase
    }

    public func load() async {
        state = .loading

        do {
            let value = try await loadUseCase.execute()
            state = .loaded(value)
        } catch {
            state = .failed
        }
    }
}

extension __FEATURE__ViewModel {

    public enum State {
        case idle
        case loading
        case loaded(__FEATURE__)
        case failed
    }
}

View:

import SwiftUI

public struct __FEATURE__View: View {

    @State
    private var viewModel: __FEATURE__ViewModel

    public init(
        viewModel: __FEATURE__ViewModel
    ) {
        self._viewModel = State(
            initialValue: viewModel
        )
    }

    public var body: some View {
        content
            .task {
                await viewModel.load()
            }
    }

    @ViewBuilder
    private var content: some View {
        switch viewModel.state {
        case .idle,
             .loading:
            ProgressView()

        case .loaded(let value):
            Text(value.id)

        case .failed:
            ContentUnavailableView(
                "불러오지 못했습니다",
                systemImage: "exclamationmark.triangle"
            )
        }
    }
}

이 부분은 프로젝트가 TCA라면 TCA Template으로 바꿔도 된다.

Architecture Rule만 유지하면 된다.


15. Composition Template

import SwiftUI
import Network
import Feature__FEATURE__Domain
import Feature__FEATURE__Data
import Feature__FEATURE__Presentation

public enum __FEATURE__FeatureFactory {

    @MainActor
    public static func make(
        httpClient: any HTTPClient
    ) -> some View {

        let repository =
            Live__FEATURE__Repository(
                client: httpClient
            )

        let useCase =
            Load__FEATURE__UseCase(
                repository: repository
            )

        let viewModel =
            __FEATURE__ViewModel(
                loadUseCase: useCase
            )

        return __FEATURE__View(
            viewModel: viewModel
        )
    }
}

Dependency 조립은 여기에서만 한다.


16. Agent가 처음부터 코드를 발명하게 하지 않는 것이 중요하다

이 방식의 장점은:

"FeatureProfile 만들어줘"

라고 했다고 해서 매번 Agent가 새로운 Architecture를 생각하지 않는다는 것이다.

대신:

Clean Feature Template

+

AGENTS.md

+

현재 요구사항

을 사용한다.

결과적으로 Agent는 생성기에 가까워진다.


17. AGENTS.md가 기존 Tuist Template 설정 역할을 한다

Codex는 Repository 안의 AGENTS.md를 읽고 프로젝트 구조, 명령어, 테스트 방법과 Coding Convention을 따를 수 있다.

따라서 Root에 다음을 만든다.

# iOS Project Rules

## Architecture

Application dependency direction:

App
→ AppComposition
→ FeatureComposition
→ Presentation / Data
→ Domain
→ Core

Data may depend on:
- Domain
- Network

Presentation may depend on:
- Domain
- DesignSystem

Domain may depend on:
- Core

Feature-to-Feature dependency is prohibited.

## Project

The Xcode project contains only:

- App
- Widget
- Extension
- Signing
- Capability

Do not modify `.xcodeproj` for normal feature creation.

## SwiftPM

Application modules live in:

`Packages/AppModules`

`Package.swift` is the source of truth for module dependencies.

## New Feature

When creating a new feature:

1. Read `.ai/templates/clean-feature/`.
2. Create Domain target.
3. Create Data target.
4. Create Presentation target.
5. Create Composition target.
6. Create Domain tests.
7. Update `Package.swift`.
8. Connect FeatureComposition to AppComposition.
9. Never add direct Feature-to-Feature dependencies.
10. Do not add dependencies not required by the feature.

## Naming

For `Profile` create:

- FeatureProfileDomain
- FeatureProfileData
- FeatureProfilePresentation
- FeatureProfileComposition

## Validation

After changing package structure:

1. Resolve SwiftPM dependencies.
2. Inspect the dependency graph.
3. Build the package/app.
4. Run relevant tests.
5. Report commands actually executed.

Never report a test as passed unless it was executed.

## Xcode

Do not edit `project.pbxproj` unless the requested change requires:

- App target
- Widget
- Extension
- Capability
- Signing
- Scheme / configuration changes

이 파일이 사실상 프로젝트의 Module Generator Policy가 된다.


18. 이제 개발자는 CLI를 호출하지 않는다

Feature를 추가하고 싶다.

기존:

tuist scaffold feature --name Profile

또는:

myapp create-feature Profile

AI Agent 방식:

Profile 기능 추가해줘.

clean-feature 템플릿 사용.

화면:
ProfileView

필요 기능:
- 사용자 프로필 조회
- 새로고침

Dependency:
- Core
- Network
- DesignSystem

기존 Feature를 직접 참조하지 마.

생성 후 Dependency Graph와
iOS Simulator Build까지 검증해줘.

끝이다.


19. Agent가 내부에서 하는 일

Agent는 먼저:

AGENTS.md

를 읽는다.

그다음:

.ai/templates/clean-feature/

를 확인한다.

그리고 생성한다.

FeatureProfileDomain/

FeatureProfileData/

FeatureProfilePresentation/

FeatureProfileComposition/

20. Package.swift까지 Agent가 수정한다

Agent가 다음을 추가한다.

.target(
    name: "FeatureProfileDomain",
    dependencies: [
        "Core"
    ]
),

.target(
    name: "FeatureProfileData",
    dependencies: [
        "FeatureProfileDomain",
        "Network"
    ]
),

.target(
    name: "FeatureProfilePresentation",
    dependencies: [
        "FeatureProfileDomain",
        "DesignSystem"
    ]
),

.target(
    name: "FeatureProfileComposition",
    dependencies: [
        "FeatureProfileDomain",
        "FeatureProfileData",
        "FeatureProfilePresentation"
    ]
),

.testTarget(
    name: "FeatureProfileDomainTests",
    dependencies: [
        "FeatureProfileDomain"
    ]
)

그리고 AppComposition Dependency에:

"FeatureProfileComposition"

을 추가한다.


21. AppComposition도 Agent가 연결한다

예를 들어 Route가 있다.

public enum AppRoute {
    case home
    case profile
}

Agent가 Profile 연결을 추가한다.

case .profile:
    ProfileFeatureFactory.make(
        httpClient: dependencies.httpClient
    )

이제 Xcode App Target에는 변화가 없다.


22. 결국 Project File을 건드리지 않고 Feature 하나가 완성된다

변경:

Package.swift

Sources/

Tests/

AppComposition

정도다.

변경하지 않음:

project.pbxproj

이 구조가 안정되면 Feature 추가 PR에서 Xcode Project Conflict가 크게 줄어든다.


23. 하지만 Agent가 Package.swift만 수정했다고 믿으면 안 된다

여기서부터 검증이다.

Agent에게 파일 생성만 맡기면 안 된다.

반드시 실제 Toolchain이 판단하게 한다.

먼저:

swift package resolve \
  --package-path Packages/AppModules

그리고:

swift package show-dependencies \
  --package-path Packages/AppModules

SwiftPM은 Dependency Graph를 실제로 Resolve한다.

순환 Dependency나 잘못된 Package 설정이 있으면 여기서 발견할 수 있다.


24. Domain처럼 Host에서 실행 가능한 Package는 swift test

swift test \
  --package-path Packages/AppModules

을 사용할 수 있다.

다만 UIKit·SwiftUI와 iOS 전용 Framework에 의존하는 테스트는 macOS에서 실행하는 swift test만으로 충분하지 않을 수 있다.

이 경우 Xcode/iOS Simulator 검증을 사용한다.

이 부분을 Agent Rule에도 명시한다.


25. 최종 App Build는 xcodebuild로 확인한다

예:

xcodebuild \
  -project App/MyApp.xcodeproj \
  -scheme MyApp \
  -destination 'generic/platform=iOS Simulator' \
  build

Agent가 실제 Build 결과를 확인한다.

파일 생성 완료

≠

빌드 성공

이다.


26. Test가 필요하면 Simulator를 Agent가 찾는다

특정 iPhone 이름을 Rule에 고정하지 않는 편이 좋다.

Agent가:

xcrun simctl list devices available

로 사용 가능한 Simulator를 확인한다.

그 뒤 실제 테스트 가능한 Destination을 선택한다.

즉:

AGENTS.md

"iPhone 17 Pro를 사용해"

처럼 특정 환경을 고정하기보다는:

현재 설치된 iOS Simulator에서
사용 가능한 기기를 선택해 테스트한다.

라고 적는 편이 안전하다.


27. Agent의 완료 보고 형식도 정해둔다

Agent가 다음처럼 끝내게 한다.

FeatureProfile 생성 완료

생성 Target:
- FeatureProfileDomain
- FeatureProfileData
- FeatureProfilePresentation
- FeatureProfileComposition
- FeatureProfileDomainTests

변경:
- Packages/AppModules/Package.swift
- AppComposition/AppRoute.swift

Dependency:
ProfileData
→ ProfileDomain
→ Core

ProfileData
→ Network

ProfilePresentation
→ ProfileDomain
→ DesignSystem

검증:
- swift package resolve: PASS
- dependency graph: PASS
- xcodebuild: PASS
- tests: PASS

Xcode project 변경:
없음

이렇게 하면 사람이 Review하기도 쉽다.


28. Agent에게 Dependency Graph Review도 시킨다

Agent에게 단순 Build만 요청하지 않는다.

다음 Rule도 확인하게 한다.

FeatureProfilePresentation

↓

FeatureProfileData

X
FeatureProfileDomain

↓

Network

X
FeatureHome

↓

FeatureProfile

X

SwiftPM Build가 성공하더라도 Architecture Rule 위반은 있을 수 있기 때문이다.


29. Architecture Validation을 Agent Review 단계로 둔다

구현 Agent:

Feature 생성

Reviewer Agent:

Package.swift 분석

Dependency Direction 확인

Public API 확인

Test 확인

Build:

xcodebuild

이 구조면 더 안정적이다.


30. 나중에는 간단한 Architecture Test도 추가할 수 있다

예를 들어 CI Script가 Package.swift를 읽어 다음을 검사한다.

Domain → Data

X

Presentation → Data

X

Feature → Feature

X

처음부터 이런 도구를 만들 필요는 없다.

프로젝트가 커진 뒤 반복되는 실수가 생겼을 때 추가하면 된다.

초기에는 Agent Review + SwiftPM Build만으로도 꽤 충분하다.


31. 이 방식이 별도 Scaffold CLI보다 좋은 부분

사내 CLI를 만들면 결국 CLI 자체를 유지해야 한다.

새 Architecture 추가

↓

CLI 수정

↓

Template 수정

↓

CLI Release

↓

개발자 업데이트

AI Agent + Repository Template은 다르다.

Template PR

↓

Merge

↓

모든 Agent가 바로 새 규칙 사용

별도의 Binary 배포가 없다.


32. Template 변경도 Git Diff로 보인다

예:

Presentation/
+ Coordinator.swift

Template 자체를 PR로 Review한다.

다음부터 생성되는 Feature가 모두 같은 구조를 사용한다.

Tuist Template의 역할을 Repository 자체가 담당한다.


33. Architecture가 바뀌어도 자연어 Rule을 같이 수정한다

예를 들어 ViewModel 방식에서 TCA로 이동한다고 하자.

기존:

Presentation

View
ViewModel

새 방식:

Presentation

Feature
State
Action
Reducer
View

그러면:

.ai/templates/clean-feature/

와:

AGENTS.md

를 수정한다.

Agent에게:

새 Feature부터 TCA Template을 사용하고
기존 Feature는 자동 Migration하지 마.

라고 적을 수도 있다.


34. 여러 Template도 만들 수 있다

.ai/templates/
├── clean-feature/
├── clean-feature-tca/
├── core-module/
├── network-service/
├── widget/
└── app-extension/

하지만 Widget이나 Extension처럼 실제 Xcode Target이 필요한 것은 별도로 취급한다.

Agent가 일반 Feature처럼 .xcodeproj를 건드리지 않고 끝낼 수 없기 때문이다.


35. Template 선택도 Agent가 판단할 수 있다

사용자:

설정 화면 추가해줘.

Agent:

단순 UI Feature
→ clean-feature

사용자:

Notification Service Extension 추가해줘.

Agent:

Xcode Target 필요
→ extension template
→ project 변경 필요

이렇게 분기한다.


36. 그래서 AGENTS.md에 Project 변경 조건을 명확하게 적는다

일반 Feature

→ Package.swift만 수정
Widget

→ Xcode Project 변경 허용
App Extension

→ Xcode Project 변경 허용
Capability

→ Xcode Project / Entitlement 검토
Signing

→ Human Review

Agent가 무엇을 건드려야 하는지 판단하기 쉬워진다.


37. 여러 Package.swift가 더 좋은 경우도 있다

하나의 AppModules가 항상 정답은 아니다.

다음 상황에서는 Package를 분리하는 편이 좋다.

다른 앱에서도 사용

독립 Release 필요

다른 Repository로 이동 가능

팀 Ownership 분리

외부 배포 가능성

독립적인 Dependency Graph 필요

예:

NetworkKit

AnalyticsKit

CameraKit

같은 모듈이다.

이때는:

Packages/
├── NetworkKit/
├── CameraKit/
└── AppModules/

처럼 하이브리드 구조로 가도 된다.


38. 일반 Feature는 Target, 재사용 Library는 Package

기준을 아주 단순하게 잡을 수 있다.

이 App 안에서만 사용

→ AppModules 안 Target
여러 App / Repository에서 사용

→ 별도 Swift Package

이렇게 하면 Package.swift가 지나치게 많이 생기는 것도 막을 수 있다.


39. SwiftPM이 좋아진 것도 이 구조를 밀어준다

SwiftPM은 Local Package Dependency를 공식적으로 지원하며, Local Package는 버전 Resolution 없이 지정한 경로의 Source를 그대로 사용한다.

또:

Package.resolved

Package Traits

Package Plugins

Package Registry

show-dependencies

같은 기능을 제공한다.

Swift 6.3에서는 Xcode와 같은 오픈소스 빌드 엔진인 Swift Build를 SwiftPM에서 사용하는 통합 기능도 Preview로 들어갔다.

방향 자체가:

Xcode Build

SwiftPM Build

사이의 기반을 점점 통합하는 쪽이다.


40. 하지만 Agent에게 Dependency Manager 역할까지 넘기지는 않는다

잘못된 요청:

어떤 Library가 필요한지 네가 알아서
최신 버전으로 추가해.

보다는:

기존 Dependency로 먼저 해결해.

새 외부 Package가 꼭 필요하면
추가하기 전에 이유와 대안을 보고해.

가 좋다.

AGENTS.md:

## External Dependencies

Do not add a new external dependency automatically.

Before adding one:

1. Check whether the current project can solve the problem.
2. Explain why a new dependency is required.
3. Report its license and maintenance risk.
4. Wait for approval.

Dependency 추가는 Supply Chain 문제이기도 하다.


41. Package Manager를 Agent로 대체하면 안 되는 이유

AI가:

Dependency Graph는 문제없어 보입니다.

라고 말하는 것은 검증이 아니다.

SwiftPM이:

Resolved

해야 한다.

Agent:

코드가 컴파일될 것 같습니다.

도 검증이 아니다.

Xcode가:

BUILD SUCCEEDED

해야 한다.

역할을 나눈다.

Agent
→ 생성 / 수정 / 판단

SwiftPM
→ Dependency 검증

Compiler
→ Type 검증

Tests
→ 동작 검증

이 구조가 좋다.


42. 결국 “CLI 대신 Agent”라는 표현은 이렇게 이해하면 된다

개발자는:

CLI 명령을 기억하지 않는다.

Agent는:

필요한 CLI를 알고 실행한다.

따라서 정확하게는:

CLI-less Developer Experience

+

CLI-backed Verification

이다.

Custom CLI를 하나 더 유지하지 않아도 된다.


43. 실제 일상 Workflow

아침에 개발자가 말한다.

FeatureFavorites 만들어줘.

Clean Architecture 기본 Template.

Domain:
즐겨찾기 Entity와 Repository Protocol

Data:
현재 Network 사용

Presentation:
SwiftUI + Observation

Composition:
기존 AppDependencies 사용

Home Feature와 직접 Dependency 만들지 마.

생성 후 Build와 Test 확인해줘.

Agent가 작업한다.

몇 분 뒤 개발자는 코드 Review만 한다.


44. 기존 Tuist Workflow와 비교하면

기존:

Architecture 변경

↓

Tuist Template 수정

↓

Project.swift 수정

↓

tuist generate

↓

Xcode

Agent + SwiftPM:

Architecture 변경

↓

Repository Template 수정

↓

AGENTS.md 수정

↓

Agent가 Package.swift와 Source 수정

↓

SwiftPM / Xcode Build

중간에 Project Generation 단계 자체가 없다.


45. 지금 새 iOS 프로젝트를 만든다면

최종적으로는 이렇게 간다.

                   MyApp.xcodeproj
                          │
                      App Target
                          │
                          ▼
                   AppComposition
                          │
                 SwiftPM AppModules
                          │
         ┌────────────────┼────────────────┐
         │                │                │
       Core            Shared          Features
         │                │                │
      Network        DesignSystem      Home
      Storage                          Profile
      Logging                          Camera
                                      Settings

그리고 옆에:

AGENTS.md

+

.ai/templates/

+

Codex

를 둔다.


46. 이 구조에서 Agent가 사실상 Tuist Scaffold 역할을 한다

하지만 중요한 차이가 있다.

Tuist Scaffold는:

정해진 Parameter

↓

정해진 Template

중심이다.

Agent는:

Template

+

Architecture Rule

+

현재 Repository

+

사용자 요구사항

을 함께 본다.

예를 들어:

Profile은 Network가 필요하지만
Settings는 Network가 필요 없음

을 판단해 Dependency를 다르게 만들 수 있다.

그래서 불필요한 Dependency도 줄일 수 있다.


47. 대신 자유도를 반드시 제한해야 한다

Agent에게:

적당히 좋은 Architecture로 만들어줘.

라고 하면 매번 구조가 달라질 수 있다.

좋은 방식:

Template은 고정

Dependency Rule은 고정

Naming Rule은 고정

변경 가능 영역만 Agent 판단

이다.

AI를 Architecture 결정자로 쓰기보다 Architecture 규칙을 실행하는 Agent로 쓰는 편이 안정적이다.


48. 처음 적용한다면 이 순서로 하면 된다

1단계

Xcode Project에서 일반 Business Logic을 분리한다.

App

과:

AppModules

로 나눈다.

2단계

SwiftPM에:

Core

Network

DesignSystem

AppComposition

을 만든다.

3단계

첫 Feature를 사람이 제대로 만든다.

FeatureHome

이게 Reference Implementation이 된다.

4단계

FeatureHome 구조에서:

clean-feature Template

을 만든다.

5단계

AGENTS.md에 Architecture Rule을 기록한다.

6단계

Agent에게 두 번째 Feature를 생성시킨다.

FeatureProfile

7단계

Diff를 사람이 Review한다.

8단계

잘못된 패턴이 나오면 Prompt를 길게 고치는 대신:

Template

또는

AGENTS.md

를 고친다.


49. 이게 중요한 운영 원칙이다

Agent가 같은 실수를 두 번 했다.

매번:

이번에는 Data에서 Presentation 참조하지 마.

라고 말하지 않는다.

AGENTS.md에 Rule을 추가한다.

Data targets MUST NOT depend on Presentation targets.

Agent 실패를 Repository Rule 개선으로 돌린다.

그렇게 시간이 지나면 프로젝트 자체가 Agent에게 점점 더 이해하기 쉬운 구조가 된다.


50. 마무리

Tuist를 제거한다고 해서 다시 새로운 Project Generator나 Scaffold CLI를 만들어야 하는 것은 아니다.

2026년 iOS 프로젝트에서는 역할을 훨씬 단순하게 나눌 수 있다.

SwiftPM

Module / Dependency
Xcode

Application Target
Signing
Capability
Repository Template

Architecture Skeleton
AGENTS.md

Architecture Policy
AI Agent

Module 생성
Package.swift 수정
Dependency 연결
검증 실행

그리고 실제 검증은:

SwiftPM

Compiler

xcodebuild

Tests

가 한다.

이 구조에서 개발자는:

tuist scaffold ...

swift package ...

project.yml ...

같은 생성 명령을 매번 기억할 필요가 없다.

그냥 말하면 된다.

FeatureProfile 만들어줘.

우리 Clean Architecture 규칙대로 만들고
필요한 Dependency만 연결하고
빌드와 테스트까지 확인해줘.

Agent가 Repository에 이미 존재하는 Template과 Policy를 읽고 실행한다.

여기서 가장 중요한 부분은 AI가 마음대로 Architecture를 만드는 것이 아니다.

Architecture

사람이 정의
Template

Repository가 보관
Rule

AGENTS.md가 강제
반복 작업

Agent가 수행

하는 구조다.

한 줄로 정리하면:

Tuist를 AI로 대체하는 것이 아니라,

SwiftPM을 기반으로
Tuist가 제공하던 반복적인 개발자 경험을
AI Agent에게 맡기는 것이다.

저라면 지금 새 iOS 프로젝트에서는 작은 Xcode Project + SwiftPM Local Package + Clean Architecture Template + AGENTS.md + Coding Agent부터 시작한다.

그리고 실제로 불편해지는 지점이 나타날 때만 XcodeGen이나 Bazel 같은 더 무거운 도구를 추가한다.

AI Agent 시대에는 새로운 CLI를 하나 더 만드는 것보다, 프로젝트 자체를 Agent가 안정적으로 이해하고 수정할 수 있는 구조로 만드는 것이 점점 더 중요해지고 있다.


참고 자료

  • Apple Developer — Organizing your code with local packages
    앱 내부 코드를 Local Swift Package로 분리해 모듈화하고 유지보수성과 재사용성을 높이는 Apple 공식 가이드.

  • Apple Developer — Managing files and folders in your Xcode project
    File-system folder 기반 프로젝트 구조와, 파일 추가·삭제 시 Project File 변경을 줄여 Merge Conflict를 줄이는 Xcode 공식 가이드.

  • Swift.org — Swift 6.3 Released
    Swift 6.3의 Swift Package Manager 개선과 Swift Build 통합 Preview를 설명하는 공식 릴리스 자료.

  • Swift Package Manager — Adding dependencies to a Swift package
    package(path:)를 이용한 Local Package Dependency와 SwiftPM Dependency 관리에 대한 공식 문서.

  • Swift Package Manager — Resolving package dependency failures
    swift package resolve, Package.resolved, swift package show-dependencies를 이용한 Dependency Resolution과 Graph 확인 방법.

  • OpenAI — Introducing Codex
    Repository의 AGENTS.md를 통해 Codex에 코드 구조, 프로젝트 규칙, Test Command와 작업 방식을 제공하는 공식 자료.

  • OpenAI — Unrolling the Codex agent loop
    Codex가 Repository와 디렉터리별 AGENTS.md를 어떻게 읽고, Sandbox·Tool·Instruction을 결합해 실제 작업을 수행하는지 설명한 공식 자료.

핵심 참고 포인트

Apple은 Local Swift Package를 앱 내부 코드 모듈화에 사용하는 방식을 공식적으로 지원한다. Local Package는 앱과 같은 Repository에서 관리할 수 있으며 필요하면 이후 별도 Repository로 분리해 재사용할 수도 있다.

현재 Xcode의 folder 기반 프로젝트 구조는 디스크의 Folder와 Project Navigator를 동기화하고, 파일 추가·삭제 때 Project File 표현을 크게 늘리지 않도록 설계되어 있어 기존 Group 중심 구조보다 project.pbxproj 변경과 Merge Conflict를 줄이는 데 도움이 된다.

SwiftPM은 Local Dependency에 package(path:)를 공식 지원하고, swift package resolve로 Dependency Graph를 해석하며 swift package show-dependencies로 전체 의존 관계를 확인할 수 있다.

Swift 6.3은 2026년 3월 정식 출시됐으며 SwiftPM이 Swift Build를 사용할 수 있는 통합 기능이 Preview로 추가됐다. Swift 프로젝트의 Build Infrastructure를 장기적으로 더 통합하려는 방향을 확인할 수 있다.

Codex는 Repository의 AGENTS.md를 프로젝트 작업 지침으로 사용할 수 있다. Code Convention뿐 아니라 Repository 구조, Test 명령, 검증 절차 등을 제공할 수 있고, 하위 디렉터리에 더 구체적인 AGENTS.md를 배치해 Scope별 규칙을 적용하는 것도 가능하다.

따라서 이 구조에서 AI Agent는 새로운 Package Manager가 아니라 Repository Policy를 읽고 SwiftPM·Xcode Toolchain을 조작하는 자동화 계층으로 보는 것이 가장 정확하다.

profile
iOS 앱 개발자

0개의 댓글