fromm μ±μ λͺ¨λν λμ νκΈ°
- #λͺ¨λν
- #Modularization
- #swift
- #ios
- #fromm
λ€μ΄κ°λ©°
μλ νμΈμ. μ λ νμ¬ βfrommβ μ±μμ iOS κ°λ°μ νκ³ μλ μ΄νμ°μ λλ€.
νμ¬ fromm μ±μ Clean Architectureμ MVVM ν¨ν΄μ κΈ°λ°μΌλ‘ λ§λ€μ΄μ Έ μμ΅λλ€.
μ΅κ·Ό μλΉμ€ κ·λͺ¨κ° νμ₯ λλ©΄μ 리ν©ν λ§μ μ§ννκ² λμκ³ , κΈ°μ‘΄ κ°λ° κ³Όμ μμ κ²ͺμλ λͺκ°μ§ μ΄λ €μμ ν΄κ²°νκΈ° μν΄ TCA Architectureμ λͺ¨λνλ₯Ό λμ νκΈ°λ‘ κ²°μ νκ² λμμ΅λλ€. κ·Έλμ μ΄λ² κΈμμλ λͺ¨λνλ₯Ό νλ κ³Όμ μμ λκΌλ μ μ ν¨κ» 곡μ νλ €κ³ ν©λλ€.
AS-IS λΆμμ ν΅ν μ΄μ μν© μκ°
λ¨Όμ , μλΉμ€λ₯Ό κ°λ°νλ©΄μ μ§λ©΄νλ λ¬Έμ μ λ€μ λ§μλλ¦¬κ² μ΅λλ€.
μλΉμ€ κ°λ° κ³Όμ μμ κ°μ₯ ν¬κ² λκΌλ λ¬Έμ μ μ€ νλλ λ°λ‘ μ½λμ κΈΈμ΄μ κ°λ μ± λ¬Έμ μμ΅λλ€.
νΉν βfrommβ μ±μμ μ±ν κΈ°λ₯μ κ΄λ¦¬νλ ν΄λμ€λ§ λ΄λ μ½λλ λ¬΄λ € 1170μ€μ λ¬νλλ°μ. μ΄λ κ² κΈ΄ μ½λλ κ°λ μ±μ ν¬κ² μ ν΄ν λΏλ§ μλλΌ, νΉμ λΆλΆμ μμ νκ±°λ κΈ°λ₯μ μΆκ°νλ € ν λ μ 체 μ½λλ₯Ό νμ νλ λ° λ§μ μκ°μ΄ μμλμ΄ μ 무 ν¨μ¨μ±μ΄ λ¨μ΄μ§λ λ¬Έμ κ° μμ΅λλ€.
κ·Έ λ€μμΌλ‘ λκΌλ λ¬Έμ μ μ, μμ‘΄μ± κ΄λ¦¬μ μ΄λ €μ λ¬Έμ μμ΅λλ€.
fromm μλΉμ€κ° μ μ 컀μ§κ² λλ©΄μ, λ€μν μ»΄ν¬λνΈλ κΈ°λ₯λ€ μ¬μ΄μ 볡μ‘ν μμ‘΄μ±μ΄ νμ±λμμ΅λλ€. μ΄ λ³΅μ‘ν μμ‘΄μ±μΌλ‘ μΈν΄ ν λΆλΆμ λ³νκ° λ€λ₯Έ λΆλΆμλ λ―ΈμΉλ μν₯μ μμΈ‘νκ³ κ΄λ¦¬νλ κ²μ΄ μ μ μ΄λ €μμ§κ³ μμ΅λλ€.
λ§μ§λ§μΌλ‘ λκΌλ λ¬Έμ μ μ, λΉλ λ° μ»΄νμΌ μκ°μ λ¬Έμ μμ΅λλ€.
νμ¬ μ½λ ꡬ쑰μ, A κΈ°λ₯μμ μμ£Ό μμ μμ μ΄ μ΄λ£¨μ΄μ Έλ μ 체 μ±μ λΉλν΄μΌ νλ μν©μ΄ λ°μν©λλ€.
λ§€λ² μμ μμ μ¬νλ§λ€ μ 체λ₯Ό λΉλνκ³ νμΈν΄μΌνλ κ²μ μκ° μλͺ¨κ° ν¬λ©°, μ΄λ λΆνμν μκ° λλΉλ₯Ό μ΄λνκ² λ©λλ€. μ΄λ¬ν λ¬Έμ λ€μ ν΄κ²°νκΈ° μν΄μλ κ° κΈ°λ₯λ³λ‘ λ 립μ μΈ λΉλμ ν μ€νΈκ° κ°λ₯ν κ΅¬μ‘°λ‘ κ°μ ν νμκ° μλ€κ³ μκ°νμ΅λλ€.
λͺ¨λνλ?
λͺ¨λνλ λκ·λͺ¨ νλ‘μ νΈλ₯Ό λ μκ³ κ΄λ¦¬νκΈ° μ¬μ΄ λͺ¨λλ‘ λλλ μννΈμ¨μ΄ κ°λ° κΈ°μ μ λλ€.
κ° λͺ¨λμ λ 립μ μΌλ‘ μλνλ©° νΉμ κΈ°λ₯μ λ΄λΉνκ² λ©λλ€. μ΄λ κ² νλ‘μ νΈλ₯Ό μ¬λ¬ λͺ¨λλ‘ λλλ©΄, κ° λͺ¨λμ μ체μ μΌλ‘ ν μ€νΈμ λλ²κΉ μ΄ κ°λ₯νλ©° λ 립μ μΌλ‘ μ λ°μ΄νΈνκ±°λ μμ ν μ μμ΅λλ€.
λͺ¨λν μ€κ³ λ° κ΅¬ν
λͺ¨λν μ€κ³
κΈ°μ‘΄μλ νλμ νλ‘μ νΈ μμ Application, Domain, Data, Presentation, Common λ± ν¨κ» μ‘΄μ¬νμ¬ κ΄λ¦¬λκ³ μμμ΅λλ€. μ΄λ₯Ό λͺ¨λν νκΈ° μν΄ App, Base, Feature, Data, Core Module κ΄κ³λ₯Ό κ·Έλ €λ³΄μμ΅λλ€.
λ¨Όμ App Moduleμ μ ν리μΌμ΄μ μ μ£Όμ μ§μ μ μΌλ‘, νλ‘μ νΈμ μ λ°μ μΈ μ€μ κ³Ό μ΄κΈ°ν μμ μ λ΄λΉν©λλ€.
νμ¬ fromm μλΉμ€μ κ²½μ°, μ± λͺ¨λμ΄ fan-appκ³Ό arti-appμΌλ‘ λλμ΄μ Έ μμ΅λλ€.
Feature Moduleμ μ ν리μΌμ΄μ μ κ° κΈ°λ₯ λ¨μλ‘ κ΅¬λΆλμ΄, νΉμ κΈ°λ₯μ λ 립μ μΌλ‘ λ΄λΉν©λλ€.
μ΄ λͺ¨λμ μ£Όλ‘ λ λΆλΆμΌλ‘ ꡬμ±λκ³ μμ΅λλ€.
- Reducer : λΉμ¦λμ€ λ‘μ§κ³Ό μν κ΄λ¦¬λ₯Ό λ΄λΉνλ λΆλΆ
- Presentation : Viewμ κ΄λ ¨λ μμ μ μ²λ¦¬νλ λΆλΆ ( μ£Όλ‘ ViewController, View λ±μ μ»΄ν¬λνΈλ‘ κ΅¬μ± )
Data Moduleμ λ°μ΄ν° κ΄λ ¨ μμ μ λͺ¨μλμ λͺ¨λλ‘, ν¬κ² DataModel, Client, Network μΈ λΆλΆμ λ΄λΉν©λλ€.
- DataModel
μ ν리μΌμ΄μ μμ μ¬μ©λλ λͺ¨λ λ°μ΄ν° λͺ¨λΈμ μ μνκ³ μλ ν¨ν€μ§μ λλ€.
λνμ μΌλ‘ APIμμ λ°μμ¨ μλ΅ λ°μ΄ν°λ₯Ό λͺ¨λΈλ§ν Response λͺ¨λΈκ³Ό λ‘컬 λ°μ΄ν°λ² μ΄μ€μ λͺ¨λΈ κ°μ²΄λ₯Ό μλ―Ένλ Entity λͺ¨λΈμ΄ ν¬ν¨λμ΄ μμ΅λλ€.
- Client
λͺ¨λ ν΄λΌμ΄μΈνΈλ€μ μΈν°νμ΄μ€μ κ·Έμ λν κΈ°λ³Έ ꡬνμ ν¬ν¨νκ³ μλ ν¨ν€μ§μ λλ€.
- NetworkProvider
μΈλΆ API νΈμΆμ νμν Service μΈν°νμ΄μ€μ CacheManagerλ₯Ό ν¬ν¨νκ³ μμ΅λλ€.
Core Moduleμ ν¬μ±, μν°μ±μμ 곡ν΅μΌλ‘ μ¬μ©νλ UI μ»΄ν¬λνΈ λ° λ¦¬μμ€ λ±μ λ΄λΉν©λλ€.
λͺ¨λν ꡬν
μ μ΄μ μΈνΈλ‘ νλ©΄μμ λ‘κ·ΈμΈ λ²νΌμ λλ₯΄λ©΄ λνλλ βλ‘κ·ΈμΈ IDλ₯Ό μ λ ₯νλ λͺ¨λ(Feature λͺ¨λ)βμ λ§λ€μ΄λ³΄κ² μ΅λλ€.
Platform ν¨ν€μ§λ λ μ±μμ 곡ν΅μΌλ‘ μ¬μ©νλ λͺ¨λλ€μ ν¬ν¨νκ³ μμ΅λλ€.
NetworkProvider λͺ¨λμ μΈλΆ API νΈμΆμ νμν Service μΈν°νμ΄μ€μ λ°μ΄ν° μΊμ±μ κ΄λ¦¬νλ CacheManagerκ° μ‘΄μ¬νλ©° μ΄λ₯Ό ν΅ν΄ μ ν리μΌμ΄μ μ μΈλΆ μμ€ν κ³Όμ ν¨μ¨μ μΈ ν΅μ λ₯Ό μνν μ μμ΅λλ€.
// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Platform",
platforms: [.iOS(.v14)],
products: [
.library(
name: "NetworkProvider",
targets: ["NetworkProvider"]),
.library(
name: "FrommUI",
targets: ["FrommUI"])
],
dependencies: [
...
],
targets: [
...
.target(
name: "NetworkProvider",
dependencies: [
"Alamofire",
.product(name: "UserKit", package: "UserKit"),
.product(name: "CommonKit", package: "CommonKit"),
]
),
.target(
name: "FrommUI",
dependencies: [
...
]
)
]
)
Client ν¨ν€μ§μλ μΈν°νμ΄μ€μ νΉμ μΈν°νμ΄μ€μ ꡬνμ΄ κ°κ° ꡬλΆλμ΄ λͺ¨λν λμ΄μμ΅λλ€.
AuthClientλ λ‘κ·ΈμΈ μμ μ νμν κ³μ μ‘΄μ¬ μ¬λΆλ₯Ό νμΈνκ±°λ, λΉλ°λ²νΈλ₯Ό μ¬μ€μ νκ±°λ, λ‘κ·ΈμΈνλ λ±μ μμ λ€μ μ μν μΈν°νμ΄μ€μ λλ€. AuthClientLiveλ AuthClient μΈν°νμ΄μ€λ₯Ό ꡬνν μ€μ ν΄λμ€λ‘, κ° λ©μλμμ λ€νΈμν¬ μμ²μ μννκ³ κ·Έ κ²°κ³Όλ₯Ό μ²λ¦¬ν©λλ€.
import PackageDescription
let package = Package(
name: "Client",
platforms: [.iOS(.v14)],
products: [
.library(name: "AuthClient", targets: ["AuthClient"]),
.library(name: "AuthClientLive", targets: ["AuthClientLive"])
],
dependencies: [
.package(path: "../../Pacakge/DataModel"),
.package(path: "../../Pacakge/Platform"),
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.1.5"),
],
targets: [
.target(
name: "AuthClient",
dependencies: [
.product(name: "AuthDataModels", package: "DataModel"),
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"),
]
),
.target(
name: "AuthClientLive",
dependencies: [
"AuthClient",
.product(name: "NetworkProvider", package: "Platform"),
]
)
]
)
μ΄μ frommArti-app ν¨ν€μ§λ₯Ό μμ±ν λ€ λ§λ€λ €κ³ νλ Feature λͺ¨λ(ArtiSignInUsername)μ μΆκ°ν΄μ€λλ€. μ΄ ν¨ν€μ§λ λ‘컬 κ²½λ‘μ μμΉν βClientβ, βCommonKitβ, βPlatformβ ν¨ν€μ§λ€κ³Ό GitHubμμ κ°μ Έμ¨ βswift-composable-architectureβ ν¨ν€μ§μ μμ‘΄μ±μ κ°μ§κ³ μμμ μ μ μμ΅λλ€.
κ·Έλ¦¬κ³ 4κ°μ§ μμ‘΄μ±μ μ§λκ³ μλ ArtiSignInUsername λͺ¨λμ μ’ λ μ΄ν΄λ³΄λ©΄, κ° ν¨ν€μ§ μ€μμ μ΄ Feature λͺ¨λμ νμν AuthClient, CommonKit, ComposableArchitecture, βFrommUIβλ§ μμ‘΄μ±μ κ°μ§κ³ μμμ νμΈν μ μμ΅λλ€. μ΄λ κ² μμ‘΄ κ΄κ³λ₯Ό νμ νκΈ° μμν΄μ‘μΌλ©° κ΄λ¦¬νκΈ° μ©μ΄ν΄μ‘μ΅λλ€.
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "frommArti-app",
defaultLocalization: "en",
platforms: [
.iOS(.v14)
],
products: [
.library(name: "ArtiCoordinator", targets: ["ArtiCoordinator"]),
.library(name: "ArtiSignInUsername", targets: ["ArtiSignInUsername"])
],
dependencies: [
.package(path: "../../Pacakge/Client"),
.package(path: "../../Pacakge/CommonKit"),
.package(path: "../../Pacakge/Platform"),
.package(url: "https://github.com/pointfreeco/swift-composable-architecture",
from: "1.6.0"),
],
targets: [
.target(
name: "ArtiCoordinator",
dependencies: [
"ArtiSignInUsername",
]
),
.target(
name: "ArtiSignInUsername",
dependencies: [
.product(name: "AuthClient", package: "Client"),
.product(name: "CommonKit", package: "CommonKit"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "FrommUI", package: "Platform"),
]
),
.testTarget(
name: "ArtiSignInUsernameTests",
dependencies: ["ArtiSignInUsername"]
)
]
)
λ§μ§λ§μΌλ‘ ArtiSignInUsername λͺ¨λμ λΉμ¦λμ€ λ‘μ§κ³Ό View κ΄λ ¨ μ½λλ₯Ό μμ±ν΄μ£Όλ©΄ λ©λλ€.
import AuthClient
import ComposableArchitecture
public struct SignInUsernameFeature: Reducer {
public struct State: Equatable { ... }
public enum Action: BindableAction { ... }
@Dependency(\.authClient.checkExistAccount) var isExistUser
public var body: some ReducerOf<Self> { ... }
private func changeInputState(
username: String
) -> State.InputState { ... }
import Combine
import CommonKit
import ComposableArchitecture
import UIKit
public final class SignInUsernameViewController: UIViewController {
struct ViewState: Equatable { ... }
}
enum ViewAction { ... }
let store: StoreOf<SignInUsernameFeature>
let viewStore: ViewStore<ViewState, ViewAction>
private lazy var layout: SignInUsernameLayout = .init(rootView: view)
...
}
extension SignInUsernameFeature.Action {
init(action: SignInUsernameViewController.ViewAction) {
...
}
}
λ§μ½ ArtiSignInUsernameμ λ³κ²½μ¬ν ν λ¬Έμ κ° μλμ§ νμΈνκ³ μΆλ€λ©΄ ν΄λΉ λͺ¨λλ§ λΉλν μ μμ΅λλ€.
μ΄μ μ 체 μλΉμ€λ₯Ό λΉλνμ¬ κΈ°μ‘΄ μΈνΈλ‘ νλ©΄μμ ArtiSignInUsername λͺ¨λμ΄ μ λμλλ μ§ νμΈνλ©΄ λ©λλ€.
import ArtiSignInUsername
public final class IntroViewController: UIViewController {
...
private func pushToSigninUsername() {
let vc = SignInUsernameViewController()
navigationController?.pushViewController(vc, animated: true)
}
}
λͺ¨λν λμ μ΄ κ°μ Έμ¨ μν₯κ³Ό κ²°λ‘
κ·Έλ λ€λ©΄, μ΄μ βfrommβ μ±μμ λͺ¨λν λμ μ ν΅ν μ¬λ¬κ°μ§ λ³νλ₯Ό μ΄ν΄λ³΄κ² μ΅λλ€.
μ°μ , βfrommβ μ±μ μ½λ κ΄λ¦¬μ κ°μ μμ μ΄ λμ± ν¨κ³Όμ μΌλ‘ μ΄λ£¨μ΄μ§κ² λμμ΅λλ€. κ° λͺ¨λμ λ 립μ μΌλ‘ λΉλλκ³ ν μ€νΈλ μ μμ΄, νΉμ λͺ¨λμ λ³κ²½μ¬νμ΄ λ€λ₯Έ λͺ¨λμ μν₯μ μ£Όμ§ μμ΅λλ€. μ΄λ‘ μΈν΄ μ±μ μμ μ±μ΄ ν₯μλκ³ , κ°λ³ λͺ¨λμ λν κ°μ μμ μ΄ λμ± μμν΄μ‘μ΅λλ€.
λν, μμ λ³κ²½μ¬ν λ°μ μμλ μ 체 μ±μ λΉλν νμ μμ΄ ν΄λΉ λͺ¨λλ§ λΉλν¨μΌλ‘μ¨ μ»΄νμΌ μλκ° ν¬κ² ν₯μλμμ΅λλ€. μ΄λ‘μ¨ κ°λ° μκ°μ λ¨μΆμν€κ³ μ 무 ν¨μ¨μ ν¬κ² ν₯μμμΌ°μ΅λλ€.
λ§μ§λ§μΌλ‘, λͺ¨λνλ₯Ό ν΅ν΄ κ° λͺ¨λμ μμ μ΄ νμλ‘ νλ μμ‘΄μ±λ§μ κ°μ§κ² λμ΄, λΆνμν μμ‘΄μ±μ ν¬ν¨μ΄ λ°©μ§λμμ΅λλ€.
μ΄λ κ² fromm μ±μ λͺ¨λν λμ μΌλ‘, κ°λ° κ³Όμ μ ν¨μ¨μ μ΄κ³ κ΄λ¦¬νκΈ° μ¬μ΄ λ°©ν₯μΌλ‘ 리ν©ν λ§ μ§νμ€μ μμ΅λλ€. μμ§μ μ μ© λ¨κ³μ΄κΈ° λλ¬Έμ λΆμ‘±ν μ μ΄ λ§μ§λ§ μ‘°κΈμ΄λλ§ λμμ΄ λμ ¨κΈ°λ₯Ό λ°λΌλ©΄μ μ΄λ§ κΈμ λ§μΉκ² μ΅λλ€. λκΉμ§ μ½μ΄μ£Όμ μ κ°μ¬ν©λλ€.