본문 바로가기
👨‍💻 프로그래밍/🍎 iOS 개발

🍎 나만의 Swift Package 만들기! - 개인 프로젝트에서 중복되는 코드 없애기

by 개발자 진개미 2024. 8. 8.
반응형

Swift Package가 뭐고 이게 왜 우리 프로젝트에 필요할까

코딩을 하다보면 중복되는 코드가 나옵니다. 이럴 경우 일반적으로 함수나 클래스로 나눠서 관심사를 분리하고 중복을 줄입니다. 하지만 문제는 중복되는 코드가 프로젝트 단위로 나올 때 발생합니다. 분명 중복되는 코드인데, 다른 프로젝트니 import 할 수 없고, 결국 복붙을 하게 됩니다. 이 경우 문제가 여러가지 있습니다.

  • 당연히 중복되는 코드의 문제가 해결되지 않는다.
  • 변경점이 있으면 프로젝트 별로 적용해 줘야 한다.
  • 같은 역할을 하는데 미묘하게 변수나 사용법이 다른 경우가 있어 헷갈린다.

사실 프로젝트나 1개~2개 정도면 괜찮지만 10개, 20개, 심지어 100개가 된다면? 상상하기도 싫습니다. 제 현재 상황은 앱스토어에 출시 해 판매중인 앱이 2개, 개발중인 앱이 2개 있는데 이쯤 되도 중복되는 코드가 상당히 많아 점점 관리하기가 힘들어 지고 있었습니다.

그런 와중에 갑자기 떠오른 생각이, 우리가 import SwiftUI, import UIKit 하듯이 import Commons를 해서 중복된 코드를 하나의 프로젝트로 관리하면 좋지 않을까!? 라는 생각이 떠올랐습니다. 이 역할을 해 주는 친구가 바로 Swift Package입니다!


Swift Package 차근차근 만들기

🐜 1 - Package Project 만들기

  • 상단의 메뉴바에서 File -> New -> Package를 선택해 줍니다. 못 찾으시겠으면 단축키 Option + Shift + Command + N 을 눌러줘도 됩니다.

  • 그러면 새로운 XCode 프로젝트가 열리고 Package의 종류를 선택해야 하는데 Library를 선택해 줍니다.

  • 원하는 이름을 넣어주고 위치를 지정해 주면 프로젝트가 생성됩니다!

 

🐜 2 - Package.swift 파일에 필요한 설정하기

  • 처음 프로젝트를 열면 기존에 보던 UI 프로젝트들과는 상당히 다릅니다. 좀 간단한 편이죠?

 

  • 여기서 Package.swift가 좀 핵심인 친구입니다. 여기에서 필요한 여러 설정을 할 수 있는데요.
    • 이 Package가 다른 Package를 또 사용한다면 그거에 관한 설정
    • Package를 사용하기 위한 최소 버전 (Swift, iOS, MacOS 등)
    • Resources의 위치
    • 자세한건 애플의 공식문서를 참고해 주세요!
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Example",
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
            name: "Example",
            targets: ["Example"]),
    ],
    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: "Example"),
        .testTarget(
            name: "ExampleTests",
            dependencies: ["Example"]),
    ]
)

 

🐜 3 - 라이브러리화 하고 싶은 코드를 작성하기

  • 일반적인 경우와 똑같이 코드를 작성해 주시면 됩니다!
  • 주의할 점은 아래 3가지 정도가 있습니다.
    • Package를 사용하는 곳의 Resource는 Parameter로 받아야 함
    • 범용적으로 사용 가능하도록 설계
    • Swift의 접근 제한자 (Access Modifier)는  기본적으로 internal이기 때문에 제공하고 싶은 코드는 public으로 해야 합니다.

 

// Extension은 public을 붙여주면 자동으로 모든 변수 / 함수에 적용됩니다.
public extension Date {
    var year: Int {
        return Calendar.current.component(.year, from: self)
    }
}

// 혹시 관심사 분리를 위해 여러개의 extension을 만든다면 여기도 public 잊지 않기!
// 아래 코드는 public이 없어서 접근이 안 됩니다.
extension Date {
    static func of(year: Int?, month: Int?, day: Int?) -> Date? {
        if let year = year, let month = month, let day = day {
            var dateComponents = DateComponents()
            dateComponents.year = year
            dateComponents.month = month
            dateComponents.day = day
            
            return Calendar.current.date(from: dateComponents)
        }
        
        return nil
    }
}

// View의 경우 View struct, var body 모두 public으로 해야 합니다.
// init도 public으로 하는 거 잊지 마세요!
public struct NumberField: View {

    var label: LocalizedStringKey
    @Binding var number: String
    
    // public으로!
    public init(_ label: LocalizedStringKey, number: Binding<String>) {
        self.label = label
        self._number = number
        self.maxNumber = 1_000_000_000
    }

	// 여기도 public!
    public var body: some View {}
}

 

