์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ Swift Package Manager์™€ ๋ชจ๋“ˆํ™”๋กœ ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ ๊ทน๋Œ€ํ™”ํ•˜๊ธฐ

๐Ÿš€ Swift Package Manager์™€ ๋ชจ๋“ˆํ™”๋กœ ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ ๊ทน๋Œ€ํ™”ํ•˜๊ธฐ

์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ๋ฐฐ์šฐ๋Š” SPM ์™„๋ฒฝ ๊ฐ€์ด๋“œ

์•ˆ๋…•! ๐Ÿ‘‹ ์˜ค๋Š˜์€ iOS ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๊ผญ ์•Œ์•„์•ผ ํ•  Swift Package Manager(SPM)์™€ ๋ชจ๋“ˆํ™”์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ํ˜น์‹œ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ ์  ์ปค์ง€๋ฉด์„œ ์ฝ”๋“œ ๊ด€๋ฆฌ๊ฐ€ ํž˜๋“ค์–ด์ง„ ์  ์žˆ์–ด? ์•„๋‹ˆ๋ฉด ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ์—์„œ ๋˜‘๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ๋ณต์‚ฌ-๋ถ™์—ฌ๋„ฃ๊ธฐ ํ•˜๋ฉด์„œ "์ด๊ฑฐ ์ข€ ๋น„ํšจ์œจ์ ์ธ๋ฐ..."๋ผ๊ณ  ์ƒ๊ฐํ•œ ์  ์žˆ๋‹ค๋ฉด, ๋ฐ”๋กœ ์ด ๊ธ€์ด ๋„ˆ๋ฅผ ์œ„ํ•œ ๊ฑฐ์•ผ! ๐Ÿ˜Š

์š”์ฆ˜ ๊ฐœ๋ฐœ ํŠธ๋ Œ๋“œ๋Š” ๋‹จ์ˆœํžˆ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์„ ๋„˜์–ด์„œ, ์–ผ๋งˆ๋‚˜ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ๋ฅผ ๋งŒ๋“œ๋А๋ƒ๊ฐ€ ํ•ต์‹ฌ์ด์•ผ. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•  ๋•Œ, ์ด๋Ÿฐ ๋ชจ๋“ˆํ™”๋œ ์ฝ”๋“œ ์ž‘์„ฑ ๋Šฅ๋ ฅ์€ ์ •๋ง ์ค‘์š”ํ•œ ์Šคํ‚ฌ๋กœ ํ‰๊ฐ€๋ฐ›๊ณ  ์žˆ์–ด!

๋ฉ”์ธ ํ”„๋กœ์ ํŠธ App Target ๋„คํŠธ์›Œํฌ ๋ชจ๋“ˆ UI ์ปดํฌ๋„ŒํŠธ ๋ชจ๋“ˆ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ชจ๋“ˆ ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ! ๐Ÿ“ฆ ๋ชจ๋“ˆํ™”๋œ ์ฝ”๋“œ ๊ตฌ์กฐ ๊ฐ ๋ชจ๋“ˆ์€ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฐœ๋ฐœํ•˜๊ณ  ์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ!

๐Ÿค” 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: ์‹ค์ œ ์ฝ”๋“œ๊ฐ€ ์žˆ๋Š” ํƒ€๊ฒŸ๋“ค

Package.swift ๊ตฌ์กฐ ๊ธฐ๋ณธ ์ •๋ณด name platforms swift-tools-version ์ œ๊ณต ํ•ญ๋ชฉ products library executable ๊ตฌ์„ฑ ์š”์†Œ targets dependencies resources 1 2 3

๐ŸŽจ ์‹ค์ „ ์˜ˆ์ œ: ๋„คํŠธ์›Œํฌ ๋ชจ๋“ˆ ๋งŒ๋“ค๊ธฐ

์ด๋ก ๋งŒ ๋ฐฐ์šฐ๋ฉด ์žฌ๋ฏธ์—†์ž–์•„? ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋„คํŠธ์›Œํฌ ๋ชจ๋“ˆ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ์ด ๋ชจ๋“ˆ์€ 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 ๊ฐ™์€ ํ—ˆ์šฉ์ ์ธ ๋ผ์ด์„ ์Šค๋ฅผ ์„ ํ˜ธํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ๐Ÿ“œ

