iyOmSd/Title: Swift

[Swift] Tuist 모듈화 응용편 - 모듈화적용후 사용하기

냄수 2023. 1. 21. 20:21
반응형

앞서 배운 기본적인 개념을 응용해서 필요한 모듈만 불러와서 사용할 수 있도록 간단하게 구현해볼예정이에요 

그래프를 그렸을때 이쁘게 딱 나올수 있도록

App에서 Feature를 만들고 Feature에서

Core에있는 Logger를 사용해보도록

해보게씁니다!

 

시작은

디렉토리를 먼저 만들어주세요

그 디렉토리로 이동한 후 터미널로 프로젝트를생성 하면 준비는 끝납니다

tuist init --platform ios --template swiftui

 

프로젝트 설정을 시작해볼까요

tuist edit

프로젝트 설정관련 정의한 파일들은

ProjectDescriptioinHelper폴더에 담을거에요

다른 프로젝트 모듈에서 불러올때 

import ProjectDescriptionHelpers

로 불러올 수 있어요

 

 

프로젝트를 만들어주는 함수를 정의해둘게요

extension Project {
    static var organizationName: String { "ns" }
    
    public static func create(
        name: String,
        packages: [Package],
        settings: Settings? = Settings.projectSettings,
        targets: [Target],
        schemes: [Scheme]
    ) -> Project {
        Project(
            name: name,
            organizationName: organizationName,
            options: .options(
                disableBundleAccessors: true,
                disableSynthesizedResourceAccessors: true
            ),
            packages: packages,
            settings: settings,
            targets: targets,
            schemes: schemes
        )
    }
}

간단하게 organization이름을 정의해두고 사용하고

필요한값들은 선택적으로 받아오도록 구현했어요

 

 

다음으로는 타겟을 만들어주는 함수를 정의할거에요

extension Target {
    public static func create(
        targetName: String,
        product: Product,
        infoPlist: [String: InfoPlist.Value],
        scripts: [TargetScript],
        dependencies: [TargetDependency],
        settings: Settings?
    ) -> Target {
        Target(
            name: targetName,
            platform: .iOS,
            product: product,
            productName: targetName,
            bundleId: Project.organizationName + "." + workspaceName + "." + targetName,
            deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone),
            infoPlist: .extendingDefault(with: infoPlist),
            sources: ["Sources/**"],
            resources: ["Resources/**"],
            entitlements: nil,
            scripts: scripts,
            dependencies: dependencies,
            settings: settings
        )
    }
    
    public static func createWithoutResource(
        targetName: String,
        product: Product,
        infoPlist: [String: InfoPlist.Value],
        scripts: [TargetScript],
        dependencies: [TargetDependency],
        settings: Settings?
    ) -> Target {
        Target(
            name: targetName,
            platform: .iOS,
            product: product,
            productName: targetName,
            bundleId: Project.organizationName + "." + workspaceName + "." + targetName,
            deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone),
            infoPlist: .extendingDefault(with: infoPlist),
            sources: ["Sources/**"],
            entitlements: nil,
            scripts: scripts,
            dependencies: dependencies,
            settings: settings
        )
    }
}

bundleId는 {org 이름}.{workspace 이름}.{target 이름}

으로 지정될거라 구분이 모듈별로 이쁘게 되도록 했어요

예를들어

Core 모듈이라면

ns.NSWorkspace.Core이렇게 지정되겠네요

 

필요한부분은 함수로 정보를 받고

공통으로쓰는건 내부에 하드코딩으로 정의해서 사용했어요

 

 

workspace정의는

private func projectNameWith(module: Module) -> Path {
    return "\(workspaceName)/\(module.name)"
}

let workspace = Workspace(
    name: workspaceName,
    projects: [
        "\(workspaceName)/\(mainProjectName)",
        projectNameWith(module: .feature),
        projectNameWith(module: .ui),
        projectNameWith(module: .core),
        projectNameWith(module: .logger)
    ]
)

위에서 정의하려는 App, Feature, UI, Core, Logger

모듈들을 프로젝트로 만들어줍니다

 

이제 모듈하나하나 구조를 정의해줄거에요

워크스페이스이름을

NSWorkspace로 설정하고

그안에 프로젝트들(모듈들) 이들어갈 거에요

먼저 NSWorkspace디렉토리를 만들어주고

Workspace파일도 같은경로에 위치시켜주세요

Project가 해당 프로젝트를 만들어주는것처럼

Workspace파일이 워크스페이스를 만들어줍니다!

 

그뒤에

각각 모듈에맞는 디렉토리를 만들어주고

각디렉토리마다 Project.swift파일을 만들어주세요

각 프로젝트(모듈)들을 만들어주는작업이에요

 

 

작성하면 이런구조처럼 되겠네요

 

각 디렉토리에

Project.swift파일을 넣어줬으면

tuist edit으로 킨 엑스코드를 끄고

다시 tuist edit을 실행시켜서

최신화된 프젝트구조에서 수정할거에요

 

private let dependencies: [TargetDependency] = [
    .featureProject
]

private let mainTarget = Target.create(
    targetName: mainProjectName,
    product: .app,
    infoPlist: infoPlist,
    scripts: [],
    dependencies: dependencies,
    settings: .realSettings
)