🐜 4 - Tag를 붙여서 Github에 올리기

  • 이대로 바로 코드를 올리면... 사실 사용할 수 없습니다. Package는 기본적으로 버전을 관리해 줘야 하기 때문인데요. 
  • Github를 사용하신다면 Releases라는 기능이 있습니다. 여기에 Tags을 붙여 주면 버저닝을 할 수 있어 Tags를 같이 올려줘야 합니다.

 

  • 방법은 어렵지 않습니다. 우선 Github를 XCode와 연결해 주고 Commit까지 한 상태에서 Source Control 쪽에 가면 (단축키 Command + 2) Tag를 지정할 수 있습니다.

 

  • 그런 후 Push를 할 때 Include tags에 체크를 해서 Push를 하면 됩니다.


만든 Swift Package 프로젝트에서 사용하기

  • Swift PackageTags까지 잘 붙여서 Github에 올렸으면, XCode에서 추가할 수 있습니다.
  • ProejctTarget으로 이동해 주세요.

 

  • 그리고 Frameworks, Libraries, and Embedded Content로 가시면 추가 (+) 버튼이 있는데 이 아이를 눌러 줍니다.

 

  • 추가 목록에 보이는건 기본적인 Apple에서 제공하는 Swift Package와 내가 이미 추가한 Package들 입니다. 새로 추가하려면 밑의 Add Other...에서 Add Package Dependency를 선택해 주세요.

 

  • 이제 여기에 Github에서 Star를 누른 모든 프로젝트가 뜨는데 굳이 여기서 찾지 마시고, 오른쪽 위의 검색 창에 Github Repo의 URL을 넣어 줍니다. Github를 연결 안 하셨으면 연결해 주세요! Private Repo인 경우 Github에서 토큰 발행도 진행 해 주셔야 합니다.



  • 추가하고 싶은 Swift Package를 찾으셨으면, 추가 (Add Package)를 누르고 과정을 따라주면 됩니다.



  • 추가가 다 되면 프로젝트 왼쪽 아래 Package Dependencies에 나만의 Swift Package가 추가된 걸 보실 수 있습니다! 이제 import로 사용하시면 됩니다.

 

  • Package 버전이 업데이트 됐다면 업데이트를 원하는 Package를 마우스 왼쪽 버튼을 눌러 Update Package를 선택해 주시면 됩니다. 저는 Update Package를 자주 해서 단축키도 지정 해 놨습니다.


어떤 코드를 공통화 해야 할까?

사실 거창하게 라이브러리 (Library)라고 했지만 사실 지금 만드는 Package는 내 프로젝트에서만 쓰이니 범용성을 너무 생각할 필요는 없습니다. 내 앱들에서 정말 공통되는 부분이라면 과감하게 넣었습니다.

예를들어 저 같은 경우 모든 앱의 설정 화면 맨 아래와 아래와 같이 앱의 버전명과 문의할 수 있는 이메일, 그리고 다른 서비스에 대한 홍보 링크를 집어 넣는데요. 이건 당연히 제 앱들 아니면 쓰이지 않는 코드지만, 제 앱에서는 무조건 쓰이기 때문에 과감하게 컴포넌트화 했습니다.

 

public struct SettingsInfoSection: View {
    
    let version: String
    let targetService: BooksitoutService
    
    public init(version: String, targetService: BooksitoutService) {
        self.version = version
        self.targetService = targetService
    }
    
    public var body: some View {
        Section(header:  Text(LocalizedStringKey("Settings - Section - Others"), bundle: .module)) {
            HStack {
                Text(LocalizedStringKey("Versions"), bundle: .module)
                Spacer()
                Text(version)
                    .foregroundColor(.secondary)
            }
            
            SettingsEmailButton()
            
            ForEach(BooksitoutService.allCases) { service in
                if service != targetService {
                    SettingsLinkButton(serviceInfo: service.info)
                }
            }
        }
    }
}

 

또, 저는 본업에서 Kotlin을 쓰고 있는데, Kotlin의 여러 내장 함수를 정말 좋아합니다. 그런데 Swift에서는 내장 함수들이 부족해서 답답한 경우가 많더라구요. 그래서 자주 쓰는 Kotlin 내장 함수를 비슷하게 extension func로 구현해 놨습니다.

public extension Array {
    func take(_ n: Int) -> [Element] {
        guard n > 0 else { return [] }
        return Array(self.prefix(n))
    }

    func takeLast(_ n: Int) -> [Element] {
        guard n > 0 else { return [] }
        return Array(self.suffix(n))
    }
    
    func drop(_ n: Int) -> [Element] {
        guard n > 0 else { return self }
        return Array(self.dropFirst(n))
    }

    func dropLast(_ n: Int) -> [Element] {
        guard n > 0 else { return self }
        return Array(self.suffix(self.count - n))
    }
}

 

마지막으로 자주 쓰는 SwiftUI의 ViewView Modifer 중에서 사용법이 직관적이지 않거나 범용적인데 나는 특정 용도로만 쓴다! 같은 것들도 넣어 놨습니다.


참고

 

Creating a standalone Swift package with Xcode | Apple Developer Documentation

Bundle executable or shareable code into a standalone Swift package.

developer.apple.com


반응형