본문 바로가기
프로젝트관리

Tuist Feature Layer - OnBoarding 모듈 만들기

by zinozino 2025. 3. 4.

Tuist 프로젝트에서 Feature Layer를 구성하는 방법

Tuist 프로젝트에서 도메인 레이어를 구성하는 방법에 대해 알아보겠습니다.

1. Feature Layer 정의하기

Feature Layer는, Feature Layer와 App과 Domain Layer 사이에 위치하여 앱의 기능(화면)을 담당합니다.

 

 

Tuist-모듈러 아키텍처(TMA)

프로젝트 구조 설정

프로젝트에서 필요한 디렉토리 구조를 아래와 같이 생성합니다:

Manifests/
├─ ProjectDescriptionHelpers/
│  └─ FeatureLayer.swift     # Feature Layer 설정을 위한 헬퍼 파일
└─ Projects/
   └─ Feature/
      └─ Onboarding/               
         ├─ Interface/       # 인터페이스 정의
         ├─ Sources/         # 실제 구현체
         ├─ Testing/         # 테스트를 위한 Mock/Stub
         ├─ Tests/           # 단위 테스트
         ├─ Demo/            # 데모 앱 리소스 및 소스
         └─ Project.swift    # 프로젝트 설정 파일

 

 

FeatureLayer 모듈 구현

import ProjectDescription

public func makeFeatureLayer(name: String, targetDependency: [TargetDependency] = []) -> Project {
    
    let interfaceName = name + "Interface"
    let implementationName = name
    let testingTargetName = name + "Testing"
    let testTargetName = name + "Tests"
    let demopAppName = name + "DemoApp"
    
    let interface = Target.target(name: interfaceName,
                                  destinations: .iOS,
                                  product: .framework,
                                  bundleId: AppConfiguration.baseBundleId + "." + interfaceName,
                                  infoPlist: .extendingDefault(with: [
                                          "ENABLE_TESTS": .boolean(true),
                                          "DEVELOPMENT_ASSET_PATHS": "Preview Content",
                                          "ENABLE_PREVIEWS": "YES"
                                      ]),
                                  sources: ["Interface/**"]
    )
    
    let implementation = Target.target(name: implementationName,
                                       destinations: .iOS,
                                       product: Environment.isDebug ? .framework : .staticFramework,
                                       bundleId: AppConfiguration.baseBundleId + "." + implementationName,
                                       infoPlist:  .extendingDefault(with: ["ENABLE_TESTS": .boolean(true)]),
                                       sources: ["Sources/**"],
                                       dependencies: [
                                        .target(name: interfaceName)
                                       ] + targetDependency
    )
    
    let testing = Target.target(name: testingTargetName,
                                destinations: .iOS,
                                product: .framework,
                                bundleId: AppConfiguration.baseBundleId + "." + testingTargetName,
                                infoPlist:  .extendingDefault(with: ["ENABLE_TESTS": .boolean(true)]),
                                sources: ["Testing/**"],
                                dependencies: [
                                 .target(name: interfaceName)
                                ]
    )
    
    let test = Target.target(name: testTargetName,
                             destinations: .iOS,
                             product: .unitTests,
                             bundleId: AppConfiguration.baseBundleId + "." + testTargetName,
                             infoPlist:  .extendingDefault(with: ["ENABLE_TESTS": .boolean(true)]),
                             sources: ["Tests/**"],
                             dependencies: [
                                .target(name: testingTargetName),
                                .target(name: implementationName)
                             ]
    )
    
    //DemoApp
    let infoPlist: [String: ProjectDescription.Plist.Value] = [
        "UIApplicationSceneManifest": [
            "UIAppliactionSupportsMultipleScenes": true,
            "UISceneConfigurations": [
                "UIWindowSceneSessionRoleApplication": [
                    [
                        "UISceneConfigurationName": "Default Configuration",
                        "UISceneDelegateClassName": "$(PRODUCT_MODULE_NAME).SceneDelegate"
                    ],
                ]
            ]
        ],
        "UILaunchScreen": "LaunchScreen.storyboard",
        "UISupportedInterfaceOrientations":
            [
                "UIInterfaceOrientationPortrait",
            ],
        "CFBundleShortVersionString": "1.0.0",
        "CFBundleVersion": "1",
        "CFBundleDisplayName": "$(PRODUCT_MODULE_NAME)",
    ]
    
    let demoApp = Target.target(name: demopAppName,
                             destinations: .iOS,
                                product: .app,
                                bundleId: AppConfiguration.baseBundleId + "." + demopAppName,
                             infoPlist:  .extendingDefault(with: infoPlist),
                             sources: ["Demo/Sources/**"],
                                resources: ["Demo/Resources/**"],
                             dependencies: [
                                .target(name: testingTargetName),
                                .target(name: implementationName)
                             ]
    )
    
    let settings = makeFeatureSettings(name: name)
    let scheme = makeScheme(name: name)
    
    return Project(name: name,
                   organizationName: AppConfiguration.baseBundleId,
                   packages: [],
                   settings: settings,
                   targets: [interface, implementation, testing, test, demoApp
                   ],
                   schemes: [scheme]
    )
}

 

이렇게 설계한 이유

Feature Layer 계층의 특징

  • Interface, Implementation, Testing, Tests 타겟으로 구성
  • 각 기능을 독립적으로 실행해볼 수 있는 Demo 앱 포함
  • Testing 타겟을 통한 테스트 더블(Mock, Stub) 제공
  • 기능 모듈의 독립적인 테스트 환경 구성
  • UI/UX 검증을 위한 DemoApp 제고

의존성 관리

  • Interface를 통한 모듈 간 의존성 관리
  • Testing을 통한 테스트 코드 재사용성 확보
  • Implementation의 구체적인 의존성 은닉

 

주의 사항

Feature끼리는 Interface타겟으로 참조하기 때문에 구현부는 App에서 모두 넣어야하는 상황이 되므로,

App 시작 시점에 의존성의 앱에서 구체적으로 모두 제공 해야합니다.