let project = Project.create(
    name: mainProjectName,
    packages: [],
    targets: [targetWithMode(mode)],
    schemes: [schemeWithMode(mode)]
)

mainApp Target을 아래와같이 설정해주고

프로젝트를 생성하도록 정의했어요

 

targetWithMode함수는

입력된 mode의 값에따라 dev, rc, real을 구분하기위한 목적이라

그 값중 mainTarget이 반환된다 생각하시면되요!

스키마도 마찬가지입니다

무시하셔도됩니다!

 

이렇게정의하면

현재 

App -> Feature상태겠죠

 

 

프로젝트에 의존성을 걸기위해 미리 정의해두고 사용하도록

사용할 프로젝트의 디팬던시를 정의해줄거에요

public extension TargetDependency {
    static let coreProject: TargetDependency = .project(
        target: Module.core.name,
        path: "../\(Module.core.name)"
    )
    static let loggerProject: TargetDependency = .project(
        target: Module.logger.name,
        path: "../\(Module.logger.name)"
    )
    static let uiProject: TargetDependency = .project(
        target: Module.ui.name,
        path: "../\(Module.ui.name)"
    )
    static let featureProject: TargetDependency = .project(
        target: Module.feature.name,
        path: "../\(Module.feature.name)"
    )
}

 

 

이제 Feature프로젝트를 정의해줍시다

let featureTarget = Target.createWithoutResource(
    targetName: Module.feature.name,
    product: .framework,
    infoPlist: infoPlist,
    scripts: [],
    dependencies: [
        .coreProject,
        .uiProject
    ],
    settings: nil
)

let project = Project.create(
    name: Module.feature.name,
    packages: [
    ],
    targets: [
        featureTarget
    ],
    schemes: []
)

Feature는 dependencies로

core와 ui를 가지고있도록 설정했어요

여기까지는

이런그래프가 그려지겠네요!

 

 

다음으로

Core모듈을 설정해줄게요

let coreTarget = Target.createWithoutResource(
    targetName: Module.core.name,
    product: .framework,
    infoPlist: infoPlist,
    scripts: [],
    dependencies: [
        .loggerProject
    ],
    settings: nil
)

let project = Project.create(
    name: Module.core.name,
    packages: [
    ],
    targets: [
        coreTarget
    ],
    schemes: []
)

Core에서 Logger를 의존하도록 추가해줬어요

위와같은 그림이 완성됬겠네요!

 

설명은 생략됬지만

Logger, UI모듈의 타겟, 프로젝트 설정해주고

Sources, Resources 디렉토리까지 만들고

(더미파일까지 넣어야 프로젝트 구조가 잘보여요)

한번 tuist generate로 생성해볼까요

원하는데로 의존성이 잘 연결된것같네요

 

tuist graph를 이용해서 확인해볼 수 도있어요

계획하던데로 구조를 잘 구현했네요!

 

 

추가로

print를 이용해서

터미널에서 필요한정보를 노출시킬수도있어요

이런식으로

Mode별 분기를 하는데 현재 설정이 잘먹혔는지 확인해볼 수 있어요

 

 

이제 모듈화를 시켰으니

모듈을 사용해봅시다!

 

해보려고하는것은

MainApp에서

Logger모듈에 정의된 로거를 사용할거에요

 

MainApp은 Core모듈을 의존하고

Core모듈는 Logger모듈을 의존하도록 구현했으니까

 

Core에서 import Logger해서 로거를 구현해주고

MainApp에서 Core의 로거를 가져다쓰도록 해볼거에요

 

 

// Logger모듈

public enum NSLogger {
    public static func log(_ text: String) {
        print(text)
    }
}

로거모듈에서 간단하게

print를 찍는함수를 구현해뒀어요

다른모듈에서 접근해야하니까 public접근제어자를 사용했어요

 

 

// Core 모듈

import Foundation
import Logger

public protocol NSLoggerProtocol {
    func log(_ text: String)
}

public struct ALoggerImpl: NSLoggerProtocol {
    public func log(_ text: String) {
        NSLogger.log("🫠 "+text)
    }
    
    public init() { }
}

public struct BLoggerImpl: NSLoggerProtocol {
    public func log(_ text: String) {
        NSLogger.log("🐙 "+text)
    }
    
    public init() { }
}

Core모듈에서는 Main App에서 사용할 2개의 로거를 구현해뒀어요

 

 

// MainApp 모듈 App.swift

import SwiftUI
import Core

@main
struct test_App: App {
    var logger: any NSLoggerProtocol {
        [ALoggerImpl(), BLoggerImpl()].randomElement()!
    }
    var body: some Scene {
        WindowGroup {
            MainView(logger: logger)
        }
    }
}

Core모듈을 임포트해서

필요한 로거를 랜덤으로 넣어주도록하고

 

 

// MainView.swift

import SwiftUI
import Core

struct MainView: View {
    let logger: any NSLoggerProtocol
    
    var body: some View {
        Text("Hello, World!")
            .onAppear {
                logger.log("MainView 입니다")
            }
    }
}

뷰에서는 뭔지모를 로거를 실행만시키면되요

로거에 맞게 이모지가 변하겠네요

 

 

마무리된 프로젝트

폴더구조입니다

 

 

 

 

반응형