버그 잡이

Swift Pakcage Manager로 프로젝트 기능 모듈화 하기 본문

IOS

Swift Pakcage Manager로 프로젝트 기능 모듈화 하기

버그잡이 2024. 6. 22. 14:58

왜 SPM으로 모듈화를 하는가?

- 모듈화를 해보고 싶었습니다.

- Tuist를 활용해서 하는 방법도 있었지만 Tuist까지 같이 도입하기에는 시간이 부족했고

- Tuist를 도입하기 전에 SPM으로 간단하게(?) 레거시 프로젝트에서 모듈화가 가능할지 테스트 해보고 싶었습니다.

- 그리고 SPM으로도 충분하지 않을까? 라는 생각도 있었습니다.

 

이번 목표

- Invest라는 새로운 feature를 Package로 분리하고 Demo Project까지 만드는 것

 

Invest 패키지 추가

1. 프로젝트 생성 후 프로젝트 루트 경로에 Modules 라는 폴더를 추가합니다.

 

2. Modules 폴더 안에 Invest 패키지를 추가합니다.

 

- Library를 선택해주고

- 생성시 경로를 Modules 폴더 안으로 설정 후 create 해주어야 합니다.

 

3. 그 결과 아래와 같이 Invest 패키지가 생성된 것을 볼 수 있습니다.

 

이렇게 생성된 패키지가 프로젝트에서 보이지 않을 수도 있는데요.

 

이때는 

1. finder에서 Invest 패키지 위치를 찾고 해당 폴더를 드래그 앤 드랍으로 프로젝트에 추가해줍니다.

2. Invest 패키지가 열린 상태에서는 프로젝트에서 접근시 Invest 패키지가 열리지 않습니다.

(이때는 패키지와 프로젝트를 모두 닫고 프로젝트를 다시 실행시켜주세요)

 

...

 

새로 만드는 프로젝트라면 이렇게 하면 끝입니다.

이렇게 하고 import Invest 해서 사용하시면 됩니다.

하지만 저는 레거시 프로젝트에 적용해야 했기 때문에 몇 가지 수정이 더 필요했습니다.

 

 

UIColor, UIFont 관련 패키지 설정

- 프로젝트 전반에 UIColor, UIFont를 extension으로 만들어서 쓰고 있었는데요.

- 새로운 패키지에서는 이에 접근할 수 없었습니다.

- 그래서 UIFont, UIColor 관련 로직을 Resource라는 패키지로 분리하고

- 기존 App과 Invest 패키지가 Resource를 바라보도록 설정했습니다.

 

위와 같이 Resource 패키지를 추가해주고

Invest의 Package에 dependency를 추가해서 Resource를 import 할 수 있게 합니다.

// swift-tools-version:5.3
import PackageDescription

let package = Package(
    name: "Invest",
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
            name: "Invest",
            targets: ["Invest"]),
    ],
    dependencies: [
        .package(path: "../Resource")
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .target(
            name: "Invest",
            dependencies: [
                "Resource"
            ]),
        .testTarget(
            name: "InvestTests",
            dependencies: ["Invest"]),
    ]
)

 

그런데 여기서 문제가 하나 더 있습니다.

기존 프로젝트(ModuleApp)는 customFont와 customColor를 수많은 폴더에서 사용하고 있습니다.

이 모든 사용부에 import를 해줘야 하는데요... 너무 많습니다...

 

@_exported

그래서 찾은 방법이 @_exported입니다.

하위 모듈을 import 할 때, @_exported를 사용하면 한번만 import 해서 모듈 전역적으로 사용할 수 있습니다.

(비공식 속성이라고 하니 주의해서 쓸 필요가 있겠습니다.)

/// ModuleApp에서 아래 코드 한번 호출

@_exported import Resource


자세한 내용은 민소네님 블로그를 참조하시기 바랍니다.
https://minsone.github.io/programming/swift-annotation-_exported

 

 

Network config

다음으로는 네트워크 모듈입니다.

네트워크도 위와 같은 방식으로 모듈을 하나 추가해주시고요.

모듈 안에 아래와 같이 NetworkManager를 추가할 수 있습니다.

(아래 코드는 그냥 샘플이니 가볍게 봐주시고요. 여기서 포인트는 endpoint를 넣어주는 것이니 이 부분만 주의깊게 봐주세요)

import Foundation

