본문 바로가기

Tuist

[Xcode] 프로젝트 모듈화 개념 정리(with Tuist)

 

안녕하세요 주니오스 입니다 🙌

 

 

이번글은 프로젝트 모듈화에 대해 다뤄보려고 합니다.

 

 

일반적으로 우리가 Xcode를 통해 프로젝트를 생성하게 되면 프로젝트 하나와 타깃하나가 생성되는 것을 확인할 수 있습니다.

(XCTest를 포함하는 경우 타깃은 2개입니다.)

 

 

그렇게 폴더구조를 나누고 개발을 진행하다보면 프로젝트의 규모가 엄청나게 커지는 것을 확인할 수 있습니다.

 

 

이렇게 되면 어떤 폴더에 어떤 것이 있는지 잘 알 수 없게 됩니다.

 

 

즉, 유지 보수가 힘들어 집니다.

(협업을 하는 경우 효율이 매우 떨어지는 결과를 야기합니다.)

 

 

그래서 사실 프로젝트 모듈화는 필수적이라고 필자는 생각합니다.

 

 

Xcode에서 모듈이란?

 

글을 읽는 분이 개발자라면 모듈에 대해 지겹도록 들어보셨을 것이라고 생각합니다.

 

 

Xcode에서 모듈이란 Target을 의미합니다.

 

 

그전에 먼저 Target이 뭔지 정리해보겠습니다.

 

Target

우리가 프로젝트를 만드는 이유는 결과적으로 Product(어플리케이션)를 만들기 위해서 입니다.

 

 

프로젝트란 수많은 디렉토리로 이루어진 큰 디렉토리일 뿐입니다. 그렇다면 빌드를 통해 특정 영역을 어플리케이션화 해야합니다.

 

 

해당 영역이 Target입니다.

 

 

Target은 결과적으로 Final Product(최종 생산물)로 표현됩니다.

 

 

원하는 기능을 타깃으로 나눠서 관리하면 원할한 관리를 할 수 있습니다.

 

 

Scheme

Xcode로 개발을 하시다 보면 Scheme이라는 단어를 만날 수 있습니다.

 

 

간단히 말해 빌드형식이라고 말할 수 있습니다.

 

 

Xcode는 Target단위로 빌드를 수행할 수 있습니다. 해당 타깃을 빌드하는 옵션들이 Scheme입니다.

 

 

여러개의 타깃을 한 프로젝트에서 구현하는 경우 Scheme을 바꿔가며 특정 타깃만 빌드가 가능합니다.

 

Scheme설정 사진

 

Dependency

Xcode에서 의존성은 다음을 의미합니다.

 

 

A(Target)의 코드 혹은 리소스를 B(Target)이 사용한다면 B는 A에 의존(dependent)한다고 말할 수 있습니다.

 

 

Tuist

Tuist는 Xcode프로젝트 관리를 원할하게 해주는 툴입니다.

(이 글에서 Tuist의 기능을 모두 다루지는 않습니다.)

 

 

Tuist는 manifests파일들이 swift파일로 되어있어 수정이 용이합니다! 👍🏻

 

 

아래코드는 manifest 파일들 중 하나인(사실 제일중요한) Project.swift파일의 초기 설정입니다.

 

// MARK: - Project

// Local plugin loaded
let localHelper = LocalHelper(name: "MyPlugin")

// Creates our project using a helper function defined in ProjectDescriptionHelpers
let project = Project.app(name: "TuistApp",
                          platform: .iOS,
                          additionalTargets: ["TuistAppKit", "TuistAppUI"])

 

 

저는 해당 파일을 아래와같이 수정하였습니다.

 

 

let basePath = "Targets/TuistApp"

let project = Project(
    name: "TuistApp",
    targets: [
        Target(
            name: "TuistApp",
            platform: .iOS,
            product: .app,
            bundleId: "TuistApp.bundle",
            deploymentTarget: .iOS(targetVersion: "16.0", devices: .iphone),
            sources: "\(basePath)/Sources/**",
        )
    ]
)

 

 

프로젝트가 잘 생성된 것을 확인할 수 있습니다.

(Target의 sources매개변수에 전달된 경로 끝에 **를 반드시 작성해야 합니다. 해당 경로하위 모든 파일, 디랙토리를 의미합니다.)

 

Tuist로 생성된 프로젝트

 

 

사용할 UI(Screen, View)들을 패키지 모듈로 분리하여 프로젝트를 구현해 보도록 하겠습니다.

 

 

먼저 Swift Package를 생성합니다.

 

 

프로젝트 폴더아래 Packages라는 폴더를 만들어 패키지를 저장하겠습니다.

 

 

MyAppUI Package(Swift Package)

 

 

Package를 생성하면 패키지 프로젝트 내부에 Package.swift파일을 확인할 수 있습니다.

 

 

이 파일을 수정하여 Package의 Target들을 선언하거나 의존성을 만들 수 있습니다.

 

 

import PackageDescription

