Ver3.0 ๐ Swift Package Manager์ ๋ชจ๋ํ๋ก ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ ๊ทน๋ํํ๊ธฐ

๐ Swift Package Manager์ ๋ชจ๋ํ๋ก ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ ๊ทน๋ํํ๊ธฐ
์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ๋ฐฐ์ฐ๋ SPM ์๋ฒฝ ๊ฐ์ด๋
์๋
! ๐ ์ค๋์ iOS ๊ฐ๋ฐ์๋ผ๋ฉด ๊ผญ ์์์ผ ํ Swift Package Manager(SPM)์ ๋ชจ๋ํ์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊ฒ. ํน์ ํ๋ก์ ํธ๊ฐ ์ ์ ์ปค์ง๋ฉด์ ์ฝ๋ ๊ด๋ฆฌ๊ฐ ํ๋ค์ด์ง ์ ์์ด? ์๋๋ฉด ๋ค๋ฅธ ํ๋ก์ ํธ์์ ๋๊ฐ์ ์ฝ๋๋ฅผ ๋ณต์ฌ-๋ถ์ฌ๋ฃ๊ธฐ ํ๋ฉด์ "์ด๊ฑฐ ์ข ๋นํจ์จ์ ์ธ๋ฐ..."๋ผ๊ณ ์๊ฐํ ์ ์๋ค๋ฉด, ๋ฐ๋ก ์ด ๊ธ์ด ๋๋ฅผ ์ํ ๊ฑฐ์ผ! ๐
์์ฆ ๊ฐ๋ฐ ํธ๋ ๋๋ ๋จ์ํ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ ๋์ด์, ์ผ๋ง๋ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๊ณ ์ ์ง๋ณด์ํ๊ธฐ ์ฌ์ด ์ฝ๋๋ฅผ ๋ง๋๋๋๊ฐ ํต์ฌ์ด์ผ. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ํ๋ก๊ทธ๋๋ฐ ์ฌ๋ฅ์ ๊ณต์ ํ ๋, ์ด๋ฐ ๋ชจ๋ํ๋ ์ฝ๋ ์์ฑ ๋ฅ๋ ฅ์ ์ ๋ง ์ค์ํ ์คํฌ๋ก ํ๊ฐ๋ฐ๊ณ ์์ด!
๐ค Swift Package Manager๊ฐ ๋ญ๊ธธ๋?
Swift Package Manager, ์ค์ฌ์ SPM์ Apple์ด ๊ณต์์ ์ผ๋ก ์ ๊ณตํ๋ ์์กด์ฑ ๊ด๋ฆฌ ๋๊ตฌ์ผ. ์ฝ๊ฒ ๋งํ๋ฉด, ๋ ๊ณ ๋ธ๋ก์ฒ๋ผ ์ฝ๋๋ฅผ ์กฐ๊ฐ์กฐ๊ฐ ๋๋ ์ ๊ด๋ฆฌํ๊ณ , ํ์ํ ๋๋ง๋ค ๊ฐ์ ธ๋ค ์ธ ์ ์๊ฒ ํด์ฃผ๋ ๋๊ตฌ์ง! ๐งฉ
์์ ์๋ CocoaPods๋ Carthage ๊ฐ์ ์๋ํํฐ ๋๊ตฌ๋ฅผ ๋ง์ด ์ผ๋๋ฐ, Apple์ด ์ง์ ๋ง๋ SPM์ Xcode์ ์๋ฒฝํ๊ฒ ํตํฉ๋์ด ์์ด์ ์ค์ ๋ ๊ฐ๋จํ๊ณ ์ฌ์ฉํ๊ธฐ๋ ํธํด. Swift 3.0๋ถํฐ ๋์
๋์๊ณ , ์ง๊ธ์ iOS ๊ฐ๋ฐ์ ํ์ค์ผ๋ก ์๋ฆฌ์ก์์ด!
๐ฏ SPM์ ํต์ฌ ์ฅ์
1. Xcode ๋ค์ดํฐ๋ธ ์ง์ - ๋ณ๋ ์ค์น ์์ด ๋ฐ๋ก ์ฌ์ฉ ๊ฐ๋ฅ
2. ๋น ๋ฅธ ๋น๋ ์๋ - ํจ์จ์ ์ธ ์บ์ฑ ์์คํ
3. ๋ฒ์ ๊ด๋ฆฌ ์ฉ์ด - Git ๊ธฐ๋ฐ์ ๋ช
ํํ ๋ฒ์ ๊ด๋ฆฌ
4. ํฌ๋ก์ค ํ๋ซํผ - iOS, macOS, watchOS, tvOS ๋ชจ๋ ์ง์
5. ์คํ์์ค - ์ปค๋ฎค๋ํฐ ๊ธฐ์ฌ์ ํฌ๋ช
ํ ๊ฐ๋ฐ ๊ณผ์
๐ก ์ค๋ฌด ํ
์ฌ๋ฅ๋ท์์ ํ๋ก๊ทธ๋๋ฐ ์ฌ๋ฅ์ ๊ฑฐ๋ํ ๋, SPM์ ํ์ฉํ ๋ชจ๋ํ ๋ฅ๋ ฅ์ ํฐ ๊ฒฝ์๋ ฅ์ด ๋ผ. ํด๋ผ์ด์ธํธ์๊ฒ "์ด ์ฝ๋๋ ๋ค๋ฅธ ํ๋ก์ ํธ์์๋ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๊ฒ ๋ชจ๋ํํ์ต๋๋ค"๋ผ๊ณ ๋งํ ์ ์๋ค๋ฉด, ํจ์ฌ ์ ๋ฌธ์ ์ผ๋ก ๋ณด์ด๊ฑฐ๋ ! ๐
๐ ๋ชจ๋ํ๋ ๋ฌด์์ธ๊ฐ?
๋ชจ๋ํ(Modularization)๋ ํฐ ํ๋ก๊ทธ๋จ์ ์๊ณ ๋
๋ฆฝ์ ์ธ ๋ถ๋ถ๋ค๋ก ๋๋๋ ๊ฒ์ ๋งํด. ๋ง์น ํฐ ํผ์ฆ์ ์์ ์กฐ๊ฐ๋ค๋ก ๋๋๋ ๊ฒ๊ณผ ๋น์ทํด. ๊ฐ ์กฐ๊ฐ(๋ชจ๋)์ ํน์ ํ ๊ธฐ๋ฅ์ ๋ด๋นํ๊ณ , ๋ค๋ฅธ ์กฐ๊ฐ๋ค๊ณผ ๋
๋ฆฝ์ ์ผ๋ก ์๋ํ ์ ์์ด์ผ ํด.
์๋ฅผ ๋ค์ด๋ณผ๊ฒ! ๐ ํผ์ ๋ฐฐ๋ฌ ์ฑ์ ๋ง๋ ๋ค๊ณ ์๊ฐํด๋ณด์:
๋ชจ๋ํ ์ (Monolithic ๊ตฌ์กฐ)
๋ชจ๋ ์ฝ๋๊ฐ ํ๋์ ๊ฑฐ๋ํ ํ๋ก์ ํธ์ ์์ฌ ์์
- ์ฌ์ฉ์ ์ธ์ฆ ์ฝ๋
- ์ฃผ๋ฌธ ์ฒ๋ฆฌ ์ฝ๋
- ์ง๋ ํ์ ์ฝ๋
- ๊ฒฐ์ ์ฒ๋ฆฌ ์ฝ๋
- UI ์ปดํฌ๋ํธ
โ ๋ฌธ์ ์ : ์ฝ๋ ์์ ์ ์ ์ฒด ์ฑ์ ๋ค์ ๋น๋ํด์ผ ํจ
โ ๋ฌธ์ ์ : ๋ค๋ฅธ ํ๋ก์ ํธ์์ ์ฌ์ฌ์ฉ ๋ถ๊ฐ๋ฅ
โ ๋ฌธ์ ์ : ํ ํ์
์ ์ถฉ๋ ๋ฐ์ ๊ฐ๋ฅ์ฑ ๋์
๋ชจ๋ํ ํ (Modular ๊ตฌ์กฐ)
๊ฐ ๊ธฐ๋ฅ์ด ๋
๋ฆฝ์ ์ธ ๋ชจ๋๋ก ๋ถ๋ฆฌ๋จ
- ๐ฆ AuthenticationModule (์ธ์ฆ)
- ๐ฆ OrderModule (์ฃผ๋ฌธ)
- ๐ฆ MapModule (์ง๋)
- ๐ฆ PaymentModule (๊ฒฐ์ )
- ๐ฆ UIComponentsModule (UI)
โ
์ฅ์ : ํ์ํ ๋ชจ๋๋ง ์์ ํ๊ณ ๋น๋
โ
์ฅ์ : ๋ค๋ฅธ ํ๋ก์ ํธ์์ ๋ชจ๋ ์ฌ์ฌ์ฉ ๊ฐ๋ฅ
โ
์ฅ์ : ํ์๋ค์ด ๊ฐ์ ๋ค๋ฅธ ๋ชจ๋ ์์
๊ฐ๋ฅ
๐ ๋ชจ๋ํ์ ์ค์ง์ ์ด์
์ค์ ๋ก ๋๊ท๋ชจ ํ๋ก์ ํธ์์ ๋ชจ๋ํ๋ฅผ ์ ์ฉํ๋ฉด ๋น๋ ์๊ฐ์ด 50% ์ด์ ๋จ์ถ๋๋ ๊ฒฝ์ฐ๋ ๋ง์! ์ ์ฒด ํ๋ก์ ํธ๋ฅผ ๋น๋ํ๋ ๋์ ์์ ํ ๋ชจ๋๋ง ๋ค์ ์ปดํ์ผํ๋ฉด ๋๋๊น. ๊ฐ๋ฐ ์์ฐ์ฑ์ด ์์ฒญ๋๊ฒ ์ฌ๋ผ๊ฐ๋ ๊ฑฐ์ง! ๐
๐ ๏ธ SPM ์์ํ๊ธฐ: ์ฒซ ๋ฒ์งธ ํจํค์ง ๋ง๋ค๊ธฐ
์, ์ด์ ์ค์ ์ผ๋ก ๋ค์ด๊ฐ๋ณผ๊น? ์ฒซ ๋ฒ์งธ Swift Package๋ฅผ ๋ง๋๋ ๊ณผ์ ์ ๋จ๊ณ๋ณ๋ก ์๋ ค์ค๊ฒ! ์๊ฐ๋ณด๋ค ์ ๋ง ๊ฐ๋จํด. ๐
Step 1: ์ Swift Package ์์ฑ
Xcode๋ฅผ ์ด๊ณ ๋ค์ ์์๋ฅผ ๋ฐ๋ผํด๋ด:
1๏ธโฃ File โ New โ Package... ์ ํ
2๏ธโฃ ํจํค์ง ์ด๋ฆ ์
๋ ฅ (์: MyNetworkKit)
3๏ธโฃ ์ ์ฅ ์์น ์ ํ
4๏ธโฃ Create ๋ฒํผ ํด๋ฆญ!
์ง ! ๐ ์ด์ ๊ธฐ๋ณธ ํจํค์ง ๊ตฌ์กฐ๊ฐ ์์ฑ๋์ด.
์์ฑ๋ ํจํค์ง ๊ตฌ์กฐ๋ฅผ ์ดํด๋ณด๋ฉด ์ด๋ฐ ํ์ผ๋ค์ด ์์ ๊ฑฐ์ผ:
MyNetworkKit/
โโโ Package.swift // ํจํค์ง ์ค์ ํ์ผ (๊ฐ์ฅ ์ค์!)
โโโ Sources/
โ โโโ MyNetworkKit/
โ โโโ MyNetworkKit.swift
โโโ Tests/
โโโ MyNetworkKitTests/
โโโ MyNetworkKitTests.swift
๐ก Package.swift ํ์ผ ์ดํดํ๊ธฐ
Package.swift๋ ํจํค์ง์ ์ค์ ํ์ผ์ด์ผ. ์ด ํ์ผ์์ ํจํค์ง์ ์ด๋ฆ, ์ง์ํ๋ ํ๋ซํผ, ์์กด์ฑ, ํ๊ฒ ๋ฑ์ ์ ์ํด. ๋ง์น ํ๋ก์ ํธ์ ์ค๊ณ๋ ๊ฐ์ ๊ฑฐ์ง!
๊ธฐ๋ณธ Package.swift ํ์ผ์ ์ด์ด๋ณด๋ฉด ์ด๋ฐ ์ฝ๋๊ฐ ์์ ๊ฑฐ์ผ:
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyNetworkKit",
platforms: [
.iOS(.v15),
.macOS(.v12)
],
products: [
.library(
name: "MyNetworkKit",
targets: ["MyNetworkKit"]
),
],
dependencies: [
// ์ธ๋ถ ์์กด์ฑ์ ์ฌ๊ธฐ์ ์ถ๊ฐ
],
targets: [
.target(
name: "MyNetworkKit",
dependencies: []
),
.testTarget(
name: "MyNetworkKitTests",
dependencies: ["MyNetworkKit"]
),
]
)
๐ ๊ฐ ํญ๋ชฉ ์ค๋ช
name: ํจํค์ง์ ์ด๋ฆ
platforms: ์ง์ํ๋ ํ๋ซํผ๊ณผ ์ต์ ๋ฒ์
products: ์ด ํจํค์ง๊ฐ ์ ๊ณตํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์คํ ํ์ผ
dependencies: ๋ค๋ฅธ ํจํค์ง์ ๋ํ ์์กด์ฑ
targets: ์ค์ ์ฝ๋๊ฐ ์๋ ํ๊ฒ๋ค
๐จ ์ค์ ์์ : ๋คํธ์ํฌ ๋ชจ๋ ๋ง๋ค๊ธฐ
์ด๋ก ๋ง ๋ฐฐ์ฐ๋ฉด ์ฌ๋ฏธ์์์? ์ค์ ๋ก ์ฌ์ฉํ ์ ์๋ ๋คํธ์ํฌ ๋ชจ๋์ ๋ง๋ค์ด๋ณด์! ์ด ๋ชจ๋์ API ํธ์ถ์ ์ฝ๊ฒ ๋ง๋ค์ด์ฃผ๋ ์ ํธ๋ฆฌํฐ์ผ. ๐
๐ฏ ๋ชฉํ: ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ NetworkManager ๋ชจ๋
์ด ๋ชจ๋์ ๋ค์ ๊ธฐ๋ฅ์ ์ ๊ณตํ ๊ฑฐ์ผ:
- HTTP ์์ฒญ ์ฒ๋ฆฌ
- JSON ํ์ฑ
- ์๋ฌ ํธ๋ค๋ง
- async/await ์ง์
๋จผ์ Sources/MyNetworkKit/NetworkManager.swift ํ์ผ์ ๋ง๋ค๊ณ ๋ค์ ์ฝ๋๋ฅผ ์์ฑํด๋ด:
import Foundation
public enum NetworkError: Error {
case invalidURL
case noData
case decodingError
case serverError(statusCode: Int)
}
public class NetworkManager {
public static let shared = NetworkManager()
private init() {}
public func fetch<T: Decodable>(
from urlString: String,
type: T.Type
) async throws -> T {
// URL ์ ํจ์ฑ ๊ฒ์ฌ
guard let url = URL(string: urlString) else {
throw NetworkError.invalidURL
}
// ๋คํธ์ํฌ ์์ฒญ
let (data, response) = try await URLSession.shared.data(from: url)
// ์๋ต ์ํ ์ฝ๋ ํ์ธ
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
throw NetworkError.serverError(statusCode: statusCode)
}
// JSON ๋์ฝ๋ฉ
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
throw NetworkError.decodingError
}
}
public func post<T: Encodable, R: Decodable>(
to urlString: String,
body: T,
responseType: R.Type
) async throws -> R {
guard let url = URL(string: urlString) else {
throw NetworkError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let encoder = JSONEncoder()
request.httpBody = try encoder.encode(body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
throw NetworkError.serverError(statusCode: statusCode)
}
let decoder = JSONDecoder()
return try decoder.decode(R.self, from: data)
}
}
โจ ์ฝ๋ ์ค๋ช
์ด ์ฝ๋๋ ์ ๋ค๋ฆญ(Generic)์ ํ์ฉํด์ ์ด๋ค ํ์
์ ๋ฐ์ดํฐ๋ ์ฒ๋ฆฌํ ์ ์์ด. Decodable ํ๋กํ ์ฝ์ ์ค์ํ๋ ๋ชจ๋ ํ์
์ ์๋์ผ๋ก ํ์ฑํด์ฃผ์ง!
public ํค์๋๋ฅผ ์ฌ์ฉํ ๊ฒ๋ ์ฃผ๋ชฉํด๋ด. ๋ชจ๋ ์ธ๋ถ์์ ์ ๊ทผํ ์ ์๊ฒ ํ๋ ค๋ฉด ๋ฐ๋์ public์ ๋ถ์ฌ์ผ ํด! ๐
์ด์ ์ด ๋ชจ๋์ ์ฌ์ฉํ๋ ์์ ๋ฅผ ๋ณผ๊น?
// ์ฌ์ฉ ์์
import MyNetworkKit
struct User: Codable {
let id: Int
let name: String
let email: String
}
// ์ฌ์ฉํ๊ธฐ
Task {
do {
let users = try await NetworkManager.shared.fetch(
from: "https://api.example.com/users",
type: [User].self
)
print("๋ฐ์์จ ์ฌ์ฉ์: \(users.count)๋ช
")
} catch {
print("์๋ฌ ๋ฐ์: \(error)")
}
}
๐ก ํ๋ก ํ
์ค์ ํ๋ก์ ํธ์์๋ ์ด๋ฐ ๋คํธ์ํฌ ๋ชจ๋์ ๋ก๊น ๊ธฐ๋ฅ, ์ฌ์๋ ๋ก์ง, ์บ์ฑ ๋ฑ์ ์ถ๊ฐํ๋ฉด ๋์ฑ ๊ฐ๋ ฅํด์ ธ. ํ์ง๋ง ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ ์ด๋ ๊ฒ ์์ํ๋ ๊ฑฐ์ผ! ๐
๐ ์์กด์ฑ ๊ด๋ฆฌํ๊ธฐ
์ค์ ํ๋ก์ ํธ์์๋ ๋ค๋ฅธ ํจํค์ง๋ฅผ ์ฌ์ฉํด์ผ ํ ๋๊ฐ ๋ง์. ์๋ฅผ ๋ค์ด, ์ด๋ฏธ์ง ์ฒ๋ฆฌ๋ฅผ ์ํด Kingfisher๋ฅผ ์ฐ๊ฑฐ๋, ๋คํธ์ํฌ ์์ฒญ์ Alamofire๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ฒ๋ผ ๋ง์ด์ผ. SPM์์ ์์กด์ฑ์ ์ถ๊ฐํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐ฆ
๋ฐฉ๋ฒ 1: Package.swift์ ์ง์ ์ถ๊ฐ
Package.swift ํ์ผ์ dependencies ์น์ ์ ์ถ๊ฐํ๋ฉด ๋ผ:
let package = Package(
name: "MyNetworkKit",
platforms: [
.iOS(.v15)
],
products: [
.library(
name: "MyNetworkKit",
targets: ["MyNetworkKit"]
),
],
dependencies: [
// Alamofire ์ถ๊ฐ
.package(
url: "https://github.com/Alamofire/Alamofire.git",
from: "5.8.0"
),
// ํน์ ๋ฒ์ ๋ฒ์ ์ง์
.package(
url: "https://github.com/onevcat/Kingfisher.git",
.upToNextMajor(from: "7.0.0")
),
// ์ ํํ ๋ฒ์ ์ง์
.package(
url: "https://github.com/SwiftyJSON/SwiftyJSON.git",
exact: "5.0.1"
)
],
targets: [
.target(
name: "MyNetworkKit",
dependencies: [
"Alamofire",
"Kingfisher"
]
),
.testTarget(
name: "MyNetworkKitTests",
dependencies: ["MyNetworkKit"]
),
]
)
๐ ๋ฒ์ ์ง์ ๋ฐฉ์
from: "5.8.0" - 5.8.0 ์ด์์ ๋ฒ์ (6.0.0 ๋ฏธ๋ง๊น์ง)
.upToNextMajor(from: "7.0.0") - 7.0.0 ์ด์ 8.0.0 ๋ฏธ๋ง
.upToNextMinor(from: "5.2.0") - 5.2.0 ์ด์ 5.3.0 ๋ฏธ๋ง
exact: "5.0.1" - ์ ํํ 5.0.1 ๋ฒ์ ๋ง
.branch("develop") - ํน์ ๋ธ๋์น ์ฌ์ฉ
.revision("abc123") - ํน์ ์ปค๋ฐ ์ฌ์ฉ
๋ฐฉ๋ฒ 2: Xcode UI๋ก ์ถ๊ฐ (๋ ์ฌ์!)
1๏ธโฃ Xcode์์ ํ๋ก์ ํธ ํ์ผ ์ ํ
2๏ธโฃ Package Dependencies ํญ ํด๋ฆญ
3๏ธโฃ + ๋ฒํผ ํด๋ฆญ
4๏ธโฃ GitHub URL ์
๋ ฅ
5๏ธโฃ ๋ฒ์ ์ ํ ํ Add Package
์ด ๋ฐฉ๋ฒ์ด ํจ์ฌ ์ง๊ด์ ์ด๊ณ ์ค์ํ ๊ฐ๋ฅ์ฑ๋ ์ ์ด! ๐
โ ๏ธ ์ฃผ์์ฌํญ
์์กด์ฑ์ ์ถ๊ฐํ ๋๋ ๋ผ์ด์ ์ค๋ฅผ ๊ผญ ํ์ธํด์ผ ํด! ํนํ ์์ ์ฉ ํ๋ก์ ํธ์์๋ GPL ๊ฐ์ ๋ผ์ด์ ์ค๋ ๋ฌธ์ ๊ฐ ๋ ์ ์์ด. MIT, Apache 2.0 ๊ฐ์ ํ์ฉ์ ์ธ ๋ผ์ด์ ์ค๋ฅผ ์ ํธํ๋ ๊ฒ ์ข์. ๐
๐๏ธ ๋ชจ๋ ๊ตฌ์กฐ ์ค๊ณ ์ ๋ต
๋ชจ๋ํ๋ฅผ ํ ๋ ๊ฐ์ฅ ์ค์ํ ๊ฑด ์ด๋ป๊ฒ ๋๋ ๊ฒ์ธ๊ฐ์ผ. ๋ฌด์์ ๋๋๋ฉด ์คํ๋ ค ๋ณต์กํด์ง ์ ์๊ฑฐ๋ . ์ค์ ์์ ๊ฒ์ฆ๋ ๋ชจ๋ ๊ตฌ์กฐ ์ค๊ณ ์ ๋ต์ ์๋ ค์ค๊ฒ! ๐ฏ
๐จ ๋ ์ด์ด๋ณ ๋ถ๋ฆฌ ์ ๋ต (Layered Architecture)
์ ํ๋ฆฌ์ผ์ด์ ์ ๊ณ์ธต์ผ๋ก ๋๋๋ ๋ฐฉ์์ด์ผ. ๊ฐ ๊ณ์ธต์ ๋ช ํํ ์ฑ ์์ ๊ฐ์ง๊ณ ์์ด:
1๏ธโฃ Presentation Layer (ํ๋ ์ ํ ์ด์ ๊ณ์ธต)
UIModule - UI ์ปดํฌ๋ํธ, ๋ทฐ, ๋ทฐ๋ชจ๋ธ
DesignSystemModule - ์์, ํฐํธ, ์คํ์ผ ๊ฐ์ด๋
ResourcesModule - ์ด๋ฏธ์ง, ์์ด์ฝ, ๋ก์ปฌ๋ผ์ด์ ์ด์
์ด ๊ณ์ธต์ ์ฌ์ฉ์๊ฐ ์ง์ ๋ณด๊ณ ์ํธ์์ฉํ๋ ๋ถ๋ถ์ด์ผ. ๋์์ธ์ด ์์ฃผ ๋ฐ๋๋ ๋ถ๋ถ์ด๋ผ ๋
๋ฆฝ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ๊ฒ ์ข์! ๐จ
2๏ธโฃ Domain Layer (๋๋ฉ์ธ ๊ณ์ธต)
CoreModule - ๋น์ฆ๋์ค ๋ก์ง, ์ํฐํฐ, ์ ์ค์ผ์ด์ค
ModelsModule - ๋ฐ์ดํฐ ๋ชจ๋ธ ์ ์
InterfacesModule - ํ๋กํ ์ฝ ์ ์
์ฑ์ ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง์ด ๋ค์ด๊ฐ๋ ๊ณณ์ด์ผ. UI๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋
๋ฆฝ์ ์ด์ด์ผ ํด! ๐ผ
3๏ธโฃ Data Layer (๋ฐ์ดํฐ ๊ณ์ธต)
NetworkModule - API ํต์ , HTTP ํด๋ผ์ด์ธํธ
DatabaseModule - ๋ก์ปฌ ์ ์ฅ์ (CoreData, Realm)
CacheModule - ์บ์ฑ ๋ก์ง
๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ณ ์ ์ฅํ๋ ๋ชจ๋ ์์
์ ๋ด๋นํด. ์ธ๋ถ API๊ฐ ๋ฐ๋์ด๋ ์ด ๊ณ์ธต๋ง ์์ ํ๋ฉด ๋ผ! ๐พ
4๏ธโฃ Common Layer (๊ณตํต ๊ณ์ธต)
UtilsModule - ์ ํธ๋ฆฌํฐ ํจ์, ์ต์คํ
์
LoggerModule - ๋ก๊น
์์คํ
AnalyticsModule - ๋ถ์ ๋๊ตฌ
๋ชจ๋ ๊ณ์ธต์์ ๊ณตํต์ผ๋ก ์ฌ์ฉํ๋ ๊ธฐ๋ฅ๋ค์ด์ผ. ๐ ๏ธ
โจ ์ค์ ์์ : ์ผํ๋ชฐ ์ฑ ๋ชจ๋ ๊ตฌ์กฐ
์ค์ ์ผํ๋ชฐ ์ฑ์ ๋ง๋ ๋ค๋ฉด ์ด๋ ๊ฒ ๋๋ ์ ์์ด:
ShoppingApp (๋ฉ์ธ ์ฑ)
โโโ UIKit (๊ณตํต UI ์ปดํฌ๋ํธ)
โโโ DesignSystem (์์, ํฐํธ, ์คํ์ผ)
โโโ ProductModule (์ํ ๊ด๋ จ ๊ธฐ๋ฅ)
โโโ CartModule (์ฅ๋ฐ๊ตฌ๋ ๊ธฐ๋ฅ)
โโโ PaymentModule (๊ฒฐ์ ๊ธฐ๋ฅ)
โโโ UserModule (์ฌ์ฉ์ ๊ด๋ฆฌ)
โโโ NetworkKit (๋คํธ์ํฌ ํต์ )
โโโ CoreUtils (๊ณตํต ์ ํธ๋ฆฌํฐ)
๐ก ๋ชจ๋ ํฌ๊ธฐ ๊ฐ์ด๋๋ผ์ธ
๋ชจ๋์ด ๋๋ฌด ์์ผ๋ฉด ๊ด๋ฆฌ๊ฐ ๋ณต์กํด์ง๊ณ , ๋๋ฌด ํฌ๋ฉด ๋ชจ๋ํ์ ์๋ฏธ๊ฐ ์์ด์ ธ. ๊ฒฝํ์ ํ ๋ชจ๋๋น 10~50๊ฐ ํ์ผ ์ ๋๊ฐ ์ ๋นํด. ๊ทธ๋ฆฌ๊ณ ๋น๋ ์๊ฐ์ด 5์ด ์ด์ ๊ฑธ๋ฆฌ๋ฉด ๋ ์๊ฒ ๋๋๋ ๊ฑธ ๊ณ ๋ คํด๋ด! โฑ๏ธ
๐ ์ ๊ทผ ์ ์ด์ ์บก์ํ
๋ชจ๋์ ๋ง๋ค ๋ ์ ๊ทผ ์ ์ด(Access Control)๋ ์ ๋ง ์ค์ํด! ์ด๋ค ์ฝ๋๋ฅผ ์ธ๋ถ์ ๊ณต๊ฐํ๊ณ , ์ด๋ค ์ฝ๋๋ฅผ ์จ๊ธธ์ง ๊ฒฐ์ ํ๋ ๊ฑฐ๊ฑฐ๋ . ์ด๊ฒ ๋ฐ๋ก ์บก์ํ์ ํต์ฌ์ด์ผ! ๐
Swift์ ์ ๊ทผ ์ ์ด ๋ ๋ฒจ
Swift๋ 5๊ฐ์ง ์ ๊ทผ ์ ์ด ๋ ๋ฒจ์ ์ ๊ณตํด:
1. open - ๊ฐ์ฅ ๊ฐ๋ฐฉ์
๋ค๋ฅธ ๋ชจ๋์์ ์ ๊ทผ ๊ฐ๋ฅํ๊ณ , ์์๊ณผ ์ค๋ฒ๋ผ์ด๋๋ ๊ฐ๋ฅํด.
์ฃผ๋ก ํ๋ ์์ํฌ๋ฅผ ๋ง๋ค ๋ ์ฌ์ฉํด.
open class BaseViewController: UIViewController { }
2. public
๋ค๋ฅธ ๋ชจ๋์์ ์ ๊ทผ ๊ฐ๋ฅํ์ง๋ง, ์์๊ณผ ์ค๋ฒ๋ผ์ด๋๋ ๋ถ๊ฐ๋ฅํด.
๋ชจ๋์ ๊ณต๊ฐ API๋ฅผ ์ ์ํ ๋ ์ฌ์ฉํด.
public struct User { }
public func fetchData() { }
3. internal - ๊ธฐ๋ณธ๊ฐ
๊ฐ์ ๋ชจ๋ ๋ด์์๋ง ์ ๊ทผ ๊ฐ๋ฅํด.
์๋ฌด๊ฒ๋ ๋ช
์ํ์ง ์์ผ๋ฉด internal์ด ๊ธฐ๋ณธ์ด์ผ.
struct InternalModel { }
4. fileprivate
๊ฐ์ ํ์ผ ๋ด์์๋ง ์ ๊ทผ ๊ฐ๋ฅํด.
ํ์ผ ๋จ์๋ก ์ฝ๋๋ฅผ ์จ๊ธฐ๊ณ ์ถ์ ๋ ์ฌ์ฉํด.
fileprivate var cache: [String: Any] = [:]
5. private - ๊ฐ์ฅ ์ ํ์
๊ฐ์ ์ ์ธ ๋ฒ์ ๋ด์์๋ง ์ ๊ทผ ๊ฐ๋ฅํด.
ํด๋์ค๋ ๊ตฌ์กฐ์ฒด์ ๋ด๋ถ ๊ตฌํ์ ์์ ํ ์จ๊ธธ ๋ ์ฌ์ฉํด.
private func validateInput() { }
์ค์ ์์ ๋ฅผ ๋ณผ๊น? ๋คํธ์ํฌ ๋ชจ๋์์ ์ ๊ทผ ์ ์ด๋ฅผ ์ด๋ป๊ฒ ํ์ฉํ๋์ง ๋ณด์ฌ์ค๊ฒ:
// NetworkKit ๋ชจ๋
// ๊ณต๊ฐ API - ์ธ๋ถ์์ ์ฌ์ฉ ๊ฐ๋ฅ
public protocol NetworkServiceProtocol {
func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
}
// ๊ณต๊ฐ ๊ตฌํ์ฒด
public class NetworkService: NetworkServiceProtocol {
// ์ฑ๊ธํค ์ธ์คํด์ค - ์ธ๋ถ์์ ์ ๊ทผ ๊ฐ๋ฅ
public static let shared = NetworkService()
// ๋ด๋ถ ๊ตฌํ - ์ธ๋ถ์์ ์ ๊ทผ ๋ถ๊ฐ
private let session: URLSession
private let decoder: JSONDecoder
// private ์ด๊ธฐํ - ์ฑ๊ธํค ํจํด ๊ฐ์
private init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
self.session = URLSession(configuration: config)
self.decoder = JSONDecoder()
}
// ๊ณต๊ฐ ๋ฉ์๋
public func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
let request = try buildRequest(from: endpoint)
let (data, response) = try await session.data(for: request)
try validateResponse(response)
return try decode(data)
}
// ๋ด๋ถ ํฌํผ ๋ฉ์๋๋ค - ์ธ๋ถ์์ ์ ๊ทผ ๋ถ๊ฐ
private func buildRequest(from endpoint: Endpoint) throws -> URLRequest {
guard let url = URL(string: endpoint.baseURL + endpoint.path) else {
throw NetworkError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = endpoint.method.rawValue
return request
}
private func validateResponse(_ response: URLResponse) throws {
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.serverError
}
}
private func decode<T: Decodable>(_ data: Data) throws -> T {
try decoder.decode(T.self, from: data)
}
}
// ๋ด๋ถ์์๋ง ์ฌ์ฉํ๋ ์๋ฌ ํ์
internal enum NetworkError: Error {
case invalidURL
case serverError
case decodingError
}
๐ฏ ์ ๊ทผ ์ ์ด ๋ฒ ์คํธ ํ๋ํฐ์ค
1. ์ต์ ๊ถํ ์์น: ๊ธฐ๋ณธ์ ์ผ๋ก private์ผ๋ก ์์ํ๊ณ , ํ์ํ ๋๋ง public์ผ๋ก ๋ณ๊ฒฝํด
2. ๋ช
ํํ API ๊ฒฝ๊ณ: public์ผ๋ก ๊ณต๊ฐํ๋ ๊ฒ๋ค์ ์ ๋ฌธ์ํํ๊ณ ์์ ์ ์ผ๋ก ์ ์งํด
3. ๋ด๋ถ ๊ตฌํ ์จ๊ธฐ๊ธฐ: ๊ตฌํ ์ธ๋ถ์ฌํญ์ private์ผ๋ก ์จ๊ฒจ์ ๋ณ๊ฒฝ์ ์์ ๋ฅผ ํ๋ณดํด
4. ํ๋กํ ์ฝ ํ์ฉ: public ํ๋กํ ์ฝ๋ก ์ธํฐํ์ด์ค๋ฅผ ์ ์ํ๊ณ , ๊ตฌํ์ฒด๋ internal๋ก ์ ์งํ ์ ์์ด
๐งช ํ ์คํธ ๊ฐ๋ฅํ ๋ชจ๋ ๋ง๋ค๊ธฐ
๋ชจ๋ํ์ ํฐ ์ฅ์ ์ค ํ๋๊ฐ ๋ฐ๋ก ํ ์คํธํ๊ธฐ ์ฝ๋ค๋ ๊ฑฐ์ผ! ๊ฐ ๋ชจ๋์ ๋ ๋ฆฝ์ ์ผ๋ก ํ ์คํธํ ์ ์์ด์ ๋ฒ๊ทธ๋ฅผ ๋นจ๋ฆฌ ์ฐพ๊ณ ์์ ํ ์ ์์ง. ๐
ํ ์คํธ ๊ฐ๋ฅํ ์ฝ๋์ ํน์ง
โ
์์กด์ฑ์ด ๋ช
ํํ๊ฒ ์ ์๋์ด ์์
โ
์์กด์ฑ ์ฃผ์
(Dependency Injection)์ ์ฌ์ฉํจ
โ
ํ๋กํ ์ฝ ๊ธฐ๋ฐ์ผ๋ก ์ค๊ณ๋์ด ์์
โ
๋ถ์์ฉ(Side Effect)์ด ์ต์ํ๋์ด ์์
โ
๋จ์ผ ์ฑ
์ ์์น์ ๋ฐ๋ฆ
์ค์ ์์ ๋ฅผ ๋ณด๋ฉด์ ํ ์คํธ ๊ฐ๋ฅํ ๋ชจ๋์ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์์๋ณด์:
// UserService.swift - ํ๋กํ ์ฝ ์ ์
public protocol UserServiceProtocol {
func fetchUser(id: String) async throws -> User
func updateUser(_ user: User) async throws
}
// ์ค์ ๊ตฌํ
public class UserService: UserServiceProtocol {
private let networkService: NetworkServiceProtocol
private let cacheService: CacheServiceProtocol
// ์์กด์ฑ ์ฃผ์
์ ํตํ ์ด๊ธฐํ
public init(
networkService: NetworkServiceProtocol,
cacheService: CacheServiceProtocol
) {
self.networkService = networkService
self.cacheService = cacheService
}
public func fetchUser(id: String) async throws -> User {
// ์บ์ ํ์ธ
if let cachedUser = cacheService.get(key: "user_\(id)") as? User {
return cachedUser
}
// ๋คํธ์ํฌ ์์ฒญ
let endpoint = UserEndpoint.getUser(id: id)
let user: User = try await networkService.request(endpoint)
// ์บ์ ์ ์ฅ
cacheService.set(user, forKey: "user_\(id)")
return user
}
public func updateUser(_ user: User) async throws {
let endpoint = UserEndpoint.updateUser(user)
let _: User = try await networkService.request(endpoint)
// ์บ์ ์
๋ฐ์ดํธ
cacheService.set(user, forKey: "user_\(user.id)")
}
}
์ด์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด๋ณด์:
// UserServiceTests.swift
import XCTest
@testable import UserModule
class UserServiceTests: XCTestCase {
var sut: UserService!
var mockNetworkService: MockNetworkService!
var mockCacheService: MockCacheService!
override func setUp() {
super.setUp()
mockNetworkService = MockNetworkService()
mockCacheService = MockCacheService()
sut = UserService(
networkService: mockNetworkService,
cacheService: mockCacheService
)
}
override func tearDown() {
sut = nil
mockNetworkService = nil
mockCacheService = nil
super.tearDown()
}
func test_fetchUser_์บ์์_์์๋_๋คํธ์ํฌ_ํธ์ถ_์ํจ() async throws {
// Given
let expectedUser = User(id: "123", name: "ํ
์คํธ")
mockCacheService.mockData["user_123"] = expectedUser
// When
let user = try await sut.fetchUser(id: "123")
// Then
XCTAssertEqual(user.id, expectedUser.id)
XCTAssertEqual(user.name, expectedUser.name)
XCTAssertFalse(mockNetworkService.requestCalled)
}
func test_fetchUser_์บ์์_์์๋_๋คํธ์ํฌ_ํธ์ถํจ() async throws {
// Given
let expectedUser = User(id: "456", name: "์๋ก์ด์ ์ ")
mockNetworkService.mockResponse = expectedUser
// When
let user = try await sut.fetchUser(id: "456")
// Then
XCTAssertEqual(user.id, expectedUser.id)
XCTAssertTrue(mockNetworkService.requestCalled)
XCTAssertTrue(mockCacheService.setCalled)
}
func test_updateUser_์ฑ๊ณต์_์บ์_์
๋ฐ์ดํธ๋จ() async throws {
// Given
let user = User(id: "789", name: "์
๋ฐ์ดํธ์ ์ ")
mockNetworkService.mockResponse = user
// When
try await sut.updateUser(user)
// Then
XCTAssertTrue(mockCacheService.setCalled)
let cachedUser = mockCacheService.mockData["user_789"] as? User
XCTAssertEqual(cachedUser?.name, user.name)
}
}
// Mock ๊ฐ์ฒด๋ค
class MockNetworkService: NetworkServiceProtocol {
var requestCalled = false
var mockResponse: Any?
var mockError: Error?
func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
requestCalled = true
if let error = mockError {
throw error
}
guard let response = mockResponse as? T else {
throw NetworkError.decodingError
}
return response
}
}
class MockCacheService: CacheServiceProtocol {
var mockData: [String: Any] = [:]
var setCalled = false
func get(key: String) -> Any? {
return mockData[key]
}
func set(_ value: Any, forKey key: String) {
setCalled = true
mockData[key] = value
}
}
โจ ํ ์คํธ ์ฝ๋์ ์ฅ์
์ด๋ ๊ฒ ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ฉด:
1. ์ฝ๋ ๋ณ๊ฒฝ ์ ๊ธฐ์กด ๊ธฐ๋ฅ์ด ๊นจ์ง๋์ง ์ฆ์ ํ์ธ ๊ฐ๋ฅ ๐
2. ๋ฆฌํฉํ ๋งํ ๋ ์์ ๊ฐ์ ๊ฐ์ง ์ ์์ ๐ช
3. ๋ฒ๊ทธ๋ฅผ ํ๋ก๋์
๋ฐฐํฌ ์ ์ ๋ฐ๊ฒฌ ๊ฐ๋ฅ ๐
4. ์ฝ๋์ ๋ฌธ์ ์ญํ ๋ ํจ ๐
5. ํ์๋ค๊ณผ ํ์
ํ ๋ ์ ๋ขฐ๋ ํฅ์ ๐ค
๐ก ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง ๋ชฉํ
์ค๋ฌด์์๋ ๋ณดํต 70~80% ์ด์์ ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ๋ชฉํ๋ก ํด. 100%๋ฅผ ๋ชฉํ๋ก ํ๋ฉด ์คํ๋ ค ๋นํจ์จ์ ์ผ ์ ์์ด. ์ค์ํ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋ณต์กํ ์๊ณ ๋ฆฌ์ฆ์ ์ง์คํ๋ ๊ฒ ์ข์! ๐ฏ
โก ๋น๋ ์๊ฐ ์ต์ ํํ๊ธฐ
๋ชจ๋ํ์ ๊ฐ์ฅ ํฐ ์ค์ง์ ์ด์ ์ค ํ๋๊ฐ ๋ฐ๋ก ๋น๋ ์๊ฐ ๋จ์ถ์ด์ผ! ํฐ ํ๋ก์ ํธ์์ ์ ์ฒด ๋น๋์ 5๋ถ์ฉ ๊ฑธ๋ฆฐ๋ค๋ฉด ์ ๋ง ๋ต๋ตํ์์? ๋ชจ๋ํ๋ก ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ด! โก
๐ ๋น๋ ์๊ฐ์ด ๋จ์ถ๋๋ ์๋ฆฌ
1. ์ฆ๋ถ ๋น๋(Incremental Build): ๋ณ๊ฒฝ๋ ๋ชจ๋๋ง ๋ค์ ์ปดํ์ผ
2. ๋ณ๋ ฌ ๋น๋: ๋
๋ฆฝ์ ์ธ ๋ชจ๋๋ค์ ๋์์ ๋น๋
3. ์บ์ฑ: ๋ณ๊ฒฝ๋์ง ์์ ๋ชจ๋์ ์บ์ ์ฌ์ฉ
4. ์์กด์ฑ ์ต์ํ: ๋ถํ์ํ ์์กด์ฑ ์ ๊ฑฐ๋ก ๋น๋ ๋ฒ์ ์ถ์
์ค์ ์ฌ๋ก: ๋๊ท๋ชจ ํ๋ก์ ํธ ๋น๋ ์๊ฐ ๋น๊ต
๋ชจ๋ํ ์
- ์ ์ฒด ๋น๋: 8๋ถ 30์ด โฑ๏ธ
- ์์ ์์ ํ ๋น๋: 6๋ถ 20์ด
- ๊ฐ๋ฐ์ ์์ฐ์ฑ: ๋ฎ์ ๐ซ
๋ชจ๋ํ ํ (15๊ฐ ๋ชจ๋๋ก ๋ถ๋ฆฌ)
- ์ ์ฒด ๋น๋: 4๋ถ 10์ด โก
- ์์ ์์ ํ ๋น๋: 45์ด ๐
- ๊ฐ๋ฐ์ ์์ฐ์ฑ: ๋์ ๐
์์ ํ ๋น๋ ์๊ฐ์ด 88% ๋จ์ถ๋์์ด! ํ๋ฃจ์ 20๋ฒ ๋น๋ํ๋ค๋ฉด ์ฝ 2์๊ฐ์ ์ ์ฝํ๋ ์
์ด์ง!
๐ก ๋น๋ ์๊ฐ ์ต์ ํ ํ
1. ๋ชจ๋ ์์กด์ฑ ๊ทธ๋ํ ์ต์ ํ
์ํ ์์กด์ฑ์ ์ ๊ฑฐํ๊ณ , ์์กด์ฑ ๊น์ด๋ฅผ ์ต์ํํด. ์์กด์ฑ ๊ทธ๋ํ๊ฐ ํํํ ์๋ก ๋ณ๋ ฌ ๋น๋๊ฐ ํจ์จ์ ์ด์ผ!
2. ํ๋ฆฌ์ปดํ์ผ๋ ํค๋ ์ฌ์ฉ
์์ฃผ ์ฌ์ฉํ๋ ์์คํ
ํ๋ ์์ํฌ๋ ํ๋ฆฌ์ปดํ์ผํด์ ๋น๋ ์๊ฐ์ ์ค์ฌ.
3. ๋น๋ ์ค์ ์ต์ ํ
Debug ๋น๋์์๋ ์ต์ ํ ๋ ๋ฒจ์ ๋ฎ์ถ๊ณ , ๋๋ฒ๊ทธ ์ ๋ณด ์์ฑ์ ์ต์ํํด.
4. ์ ๋ค๋ฆญ ์ฌ์ฉ ์ต์ํ
๊ณผ๋ํ ์ ๋ค๋ฆญ ์ฌ์ฉ์ ์ปดํ์ผ ์๊ฐ์ ๋๋ ค. ํ์ํ ๊ณณ์๋ง ์ฌ์ฉํด!
// Package.swift์์ ๋น๋ ์ต์ ํ ์ค์
let package = Package(
name: "MyApp",
platforms: [
.iOS(.v15)
],
products: [
.library(name: "MyApp", targets: ["MyApp"]),
],
dependencies: [
// ์์กด์ฑ ์ต์ํ
],
targets: [
.target(
name: "MyApp",
dependencies: [],
swiftSettings: [
// Debug ๋น๋ ์ต์ ํ
.unsafeFlags(["-Onone"], .when(configuration: .debug)),
// Release ๋น๋ ์ต์ ํ
.unsafeFlags(["-O"], .when(configuration: .release)),
// ๋ณ๋ ฌ ์ปดํ์ผ ํ์ฑํ
.unsafeFlags(["-j8"])
]
),
]
)
โ ๏ธ ๊ณผ๋ํ ๋ชจ๋ํ ์ฃผ์
๋ชจ๋์ ๋๋ฌด ๋ง์ด ๋๋๋ฉด ์คํ๋ ค ์ญํจ๊ณผ๊ฐ ๋ ์ ์์ด! ๋ชจ๋ ๊ฐ ํต์ ์ค๋ฒํค๋๊ฐ ์ฆ๊ฐํ๊ณ , ๊ด๋ฆฌ ๋ณต์ก๋๊ฐ ์ฌ๋ผ๊ฐ๊ฑฐ๋ . ์ ์ ํ ๊ท ํ์ ์ฐพ๋ ๊ฒ ์ค์ํด. ๋ณดํต 10~30๊ฐ ์ ๋์ ๋ชจ๋์ด ์ ๋นํด! ๐ฏ
๐ ๋ฒ์ ๊ด๋ฆฌ์ ๋ฐฐํฌ ์ ๋ต
๋ชจ๋์ ๋ง๋ค์์ผ๋ฉด ์ด์ ๋ฒ์ ๊ด๋ฆฌ๋ฅผ ํด์ผ ํด! ํนํ ์ฌ๋ฌ ํ๋ก์ ํธ์์ ์ฌ์ฉํ๋ ๊ณตํต ๋ชจ๋์ด๋ผ๋ฉด ๋ฒ์ ๊ด๋ฆฌ๊ฐ ์ ๋ง ์ค์ํ์ง. Semantic Versioning์ ์ฌ์ฉํ๋ฉด ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์์ด! ๐ฆ
๐ Semantic Versioning (์ ์์ ๋ฒ์ )
๋ฒ์ ๋ฒํธ๋ MAJOR.MINOR.PATCH ํ์์ ๋ฐ๋ผ:
1.2.3
- MAJOR (1): ํ์ ํธํ์ฑ์ด ๊นจ์ง๋ ๋ณ๊ฒฝ
- MINOR (2): ํ์ ํธํ์ฑ์ ์ ์งํ๋ ๊ธฐ๋ฅ ์ถ๊ฐ
- PATCH (3): ํ์ ํธํ์ฑ์ ์ ์งํ๋ ๋ฒ๊ทธ ์์
์ค์ ์์ : ๋ฒ์ ์ ๋ฐ์ดํธ ์๋๋ฆฌ์ค
v1.0.0 - ์ต์ด ๋ฆด๋ฆฌ์ค
โโ ๊ธฐ๋ณธ ๋คํธ์ํฌ ๊ธฐ๋ฅ ์ ๊ณต
v1.0.1 - ํจ์น ์
๋ฐ์ดํธ
โโ ํ์์์ ๋ฒ๊ทธ ์์
โโ ๋ฉ๋ชจ๋ฆฌ ๋์ ํด๊ฒฐ
v1.1.0 - ๋ง์ด๋ ์
๋ฐ์ดํธ
โโ ์บ์ฑ ๊ธฐ๋ฅ ์ถ๊ฐ
โโ ์ฌ์๋ ๋ก์ง ์ถ๊ฐ
โโ ๊ธฐ์กด API๋ ๊ทธ๋๋ก ์ ์ง
v2.0.0 - ๋ฉ์ด์ ์
๋ฐ์ดํธ
โโ async/await๋ก ์ ๋ฉด ์ฌ์์ฑ
โโ ๊ธฐ์กด ์ฝ๋ฐฑ ๊ธฐ๋ฐ API ์ ๊ฑฐ
โโ ๋ง์ด๊ทธ๋ ์ด์
๊ฐ์ด๋ ์ ๊ณต ํ์
Git ํ๊ทธ๋ฅผ ์ฌ์ฉํด์ ๋ฒ์ ์ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ:
# ์ ๋ฒ์ ํ๊ทธ ์์ฑ
git tag -a v1.2.0 -m "๋ฒ์ 1.2.0 ๋ฆด๋ฆฌ์ค: ์บ์ฑ ๊ธฐ๋ฅ ์ถ๊ฐ"
# ํ๊ทธ๋ฅผ ์๊ฒฉ ์ ์ฅ์์ ํธ์
git push origin v1.2.0
# ๋ชจ๋ ํ๊ทธ ํธ์
git push origin --tags
# ํ๊ทธ ๋ชฉ๋ก ํ์ธ
git tag -l
# ํน์ ํ๊ทธ๋ก ์ฒดํฌ์์
git checkout v1.2.0
๐ก ๋ฆด๋ฆฌ์ค ๋ ธํธ ์์ฑ ํ
๋ฒ์ ์ ์
๋ฐ์ดํธํ ๋๋ ํญ์ ๋ฆด๋ฆฌ์ค ๋
ธํธ๋ฅผ ์์ฑํด! GitHub Releases ๊ธฐ๋ฅ์ ํ์ฉํ๋ฉด ์ข์:
ํฌํจํ ๋ด์ฉ:
โ
์๋ก์ด ๊ธฐ๋ฅ (New Features)
โ
๊ฐ์ ์ฌํญ (Improvements)
โ
๋ฒ๊ทธ ์์ (Bug Fixes)
โ
์ฃผ์ ๋ณ๊ฒฝ ์ฌํญ (Breaking Changes)
โ
๋ง์ด๊ทธ๋ ์ด์
๊ฐ์ด๋ (Migration Guide)
โ
์๋ ค์ง ์ด์ (Known Issues)
๐ ๋ฐฐํฌ ์๋ํ
GitHub Actions๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฒ์ ๋ฐฐํฌ๋ฅผ ์๋ํํ ์ ์์ด:
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Swift ๋ฒ์ ํ์ธ
run: swift --version
- name: ํ
์คํธ ์คํ
run: swift test
- name: ๋น๋
run: swift build -c release
- name: GitHub Release ์์ฑ
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
โจ ๋ฒ์ ๊ด๋ฆฌ ๋ฒ ์คํธ ํ๋ํฐ์ค
1. ๋ช
ํํ ๋ฒ์ ์ ์ฑ
: ํ ๋ด์์ ๋ฒ์ ์
๋ฐ์ดํธ ๊ธฐ์ค์ ๋ช
ํํ ์ ์
2. ๋ณ๊ฒฝ ๋ก๊ทธ ์ ์ง: CHANGELOG.md ํ์ผ๋ก ๋ชจ๋ ๋ณ๊ฒฝ ์ฌํญ ๊ธฐ๋ก
3. ํ์ ํธํ์ฑ ์ ์ง: ๊ฐ๋ฅํ ํ ๊ธฐ์กด API๋ฅผ ์ ์งํ๊ณ deprecated ์ฒ๋ฆฌ
4. ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง: ๋ฆด๋ฆฌ์ค ์ ์ถฉ๋ถํ ํ
์คํธ ์ํ
5. ๋ฌธ์ํ: API ๋ฌธ์์ ์์ ์ฝ๋ ์ ๊ณต
๐ ์ค์ ํ๋ก์ ํธ: ์์ ํ ๋ชจ๋ ์ํ๊ณ ๊ตฌ์ถํ๊ธฐ
์ด์ ๋ฐฐ์ด ๋ด์ฉ์ ์ข ํฉํด์ ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํด๋ณด์! ์์ ๋ฏธ๋์ด ์ฑ์ ์๋ก ๋ค์ด ์์ ํ ๋ชจ๋ ์ํ๊ณ๋ฅผ ๊ตฌ์ถํ๋ ๊ณผ์ ์ ๋ณด์ฌ์ค๊ฒ. ๐จ
๐ฏ ํ๋ก์ ํธ ๊ตฌ์กฐ ์ค๊ณ
SocialMediaApp (๋ฉ์ธ ์ฑ)
โโโ DesignSystem - ๋์์ธ ์์คํ
โโโ NetworkKit - ๋คํธ์ํฌ ๋ ์ด์ด
โโโ StorageKit - ๋ก์ปฌ ์ ์ฅ์
โโโ AuthModule - ์ธ์ฆ/์ธ๊ฐ
โโโ FeedModule - ํผ๋ ๊ธฐ๋ฅ
โโโ ProfileModule - ํ๋กํ ๊ธฐ๋ฅ
โโโ MessagingModule - ๋ฉ์์ง ๊ธฐ๋ฅ
โโโ CoreUtils - ๊ณตํต ์ ํธ๋ฆฌํฐ
๋จผ์ DesignSystem ๋ชจ๋๋ถํฐ ๋ง๋ค์ด๋ณด์. ์ด๊ฑด ๋ชจ๋ UI ๋ชจ๋์ ๊ธฐ๋ฐ์ด ๋๋ ์ค์ํ ๋ชจ๋์ด์ผ!
// DesignSystem/Sources/DesignSystem/Colors.swift
import UIKit
public enum AppColors {
// Primary Colors
public static let primary = UIColor(hex: "667EEA")
public static let primaryDark = UIColor(hex: "764BA2")
public static let primaryLight = UIColor(hex: "A8B5FF")
// Secondary Colors
public static let secondary = UIColor(hex: "F59E0B")
public static let secondaryDark = UIColor(hex: "D97706")
// Neutral Colors
public static let background = UIColor(hex: "F9FAFB")
public static let surface = UIColor.white
public static let textPrimary = UIColor(hex: "1F2937")
public static let textSecondary = UIColor(hex: "6B7280")
// Semantic Colors
public static let success = UIColor(hex: "10B981")
public static let error = UIColor(hex: "EF4444")
public static let warning = UIColor(hex: "F59E0B")
public static let info = UIColor(hex: "3B82F6")
}
// UIColor ํ์ฅ
extension UIColor {
convenience init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 6: // RGB
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (255, 0, 0, 0)
}
self.init(
red: CGFloat(r) / 255,
green: CGFloat(g) / 255,
blue: CGFloat(b) / 255,
alpha: CGFloat(a) / 255
)
}
}
// DesignSystem/Sources/DesignSystem/Typography.swift
import UIKit
public enum AppFonts {
// Font Weights
public enum Weight {
case regular
case medium
case semibold
case bold
var systemWeight: UIFont.Weight {
switch self {
case .regular: return .regular
case .medium: return .medium
case .semibold: return .semibold
case .bold: return .bold
}
}
}
// Font Sizes
public static func heading1(weight: Weight = .bold) -> UIFont {
return .systemFont(ofSize: 32, weight: weight.systemWeight)
}
public static func heading2(weight: Weight = .bold) -> UIFont {
return .systemFont(ofSize: 24, weight: weight.systemWeight)
}
public static func heading3(weight: Weight = .semibold) -> UIFont {
return .systemFont(ofSize: 20, weight: weight.systemWeight)
}
public static func body(weight: Weight = .regular) -> UIFont {
return .systemFont(ofSize: 16, weight: weight.systemWeight)
}
public static func caption(weight: Weight = .regular) -> UIFont {
return .systemFont(ofSize: 14, weight: weight.systemWeight)
}
public static func small(weight: Weight = .regular) -> UIFont {
return .systemFont(ofSize: 12, weight: weight.systemWeight)
}
}
// DesignSystem/Sources/DesignSystem/Components/PrimaryButton.swift
import UIKit
public class PrimaryButton: UIButton {
public enum Style {
case filled
case outlined
case text
}
private let style: Style
public init(title: String, style: Style = .filled) {
self.style = style
super.init(frame: .zero)
setupButton(title: title)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupButton(title: String) {
setTitle(title, for: .normal)
titleLabel?.font = AppFonts.body(weight: .semibold)
layer.cornerRadius = 12
switch style {
case .filled:
backgroundColor = AppColors.primary
setTitleColor(.white, for: .normal)
case .outlined:
backgroundColor = .clear
setTitleColor(AppColors.primary, for: .normal)
layer.borderWidth = 2
layer.borderColor = AppColors.primary.cgColor
case .text:
backgroundColor = .clear
setTitleColor(AppColors.primary, for: .normal)
}
// ๋์ด ์ ์ฝ
heightAnchor.constraint(equalToConstant: 48).isActive = true
// ํฐ์น ํผ๋๋ฐฑ
addTarget(self, action: #selector(touchDown), for: .touchDown)
addTarget(self, action: #selector(touchUp), for: [.touchUpInside, .touchUpOutside])
}
@objc private func touchDown() {
UIView.animate(withDuration: 0.1) {
self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
self.alpha = 0.8
}
}
@objc private func touchUp() {
UIView.animate(withDuration: 0.1) {
self.transform = .identity
self.alpha = 1.0
}
}
}
โจ DesignSystem ๋ชจ๋์ ์ฅ์
1. ์ผ๊ด๋ ๋์์ธ: ์ฑ ์ ์ฒด์์ ๋์ผํ ์คํ์ผ ์ ์ง
2. ์ฌ์ด ํ
๋ง ๋ณ๊ฒฝ: ํ ๊ณณ๋ง ์์ ํ๋ฉด ์ ์ฒด ์ฑ์ ๋ฐ์
3. ๋์์ด๋์ ํ์
์ฉ์ด: ๋์์ธ ์์คํ
์ ์ฝ๋๋ก ๋ช
ํํ ์ ์
4. ์ฌ์ฌ์ฉ์ฑ: ๋ค๋ฅธ ํ๋ก์ ํธ์์๋ ๋์ผํ ๋์์ธ ์์คํ
์ฌ์ฉ ๊ฐ๋ฅ
์ด์ FeedModule์ ๋ง๋ค์ด๋ณด์. ์ด ๋ชจ๋์ DesignSystem๊ณผ NetworkKit์ ์์กดํด:
// FeedModule/Package.swift
let package = Package(
name: "FeedModule",
platforms: [
.iOS(.v15)
],
products: [
.library(
name: "FeedModule",
targets: ["FeedModule"]
),
],
dependencies: [
.package(path: "../DesignSystem"),
.package(path: "../NetworkKit"),
.package(path: "../CoreUtils")
],
targets: [
.target(
name: "FeedModule",
dependencies: [
"DesignSystem",
"NetworkKit",
"CoreUtils"
]
),
.testTarget(
name: "FeedModuleTests",
dependencies: ["FeedModule"]
),
]
)
// FeedModule/Sources/FeedModule/Models/Post.swift
import Foundation
public struct Post: Codable, Identifiable {
public let id: String
public let authorId: String
public let authorName: String
public let authorAvatar: String?
public let content: String
public let imageUrls: [String]
public let likeCount: Int
public let commentCount: Int
public let createdAt: Date
public var isLiked: Bool
public init(
id: String,
authorId: String,
authorName: String,
authorAvatar: String?,
content: String,
imageUrls: [String],
likeCount: Int,
commentCount: Int,
createdAt: Date,
isLiked: Bool
) {
self.id = id
self.authorId = authorId
self.authorName = authorName
self.authorAvatar = authorAvatar
self.content = content
self.imageUrls = imageUrls
self.likeCount = likeCount
self.commentCount = commentCount
self.createdAt = createdAt
self.isLiked = isLiked
}
}
// FeedModule/Sources/FeedModule/Services/FeedService.swift
import Foundation
import NetworkKit
public protocol FeedServiceProtocol {
func fetchFeed(page: Int) async throws -> [Post]
func likePost(id: String) async throws
func unlikePost(id: String) async throws
}
public class FeedService: FeedServiceProtocol {
private let networkService: NetworkServiceProtocol
public init(networkService: NetworkServiceProtocol) {
self.networkService = networkService
}
public func fetchFeed(page: Int) async throws -> [Post] {
let endpoint = FeedEndpoint.getFeed(page: page)
return try await networkService.request(endpoint)
}
public func likePost(id: String) async throws {
let endpoint = FeedEndpoint.likePost(id: id)
let _: EmptyResponse = try await networkService.request(endpoint)
}
public func unlikePost(id: String) async throws {
let endpoint = FeedEndpoint.unlikePost(id: id)
let _: EmptyResponse = try await networkService.request(endpoint)
}
}
// ๋น ์๋ต์ ์ํ ํ์
struct EmptyResponse: Codable {}
๐ก ๋ชจ๋ ๊ฐ ํต์ ํจํด
๋ชจ๋ ๊ฐ์๋ ํ๋กํ ์ฝ์ ํตํด ํต์ ํ๋ ๊ฒ ์ข์. ์ด๋ ๊ฒ ํ๋ฉด:
- ๋ชจ๋ ๊ฐ ๊ฒฐํฉ๋๊ฐ ๋ฎ์์ง ๐
- ํ
์คํธํ๊ธฐ ์ฌ์์ง ๐งช
- ๊ตฌํ์ฒด๋ฅผ ์ฝ๊ฒ ๊ต์ฒด ๊ฐ๋ฅ ๐
- ์์กด์ฑ ๋ฐฉํฅ์ด ๋ช
ํํด์ง โก๏ธ
๐ ์ค๋ฌด์์์ ๋ชจ๋ํ ์ ๋ต
์ด๋ก ๊ณผ ์์ ๋ฅผ ๋ง์ด ๋ดค์ผ๋, ์ด์ ์ค๋ฌด์์ ์ค์ ๋ก ์ด๋ป๊ฒ ์ ์ฉํ๋์ง ์์๋ณด์! ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ํ๋ก๊ทธ๋๋ฐ ์ฌ๋ฅ์ ์ ๊ณตํ ๋๋ ์ด๋ฐ ์ค๋ฌด ๊ฒฝํ์ด ํฐ ์ฐจ๋ณํ ํฌ์ธํธ๊ฐ ๋ผ. ๐ผ
๐ ๋จ๊ณ๋ณ ๋ชจ๋ํ ๋ง์ด๊ทธ๋ ์ด์
๊ธฐ์กด ๋ชจ๋๋ฆฌ์ ํ๋ก์ ํธ๋ฅผ ๋ชจ๋ํํ๋ ๊ณผ์ :
Phase 1: ๋ถ์ ๋ฐ ๊ณํ (1-2์ฃผ)
โ
ํ์ฌ ์ฝ๋๋ฒ ์ด์ค ๋ถ์
โ
์์กด์ฑ ๊ทธ๋ํ ์์ฑ
โ
๋ชจ๋ ๊ฒฝ๊ณ ์ ์
โ
์ฐ์ ์์ ๊ฒฐ์
โ
ํ ๊ต์ก ๋ฐ ์ปจ๋ฒค์
์๋ฆฝ
ํต์ฌ ์ง๋ฌธ:
- ์ด๋ค ์ฝ๋๊ฐ ์์ฃผ ๋ณ๊ฒฝ๋๋๊ฐ?
- ์ด๋ค ๊ธฐ๋ฅ์ด ๋
๋ฆฝ์ ์ผ๋ก ํ
์คํธ๋์ด์ผ ํ๋๊ฐ?
- ์ด๋ค ๋ถ๋ถ์ ๋ค๋ฅธ ํ๋ก์ ํธ์์ ์ฌ์ฌ์ฉํ ์ ์๋๊ฐ?
Phase 2: ๊ธฐ๋ฐ ๋ชจ๋ ๊ตฌ์ถ (2-3์ฃผ)
1๏ธโฃ CoreUtils - ๊ณตํต ์ ํธ๋ฆฌํฐ ๋ถ๋ฆฌ
2๏ธโฃ DesignSystem - UI ์ปดํฌ๋ํธ ํ์คํ
3๏ธโฃ NetworkKit - ๋คํธ์ํฌ ๋ ์ด์ด ์ถ์ํ
4๏ธโฃ StorageKit - ์ ์ฅ์ ๋ ์ด์ด ๋ถ๋ฆฌ
์ด ๋จ๊ณ์์๋ ๋ค๋ฅธ ๋ชจ๋๋ค์ ๊ธฐ๋ฐ์ด ๋๋ ํต์ฌ ๋ชจ๋๋ค์ ๋จผ์ ๋ง๋ค์ด!
Phase 3: ๊ธฐ๋ฅ ๋ชจ๋ ๋ถ๋ฆฌ (4-6์ฃผ)
1๏ธโฃ ๊ฐ์ฅ ๋
๋ฆฝ์ ์ธ ๊ธฐ๋ฅ๋ถํฐ ์์
2๏ธโฃ ํ ๋ฒ์ ํ๋์ฉ ์ ์ง์ ์ผ๋ก ๋ถ๋ฆฌ
3๏ธโฃ ๊ฐ ๋ชจ๋๋ง๋ค ํ
์คํธ ์์ฑ
4๏ธโฃ CI/CD ํ์ดํ๋ผ์ธ ๊ตฌ์ถ
์ฃผ์์ฌํญ:
- ํ ๋ฒ์ ๋ชจ๋ ๊ฑธ ๋ฐ๊พธ๋ ค๊ณ ํ์ง ๋ง!
- ๊ฐ ๋จ๊ณ๋ง๋ค ์ถฉ๋ถํ ํ
์คํธ ์ํ
- ํ์๋ค๊ณผ ์ง์์ ์ผ๋ก ์ํต
Phase 4: ์ต์ ํ ๋ฐ ์์ ํ (2-3์ฃผ)
โ
๋น๋ ์๊ฐ ์ธก์ ๋ฐ ์ต์ ํ
โ
์์กด์ฑ ๊ทธ๋ํ ์ ๋ฆฌ
โ
๋ฌธ์ํ ์์ฑ
โ
ํ ๊ต์ก ๋ฐ ๊ฐ์ด๋๋ผ์ธ ์ ๋ฆฌ
โ
๋ชจ๋ํฐ๋ง ๋ฐ ๋ฉํธ๋ฆญ ์์ง
โ ๏ธ ํํ ์ค์๋ค
1. ๋๋ฌด ์ธ๋ฐํ๊ฒ ๋๋๊ธฐ
๋ชจ๋์ด ๋๋ฌด ๋ง์ผ๋ฉด ๊ด๋ฆฌ๊ฐ ๋ณต์กํด์ ธ. ์ ์ ํ ํฌ๊ธฐ๋ฅผ ์ ์งํด!
2. ์ํ ์์กด์ฑ
A โ B โ A ๊ฐ์ ์ํ ์์กด์ฑ์ ์ ๋ ๋ง๋ค์ง ๋ง! ์ปดํ์ผ ์๋ฌ์ ์์ธ์ด ๋ผ.
3. ์ ๊ทผ ์ ์ด ๋ฌด์
๋ชจ๋ ๊ฑธ public์ผ๋ก ๋ง๋ค๋ฉด ์บก์ํ์ ์๋ฏธ๊ฐ ์์ด์ ธ.
4. ํ
์คํธ ๋ถ์กฑ
๋ชจ๋์ ๋ถ๋ฆฌํ ๋ ํ
์คํธ ์์ด ์งํํ๋ฉด ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ ํ๋ฅ ์ด ๋์.
5. ๋ฌธ์ํ ์ํ
๊ฐ ๋ชจ๋์ ๋ชฉ์ ๊ณผ ์ฌ์ฉ๋ฒ์ ๋ช
ํํ ๋ฌธ์ํํ์ง ์์ผ๋ฉด ํ์๋ค์ด ํผ๋์ค๋ฌ์ํด.
โจ ์ฑ๊ณต ์ฌ๋ก
ํ ์คํํธ์
์์ ๋ชจ๋ํ๋ฅผ ๋์
ํ ๊ฒฐ๊ณผ:
๐ ๋น๋ ์๊ฐ 70% ๊ฐ์ (10๋ถ โ 3๋ถ)
๐ ๋ฒ๊ทธ ๋ฐ์๋ฅ 40% ๊ฐ์
๐ฅ ํ ์์ฐ์ฑ 50% ํฅ์
โป๏ธ ์ฝ๋ ์ฌ์ฌ์ฉ๋ฅ 80% ์ฆ๊ฐ
๐ ๊ฐ๋ฐ์ ๋ง์กฑ๋ ํฌ๊ฒ ์์น
ํนํ ์๋ก์ด ๊ธฐ๋ฅ์ ์ถ๊ฐํ ๋ ๊ธฐ์กด ์ฝ๋๋ฅผ ๊ฑด๋๋ฆฌ์ง ์๊ณ ๋
๋ฆฝ์ ์ผ๋ก ๊ฐ๋ฐํ ์ ์์ด์ ๊ฐ๋ฐ ์๋๊ฐ ์์ฒญ ๋นจ๋ผ์ก๋! ๐
๐ฎ ๋ฏธ๋ ํธ๋ ๋์ ๋ฐ์ ๋ฐฉํฅ
Swift Package Manager์ ๋ชจ๋ํ๋ ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด! ์์ผ๋ก์ ํธ๋ ๋๋ฅผ ๋ฏธ๋ฆฌ ์์๋๋ฉด ๋ ๋์ ๊ฐ๋ฐ์๊ฐ ๋ ์ ์์ง. ๐
๐ฏ ์ฃผ๋ชฉํด์ผ ํ ํธ๋ ๋
1. Swift 6์ ๋์์ฑ ๋ชจ๋ธ
Swift 6์์๋ ์์ ํ ๋์์ฑ ์์ ์ฑ์ ์ ๊ณตํ ์์ ์ด์ผ. ๋ชจ๋ ์ค๊ณ ์ actor์ async/await๋ฅผ ๋์ฑ ์ ๊ทน์ ์ผ๋ก ํ์ฉํ๊ฒ ๋ ๊ฑฐ์ผ!
// Swift 6 ์คํ์ผ
actor DataCache {
private var cache: [String: Data] = [:]
func get(_ key: String) -> Data? {
cache[key]
}
func set(_ data: Data, forKey key: String) {
cache[key] = data
}
}
2. ๋ฐ์ด๋๋ฆฌ ํ๋ ์์ํฌ ์ง์ ๊ฐํ
SPM์ ๋ฐ์ด๋๋ฆฌ ํ๋ ์์ํฌ ์ง์์ด ๊ณ์ ๊ฐ์ ๋๊ณ ์์ด. ๋น๋ ์๊ฐ์ ๋์ฑ ๋จ์ถํ ์ ์๊ฒ ๋ ๊ฑฐ์ผ!
.binaryTarget์ ์ฌ์ฉํ๋ฉด ๋ฏธ๋ฆฌ ์ปดํ์ผ๋ ํ๋ ์์ํฌ๋ฅผ ๋ฐฐํฌํ ์ ์์ด:
targets: [
.binaryTarget(
name: "MyPrebuiltFramework",
url: "https://example.com/MyFramework.xcframework.zip",
checksum: "abc123..."
)
]
3. ํ๋ฌ๊ทธ์ธ ์์คํ
SPM ํ๋ฌ๊ทธ์ธ์ ์ฌ์ฉํ๋ฉด ๋น๋ ํ๋ก์ธ์ค๋ฅผ ์ปค์คํฐ๋ง์ด์งํ ์ ์์ด. ์ฝ๋ ์์ฑ, ๋ฆฐํ
, ํฌ๋งทํ
๋ฑ์ ์๋ํํ ์ ์์ง!
์๋ฅผ ๋ค์ด, SwiftGen ๊ฐ์ ๋๊ตฌ๋ฅผ SPM ํ๋ฌ๊ทธ์ธ์ผ๋ก ํตํฉํ ์ ์์ด.
4. ํฌ๋ก์ค ํ๋ซํผ ์ง์ ํ๋
Swift๊ฐ Linux, Windows์์๋ ์ ์ ๋ ์ ์๋ํ๊ณ ์์ด. ์๋ฒ ์ฌ์ด๋ Swift ๊ฐ๋ฐ์ด ํ์ฑํ๋๋ฉด์ ๋ชจ๋ ์ฌ์ฌ์ฉ์ฑ์ด ๋์ฑ ์ค์ํด์ง ๊ฑฐ์ผ! ๐
๐ก ๋ฏธ๋๋ฅผ ์ค๋นํ๋ ๋ฐฉ๋ฒ
1. Swift Evolution ํ๋ก์ฐ: ์๋ก์ด ๊ธฐ๋ฅ ์ ์์ ์ฃผ์ํด
2. ์คํ์์ค ๊ธฐ์ฌ: ์ธ๊ธฐ ์๋ SPM ํจํค์ง์ ๊ธฐ์ฌํ๋ฉด์ ๋ฐฐ์
3. ์ปค๋ฎค๋ํฐ ์ฐธ์ฌ: Swift ํฌ๋ผ, ์ปจํผ๋ฐ์ค์์ ์ต์ ํธ๋ ๋ ํ์
4. ์คํ์ ๊ธฐ๋ฅ ํ
์คํธ: ๋ฒ ํ ๋ฒ์ ์์ ์ ๊ธฐ๋ฅ์ ๋ฏธ๋ฆฌ ๊ฒฝํํด๋ด
5. ์ง์์ ํ์ต: ๊ธฐ์ ์ ๊ณ์ ๋ฐ์ ํ๋๊น ๊พธ์คํ ๊ณต๋ถํด์ผ ํด!
๐ ์ถ๊ฐ ํ์ต ๋ฆฌ์์ค
๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ์ด๋ฐ ๋ฆฌ์์ค๋ค์ ํ์ฉํด๋ด! ๐
๊ณต์ ๋ฌธ์
๐ Swift Package Manager ๊ณต์ ๋ฌธ์
- https://swift.org/package-manager/
- ๊ฐ์ฅ ์ ํํ๊ณ ์ต์ ์ ๋ณด๋ฅผ ์ป์ ์ ์์ด
๐ Swift.org ๋ธ๋ก๊ทธ
- https://swift.org/blog/
- ์๋ก์ด ๊ธฐ๋ฅ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค ์๊ฐ
์ถ์ฒ ๋์
๐ "Advanced Swift" - objc.io
๐ "Swift in Depth" - Tjeerd in 't Veen
๐ "iOS App Architecture" - objc.io
๐ "Clean Architecture" - Robert C. Martin
์ ์ฉํ ๋๊ตฌ๋ค
๐ ๏ธ SwiftLint - ์ฝ๋ ์คํ์ผ ๊ฒ์ฌ
๐ ๏ธ SwiftFormat - ์ฝ๋ ํฌ๋งทํ
๐ ๏ธ Sourcery - ์ฝ๋ ์์ฑ
๐ ๏ธ Tuist - ํ๋ก์ ํธ ์์ฑ ์๋ํ
๐ ๏ธ XcodeGen - Xcode ํ๋ก์ ํธ ํ์ผ ์์ฑ
์ปค๋ฎค๋ํฐ
๐ฌ Swift Forums - https://forums.swift.org/
๐ฌ iOS Developers Slack
๐ฌ Reddit r/swift
๐ฌ Stack Overflow - swift-package-manager ํ๊ทธ
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด! ๐ Swift Package Manager์ ๋ชจ๋ํ์ ๋ํด ๋ง์ ๊ฑธ ๋ฐฐ์ ์ง?
ํต์ฌ์ ๋ค์ ์ ๋ฆฌํด๋ณด๋ฉด:
๐ฏ ํต์ฌ ์์ฝ
1๏ธโฃ SPM์ Apple ๊ณต์ ์์กด์ฑ ๊ด๋ฆฌ ๋๊ตฌ
- Xcode์ ์๋ฒฝํ๊ฒ ํตํฉ
- ๊ฐ๋จํ ์ค์ ๊ณผ ์ฌ์ฉ๋ฒ
- ํฌ๋ก์ค ํ๋ซํผ ์ง์
2๏ธโฃ ๋ชจ๋ํ๋ ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ์ ํต์ฌ
- ๋น๋ ์๊ฐ ๋จ์ถ
- ํ
์คํธ ์ฉ์ด์ฑ
- ํ ํ์
ํจ์จ ํฅ์
- ์ ์ง๋ณด์์ฑ ๊ฐ์
3๏ธโฃ ์ ์ ํ ๋ชจ๋ ๊ตฌ์กฐ ์ค๊ณ๊ฐ ์ค์
- ๋ ์ด์ด๋ณ ๋ถ๋ฆฌ
- ๋ช
ํํ ์ฑ
์ ์ ์
- ์์กด์ฑ ๋ฐฉํฅ ๊ด๋ฆฌ
- ์ ๊ทผ ์ ์ด ํ์ฉ
4๏ธโฃ ์ค๋ฌด์์๋ ์ ์ง์ ๋์
- ๋จ๊ณ๋ณ ๋ง์ด๊ทธ๋ ์ด์
- ์ถฉ๋ถํ ํ
์คํธ
- ํ ๊ต์ก๊ณผ ๋ฌธ์ํ
- ์ง์์ ์ธ ์ต์ ํ
โจ ๋น์ ์ ๋ค์ ๋จ๊ณ
์ด์ ๋ฐฐ์ด ๋ด์ฉ์ ์ค์ ๋ก ์ ์ฉํด๋ณผ ์ฐจ๋ก์ผ! ์์ ํ๋ก์ ํธ๋ถํฐ ์์ํด์ ์ ์ฐจ ํ์ฅํด๋๊ฐ๋ด. ์ฒ์์๋ ์ด๋ ค์ธ ์ ์์ง๋ง, ๊ณ์ ์ฐ์ตํ๋ค ๋ณด๋ฉด ์์ฐ์ค๋ฝ๊ฒ ์ต์ํด์ง ๊ฑฐ์ผ! ๐ช
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ด๋ฐ ๋ชจ๋ํ ๋ฅ๋ ฅ์ ํ์ฉํ๋ฉด, ๋ ์ ๋ฌธ์ ์ธ ๊ฐ๋ฐ์๋ก ์ธ์ ๋ฐ์ ์ ์์ด. ํด๋ผ์ด์ธํธ์๊ฒ "์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๊ณ ์ ์ง๋ณด์ํ๊ธฐ ์ฌ์ด ์ฝ๋"๋ฅผ ์ ๊ณตํ ์ ์๋ค๋ ๊ฑด ์ ๋ง ํฐ ๊ฐ์ ์ด๊ฑฐ๋ ! ๐
๐ก ๋ง์ง๋ง ์กฐ์ธ
์๋ฒฝํ ๋ชจ๋ ๊ตฌ์กฐ๋ ์์ด. ํ๋ก์ ํธ์ ํน์ฑ, ํ์ ๊ท๋ชจ, ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ์ ๋ฐ๋ผ ์ต์ ์ ๊ตฌ์กฐ๋ ๋ฌ๋ผ์ ธ. ์ค์ํ ๊ฑด ์ง์์ ์ผ๋ก ๊ฐ์ ํ๊ณ ๋ฐฐ์ฐ๋ ์์ธ์ผ!
์ค์๋ฅผ ๋๋ ค์ํ์ง ๋ง. ๋ชจ๋ ๊ตฌ์กฐ๋ฅผ ์๋ชป ์ค๊ณํ๋ค๋ฉด ๋ฆฌํฉํ ๋งํ๋ฉด ๋ผ. ๊ทธ ๊ณผ์ ์์ ๋ ๋ง์ด ๋ฐฐ์ฐ๊ฒ ๋ ๊ฑฐ์ผ. ํ์ดํ
! ๐
๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋ ๊น์ด ์๋ ๋ด์ฉ์ ์๊ณ ์ถ๋ค๋ฉด, Swift ์ปค๋ฎค๋ํฐ์ ์ฐธ์ฌํด๋ด. ๋ง์ ๊ฐ๋ฐ์๋ค์ด ๊ธฐ๊บผ์ด ๋์์ค ๊ฑฐ์ผ. ๊ทธ๋ฆฌ๊ณ ์ฌ๋ฅ๋ท์์๋ ํ๋ก๊ทธ๋๋ฐ ๊ด๋ จ ์ฌ๋ฅ์ ๊ณต์ ํ๊ณ ๋ฐฐ์ธ ์ ์์ผ๋๊น ํ์ฉํด๋ณด๋ ๊ฒ๋ ์ข์! ๐
ํ๋ณตํ ์ฝ๋ฉ ๋๊ธธ ๋ฐ๋ผ! Happy Coding! ๐จโจ
๐ ๐ฆ ๐ป โจ
๋ชจ๋ํ๋ก ๋ ๋์ ์ฝ๋๋ฅผ, SPM์ผ๋ก ๋ ์ฌ์ด ๊ด๋ฆฌ๋ฅผ!
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.