ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Tuist] 모듈 생성 및 테스트 환경 구축
    개발/Swift 2025. 1. 14. 22:07

    Tuist를 활용하여 프로젝트를 모듈화하고, 각 모듈 내에 테스트 환경을 구축하는 구체적인 방법을 알아보겠습니다. 이는 개발 효율성을 극대화하고, 앱의 유지보수성을 향상시키는 데 큰 도움이 될 것입니다.

    프로젝트를 모듈화하는 것은 코드 재사용성을 높이고, 특정 기능에 대한 의존성을 명확히 하며, 빌드 시간을 단축하는 데 필수적입니다. Tuist는 이러한 모듈화를 매우 효율적으로 지원합니다.
    tuist edit 명령어를 실행한 후, 메인 앱 타겟의 의존성(dependencies) 설정을 살펴보면 다음과 같이 다양한 외부 라이브러리들이 설정되어 있는 것을 확인할 수 있습니다.

     

    dependencies: [TargetDependency] {
            return [            
                .external(name: "AdFitSDK"),
                .external(name: "AMPopTip"),
                .external(name: "ChannelIOSDK"),
                .external(name: "CocoaLumberjack"),
                .external(name: "CocoaLumberjackSwift"),
    						....
    						]
    
    여기에 우리가 새로 만들 모듈 프로젝트를 추가할 수 있습니다.
    예를 들어, MyProfile이라는 기능 모듈과 Entity라는 데이터 모듈을 추가한다면 다음과 같이 설정할 수 있습니다.
    .project(target: "MyProfile", path: .relativeToRoot("Projects/MyProfile")),
    .project(target: "Entity", path: .relativeToRoot("Projects/Entity")),
    
    
    이 설정은 메인 앱이 MyProfileEntity 모듈에 의존한다는 것을 의미합니다.

     

    모듈별 Project.swift 파일 생성
    모듈을 추가했다면, Project 폴더 내부에 위와 같이 각 모듈에 해당하는 폴더를 만들고 그 안에 Project.swift 파일을 생성해야 합니다. 이 파일은 해당 모듈의 빌드 방식을 정의합니다. 설정은 비교적 간단합니다.
    Entity 모듈의 Project.swift 파일 예시:
    Entity 모듈은 특정 외부 의존성 없이 순수한 데이터 구조나 공통 정의를 포함할 때 유용합니다.
    //Entity Project 파일
    
    import TemplatePlugin
    import ProjectDescription
    import TargetPlugin
    
    let project = Project(
        name: "Entity",
        settings: .librarySettings,
        targets: [
            .target(
                name: "Entity",
                destinations: .iOS,
                product: .staticFramework,
                bundleId: "com.Entity.Entity",
                sources: [
                    .glob(.path("Sources/**"))
                ],
                dependencies: [],
                settings: .settings(
                    base: [
                        "DEFINES_MODULE": "NO",
                    ]
                )
            )
        ]
    )
    MyProfile 모듈의 Project.swift 파일 예시:
    MyProfile 모듈은 Entity 모듈과 같은 내부 모듈, 그리고 ComposableArchitecture, RxSwift와 같은 외부 라이브러리에 의존할 수 있습니다.
    Tuist의 Registry 기능을 활용하면 이처럼 복잡한 외부 패키지 의존성도 빠르고 효율적으로 관리할 수 있어 패키지 해석 시간을 분 단위에서 초 단위로 단축시킬 수 있습니다.

     

    //MyProfile Project 파일
    
    let project = Project(
        name: "MyProfile",
        settings: .librarySettings,
        targets: [
            .target(
                name: "MyProfile",
                destinations: .iOS,
                product: .staticFramework,
                bundleId: "com.MyProfile.MyProfile",
                sources: [
                    .glob(.path("Sources/**"))
                ],
                dependencies: [
                    .project(target: "Entity", path: .relativeToRoot("Projects/Entity")),
                    .external(name: "ComposableArchitecture"),
                    .external(name: "RxSwift"),
                ],
           
                settings: .settings(
                    base: [
                        "DEFINES_MODULE": "NO",
                    ]
                )
            )
        ]
    )

     

     

    중요한 설정 및 고려사항:
    • product 타입: 위 예시에서는 staticFramework로 지정했습니다. framework로 지정했을 때 런타임에 EXC_BAD_ACCESS 에러가 발생했던 경험이 있습니다. 이 원인에 대해서는 추가적인 조사가 필요할 수 있습니다.
    • Sources 폴더 및 경로: 각 모듈 내에 Sources 폴더를 만들고, sources 설정에서 해당 경로를 정확히 지정하는 것이 중요합니다. 이 경로에 모듈의 실제 소스 코드가 위치하게 됩니다.
    • dependencies: 해당 모듈이 의존하는 다른 모듈(내부 프로젝트 모듈)이나 외부 라이브러리를 여기에 추가합니다.
    • settings: 이 부분은 Xcode 프로젝트의 Configuration이나 Info 탭에 들어가는 내용과 유사합니다. librarySettings와 같이 미리 정의된 설정을 활용하여 프로젝트 설정을 간소화할 수 있습니다. 예를 들어, librarySettings는 다음과 같이 정의될 수 있습니다.
    •  
     
    static let librarySettings: Settings = .settings(
        base: [
            "CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER": "NO",
            "DEVELOPMENT_TEAM": "39MSJ7FY3F",
            "EXCLUDED_ARCHS[sdk=iphonesimulator*]": ["x86_64"],
            "IPHONEOS_DEPLOYMENT_TARGET": "15.0",
            "ONLY_ACTIVE_ARCH": "YES",
            "TARGETED_DEVICE_FAMILY": "1",
        ],
        configurations: [
            .debug(name: "AppStore Debug"),
            .release(name: "AppStore Release"),
            .debug(name: "Development Debug"),
            .release(name: "Development Release"),
        ]
    )
    
    

     

    이 모든 설정을 마친 후 tuist generate 명령어를 실행하면, 새로 정의한 모듈들이 Xcode 프로젝트 내에 정확히 반영되는 것을 확인할 수 있습니다.
    Tuist의 Cache 기능 덕분에 한번 컴파일된 바이너리는 캐시되어 다음 빌드 시 컴파일 과정을 건너뛸 수 있어 전체 컴파일 시간을 크게 단축할 수 있습니다.

     

     

    모듈에서 테스트 환경 만들기

    모듈화된 프로젝트에서는 각 모듈의 기능이 독립적으로 잘 작동하는지 확인하기 위해 유닛 테스트 환경을 구축하는 것이 중요합니다.
    개발 중에 테스트 코드를 작성하려는데 유닛 테스트를 지정할 타겟이 없는 경우, tuist edit를 통해 해당 모듈의 Project.swift 파일을 수정하여 테스트 타겟을 추가할 수 있습니다.
    예를 들어, MyProfile 모듈에 대한 테스트 타겟을 추가하려면 Project.swift 파일 내의 targets 배열에 다음과 같은 unitTests 타입의 타겟을 추가합니다.
    let project = Project(
        name: "MyProfile",
        settings: .librarySettings,
        targets: [
            .target(
                name: "MyProfile",
                destinations: .iOS,
                product: .staticFramework,
                bundleId: "com.MyProfile.MyProfile",
                sources: [
                    .glob(.path("Sources/**"))
                ],
                dependencies: [
                    .project(target: "Entity", path: .relativeToRoot("Projects/Entity")),
    
                    .external(name: "ComposableArchitecture"),
                    .external(name: "RxSwift"),
                ],
           
                settings: .settings(
                    base: [
                        "DEFINES_MODULE": "NO",
                    ]
                )
            ),
            // MyProfileTests 타겟 추가
            .target(
                name: "MyProfileTests",
                destinations: .iOS,
                product: .unitTests, // 유닛 테스트 타입으로 지정
                bundleId: "com.MyProfile.MyProfileTests",
                sources: [
                    .glob(.path("Tests/**")) // 테스트 코드 소스 경로
                ],
                dependencies: [
                    .target(name: "MyProfile") // 테스트할 대상 모듈 지정
                ],
                settings: .settings(
                    base: [
                        "DEFINES_MODULE": "NO",
                    ]
                )
            )
        ]
    )
    이 코드에서 target을 하나 더 추가하고 product.unitTests 타입으로 설정합니다.
    또한, sources를 통해 테스트 코드의 경로(일반적으로 Tests/**)를 지정하고, dependencies에는 테스트하고자 하는 대상 모듈(MyProfile 모듈)을 추가합니다.
    이 설정을 마친 후 프로젝트 내부에 Tests 폴더를 생성하고 다시 tuist generate 명령어를 실행하면,
    새로운 테스트 타겟이 성공적으로 생성된 것을 확인할 수 있습니다.
    이제 Tests 폴더 안에 테스트 코드를 작성하고, 해당 코드의 타겟 멤버십을 새로 생성한 테스트 타겟으로 설정하면 됩니다.

     

     

     

    같은 모듈 내에서도 타겟이 다르면 기본적으로 internal 접근 수준의 함수는 접근할 수 없습니다. 따라서 테스트하려는 함수는 public으로 지정되어야 테스트 코드에서 사용 가능합니다.

     

    https://inf.run/iDaq4

     

    SwiftUI + TCA: 실전 프로젝트로 완성하는 차세대 iOS 아키텍처 강의 | 덤벨로퍼 - 인프런

    덤벨로퍼 | 복잡한 SwiftUI 상태 관리, TCA (The Composable Architecture)로 깔끔하고 견고한 앱을 만드세요. 실전 프로젝트 예제로 핵심만 빠르게 배웁니다. , SwiftUI + TCA: 실전 프로젝트로 완성하는 차세대

    www.inflearn.com

     

     

    댓글

Designed by Tistory.