public class NetworkManager {
    public static let shared = NetworkManager()
    
    private var endpoint: URL?
    
    private init() {}
    
    public func setEndpoint(_ url: URL) {
        self.endpoint = url
    }
    
    public func fetchData(completion: @escaping (Data?, Error?) -> Void) {
        guard let endpoint = endpoint else {
            completion(nil, NSError(domain: "Endpoint not set", code: -1, userInfo: nil))
            return
        }
        
        let task = URLSession.shared.dataTask(with: endpoint) { data, response, error in
            completion(data, error)
        }
        task.resume()
    }
}

 

근데 문제가 있습니다.

기존 레거시 프로젝트는 Target 별로 서로 다른 EndPoint 쓰는데요. 

Network 패키지는 각 타겟이 없기 때문에 이를 알 수 없습니다.

그래서 저는 Network 패키지에 ConfigManager를 만들어 App 시작시 endpoint를 전달하는 작업을 했습니다.

이렇게 하면 앱 시작시 알맞는 endpoint를 전달할 수 있습니다.

import Foundation

class ConfigManager {
    static let shared = ConfigManager()
    
    private var apiEndpoint: URL?
    
    private init() {}
    
    func configure(with endpoint: URL) {
        self.apiEndpoint = endpoint
        NetworkManager.shared.setEndpoint(endpoint)
    }
    
    func getEndpoint() -> URL? {
        return apiEndpoint
    }
}

 

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        if let apiEndpoint = URL(string: "https://api.example.com") {
            ConfigManager.shared.configure(with: apiEndpoint)
        }
       
        return true
    }
}

 

(사실 저도 이 방법이 좋은 방법인지에 대해서는 아직 확신이 들지 않습니다... 뭔가 더 좋은 방법이 있을 것 같은데... 혹시 더 좋은 방법을 알고 계신다면 댓글로 공유 부탁드립니다!)

 

 

InvestPorject 만들기

자 이제 원하는 모듈화는 끝났습니다.

이제 만든 모듈을 바탕으로 InvestProject를 만들어 보겠습니다.

 

프로젝트 루트 경로에 "DemoProejcts" 폴더를 만들고 그 안에 InvestProject를 생성해줍니다.

 

그리고 InvestProject의 Package Dependecies에 가서 아까 만든 프로젝트 들을 추가해줍니다. 

 

이렇게 Invest 모듈용 데모앱이 완성되었습니다!

 

 

 

'이렇게 모듈화를 해보니까 뭐가 좋았나요?'

 

다들 모듈화 모듈화 하는데 도대체 뭐가 그렇게 좋은지 경험으로 느껴보고 싶었습니다.

제가 느낀 모듈화의 장점은 아래와 같습니다.

 

1. 빠른 Preview

- SwiftUI를 사용한다면 빠릿빠릿한 Preview와 함께 개발이 가능합니다.

- 기존 프로젝트의 의존성에 영향받지 않아도 되기 때문에 로딩이 빠릅니다. (기존 프로젝트는 preview가 너무 느려 사용할 수 없는 수준이었습니다..)

- 이를 바탕으로 DesignSystem을 작업한다면 아주 빠르게 작업이 가능합니다. 

 

2. 독립 프로젝트를 활용한 빌드 속도 단축

- preview 속도 뿐만 아니라 DemoProject를 활용하면 빌드 속도를 비약적으로 개선할 수 있습니다.

 

3. 의존성을 더 신경쓰게 되는 개발

- 의존성을 관리하지 않고 코드를 짜면 모듈간의 컴파일 에러가 나고 불필요한 import를 하게 되는 것을 느끼기 때문에

- 보다 유연한 구조의 코드와 구조를 짜게 됩니다.

 

 

 

다음 글에서는 모듈화를 하면서 자잘하게 삽질했던 부분에 대해서 정리해보겠습니다.

좋은 하루 되세요:)

https://jinsangjin.tistory.com/188

 

Swift Pakcage Manager로 모듈화 하기 (기타 삽질편)

지난번 포스팅: https://jinsangjin.tistory.com/186 Swift Pakcage Manager로 프로젝트 기능 모듈화 하기왜 SPM으로 모듈화를 하는가?- 모듈화를 해보고 싶었습니다.- Tuist를 활용해서 하는 방법도 있었지만 Tuist

jinsangjin.tistory.com

 

 

 

 

반응형
Comments