let package = Package(
    name: "MyAppUI",
    platforms: [.iOS(.v16)],
    products: [
        .library(
            name: "MyAppUI",
            targets: ["MyAppUI"]),
    ],
    targets: [
        .target(
            name: "MyAppUI"),
        .testTarget(
            name: "MyAppUITests",
            dependencies: ["MyAppUI"]),
    ]
)

 

 

 

해당 패키지가 외부 패키지를 사용할 수 있도록 의존성을 만들어 보겠습니다.

 

 

Alamofire Package를 추가해 보겠습니다.

 

 

let package = Package(
    name: "MyAppUI",
    platforms: [.iOS(.v16)],
    products: [
        .library(
            name: "MyAppUI",
            targets: ["MyAppUI"]),
    ],
    dependencies: [
    	✋✋✋✋✋
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.1")
    ],
    
    ...

 

 

Swift Package Index

The Swift Package Index is the place to find the best Swift packages. Indexing metadata from 6,501 packages packages.

swiftpackageindex.com

 

패키지 URL은 위의 북마크를 이용하시면 빠르게 찾을 수 있습니다.

 

 

 

잘 추가된 것을 확인할 수 있습니다.

 

 

하지만 여기서 의문이 들 수 있습니다.

 

 

Target에서 코드를 작성하는데 왜 프로젝트의 Dependency에 외부 패키지를 추가하는 것일까? 🤔

(dependencies에 추가한 코드는 Project.Dependency타입입니다.)

 

 

외부 패키지 역시 여러 타깃을 포함한 프로젝트 입니다.

 

 

따라서 프로젝트간의 의존성이 선행되어야 하기 때문입니다.

 

 

이제 원하는 외부프로젝트(Alamofire)의 Target(= Product)과 해당 타깃을 사용할 저의 타깃의 의존성을 설정하면 됩니다!

 

 

.target(
    name: "MyAppUI",
    dependencies: [
        "Alamofire"
    ]
),

 

 

이 부분이 이해가 가시나요? 😚😚😚

 

 

Target의 생성자의 매개변수 dependencies의 요소는 Target.Dependency타입으로 String리터럴이 가능합니다.

 

 

String리터럴은 사용할 프로덕트(Target)을 의미합니다.

 

 

제 타깃이 포함된 프로젝트의 의존성에서 해당 프로덕트 찾아낸 후, 타깃간의 의존성이 설정됩니다.

 

 

자, 이제 저의 타깃에서 외부 프로덕트(Alamofire)를 import해보겠습니다.

 

import SwiftUI
✋✋✋✋✋✋
import Alamofire

public struct HelloFromMyAppUI: View {
    
    public init() { }
    
    public var body: some View {
        Text("Hello, from package")
            .padding(50)
            .background(
                Rectangle()
                    .foregroundStyle(.cyan)
            )
    }
}

 

 

정상적으로 import되는 것을 확인할 수 있습니다.

 

 

자이제 생성한 패키지를 메인 타깃(처음에 생성한 프로젝트의 타깃)에서 사용해 보겠습니다.

 

 

제가 만든 패키지와의 의존성이 필요함으로 먼저, manifest파일을 손봐줍니다.

 

 

마친가지로, 프로젝트간 의존성을 먼저 설정해준 후 Target(Product)간 의존성을 설정합니다.

 

 

let basePath = "Targets/TuistApp"

let project = Project(
    name: "TuistApp",
    packages: [
    	✋✋✋✋✋
        .local(path: "Packages/MyAppUI")
    ],
    targets: [
        Target(
            name: "TuistApp",
            platform: .iOS,
            product: .app,
            bundleId: "TuistApp.bundle",
            deploymentTarget: .iOS(targetVersion: "16.0", devices: .iphone),
            sources: "\(basePath)/Sources/**",
            dependencies: [
	        ✋✋✋✋✋
                .package(product: "MyAppUI")
            ]
        )
    ]
)

 

 

패키지가 문제없이 import되는 것을 확인할 수 있습니다.

 

import SwiftUI
import MyAppUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            HelloFromMyAppUI()
        }
    }
}

 

 

지금까지 Xcode에서 사용되는 모듈의 의미와 간단한 사용법에 대해 알아보았습니다.

 

 

이번글에 관심이 있으신 분들은 Tuist를 활용해볼 것을 강력하게 추천드립니다. 🙌

 

 

모듈간의 의존성 관리를 편하게 설정할 수 있습니다.

 

 

 

 

Tuist를 활용하여 몇몇 기능을 구현한 제 레포지토리입니다. 살펴보시면 좋을 것 같습니다.

 

GitHub - J0onYEong/SwiftUI-App-Tuist: Tuist툴을 사용하여 생성한 XCode SwiftUI프로젝트입니다. 연습용 프로젝

Tuist툴을 사용하여 생성한 XCode SwiftUI프로젝트입니다. 연습용 프로젝트로 Tuist사용법 숙지를 목적으로 생성된 학습용 저장소입니다. - GitHub - J0onYEong/SwiftUI-App-Tuist: Tuist툴을 사용하여 생성한 XCode

github.com

 

'Tuist' 카테고리의 다른 글

[Tuist] Build Configuration설정하기  (0) 2024.03.20