์˜์กด์„ฑ ๊ด€๋ฆฌ ํ๋ฆ„๋„ ๋‚ด ํ”„๋กœ์ ํŠธ ํŒจํ‚ค์ง€ A ๋„คํŠธ์›Œํฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํŒจํ‚ค์ง€ B ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ํŒจํ‚ค์ง€ C JSON ํŒŒ์‹ฑ ํ•˜์œ„ ์˜์กด์„ฑ 1 ์ž๋™์œผ๋กœ ํ•ด๊ฒฐ๋จ ํ•˜์œ„ ์˜์กด์„ฑ 2 ์ž๋™์œผ๋กœ ํ•ด๊ฒฐ๋จ SPM์ด ๋ชจ๋“  ์˜์กด์„ฑ์„ ์ž๋™์œผ๋กœ ๋‹ค์šด๋กœ๋“œํ•˜๊ณ  ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค!

๐Ÿ—๏ธ ๋ชจ๋“ˆ ๊ตฌ์กฐ ์„ค๊ณ„ ์ „๋žต

๋ชจ๋“ˆํ™”๋ฅผ ํ•  ๋•Œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฑด ์–ด๋–ป๊ฒŒ ๋‚˜๋ˆŒ ๊ฒƒ์ธ๊ฐ€์•ผ. ๋ฌด์ž‘์ • ๋‚˜๋ˆ„๋ฉด ์˜คํžˆ๋ ค ๋ณต์žกํ•ด์งˆ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ์‹ค์ „์—์„œ ๊ฒ€์ฆ๋œ ๋ชจ๋“ˆ ๊ตฌ์กฐ ์„ค๊ณ„ ์ „๋žต์„ ์•Œ๋ ค์ค„๊ฒŒ! ๐ŸŽฏ

๐ŸŽจ ๋ ˆ์ด์–ด๋ณ„ ๋ถ„๋ฆฌ ์ „๋žต (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๊ฐœ ์ •๋„์˜ ๋ชจ๋“ˆ์ด ์ ๋‹นํ•ด! ๐ŸŽฏ

๋นŒ๋“œ ์‹œ๊ฐ„ ๋น„๊ต ๋ชจ๋“ˆํ™” ์ „ ์ „์ฒด ๋นŒ๋“œ: 8๋ถ„ 30์ดˆ ์ˆ˜์ • ํ›„: 6๋ถ„ 20์ดˆ ๋ชจ๋“ˆํ™” ํ›„ ์ „์ฒด: 4๋ถ„ 10์ดˆ 45์ดˆ 51% ๋‹จ์ถ• 88% ๋‹จ์ถ•! ๊ฐœ๋ฐœ์ž ์ƒ์‚ฐ์„ฑ ํ–ฅ์ƒ ํ•˜๋ฃจ 20๋ฒˆ ๋นŒ๋“œ ์‹œ ์•ฝ 2์‹œ๊ฐ„ ์ ˆ์•ฝ ํ•œ ๋‹ฌ์ด๋ฉด 40์‹œ๊ฐ„, ์ผ์ฃผ์ผ์น˜ ์ž‘์—… ์‹œ๊ฐ„ ํ™•๋ณด!

๐Ÿ”„ ๋ฒ„์ „ ๊ด€๋ฆฌ์™€ ๋ฐฐํฌ ์ „๋žต

๋ชจ๋“ˆ์„ ๋งŒ๋“ค์—ˆ์œผ๋ฉด ์ด์ œ ๋ฒ„์ „ ๊ด€๋ฆฌ๋ฅผ ํ•ด์•ผ ํ•ด! ํŠนํžˆ ์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ณตํ†ต ๋ชจ๋“ˆ์ด๋ผ๋ฉด ๋ฒ„์ „ ๊ด€๋ฆฌ๊ฐ€ ์ •๋ง ์ค‘์š”ํ•˜์ง€. 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
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.