Skip to content

Latest commit

ย 

History

History
333 lines (271 loc) ยท 11.9 KB

File metadata and controls

333 lines (271 loc) ยท 11.9 KB

โœจ small-apps

์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋ชจ์ง‘ํ•˜๋Š” ํ”„๋กœ์ ํŠธ connect

Git Flow

๋ธŒ๋Ÿฐ์น˜ ๋„ค์ด๋ฐ๊ณผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ๋‚ด์šฉ. ๋ธŒ๋Ÿฐ์น˜ ์ƒ์„ฑ ๋ฐ ๋ฐ˜์˜์‹œ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”!

๐Ÿ”— Git Flow(Notion)

Git Message

๊นƒ ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ์ž‘์„ฑ์‹œ ๊ทœ์น™์„ ์ง€์ผœ์„œ ์ปค๋ฐ‹์„ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”! (ํ•˜๋‹จ ์ฐธ๊ณ ๋งํฌ์— ํ…œํ”Œ๋ฆฟ๊ณผ๋Š” ๋‹ค๋ฅธ ๋‚ด์šฉ ์ž…๋‹ˆ๋‹ค. ๊ธ€์ž์ˆ˜ ๋ฐ ์„ค๋ช…์ด ๋” ์ž์„ธํ•ด์„œ ๊ฐ€์ ธ์™”์–ด์š”)

# <ํ‚ค์›Œ๋“œ>: <์ œ๋ชฉ>
############### ์ œ๋ชฉ์€ 50์ž ๊นŒ์ง€ ################# ->|

# ๋ณธ๋ฌธ ๋‚ด์šฉ
######### ๋ณธ๋ฌธ ๋‚ด์šฉ์€ 72์ž ๊นŒ์ง€ ####################################### -> |

# ํ•œ ์ค„ ๋‚ด์šฉ์€ ๊ทธ๋ƒฅ ์ž‘์„ฑํ•œ๋‹ค
# - ์—ฌ๋Ÿฌ ์ค„์ผ ๋•Œ๋Š” ๋งจ ์•ž์— '-'๋ฅผ ๋„ฃ๋Š”๋‹ค

# --- COMMIT END ---
#
# ํ‚ค์›Œ๋“œ
#   fix (์ˆ˜์ •) : ๋™์ž‘, ์˜คํƒ€ ๋“ฑ์„ ๊ณ ์ณค์„ ๋•Œ #(์ด์Šˆ ๋„˜๋ฒ„ ํ˜น์€ URL)
#   feat (๊ธฐ๋Šฅ) : ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์‹œ
#   refactor (๋ฆฌํŒฉํ† ๋ง) : ๊ธฐ์กด ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง ์ง„ํ–‰
#   docs (๋ฌธ์„œ) : ์ฝ”๋“œ ํŒŒ์ผ๋‹จ์œ„ ์ถ”๊ฐ€, ์ˆ˜์ •, ์‚ญ์ œ, ์ด๋™ ๋“ฑ
#   test (ํ…Œ์ŠคํŠธ) : ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ๋ฐ ์ˆ˜์ •
#   style (๋””์ž์ธ) : css ๋“ฑ ์ฝ”๋“œ์ž‘์—…์ด ์•„๋‹Œ ๋””์ž์ธ ์ž‘์—…
#   build (๋นŒ๋“œ) : ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€ ์‚ญ์ œ ๋“ฑ ๋นŒ๋“œ๋‹จ์œ„ ์„ค์ • ๋ณ€๊ฒฝ
# ------------------
# ๊ทœ์น™
#   - ์ œ๋ชฉ์ค„์€ ๋Œ€๋ฌธ์ž๋กœ ์‹œ์ž‘ํ•œ๋‹ค.
#   - ์ œ๋ชฉ์ค„์€ ๋ช…๋ น์–ด๋กœ ์ž‘์„ฑํ•œ๋‹ค.
#   - ์ œ๋ชฉ์ค„์€ ๋งˆ์นจํ‘œ๋กœ ๋๋‚ด์ง€ ์•Š๋Š”๋‹ค.
#   - ๋ณธ๋ฌธ๊ณผ ์ œ๋ชฉ์—๋Š” ๋นˆ์ค„์„ ๋„ฃ์–ด์„œ ๊ตฌ๋ถ„ํ•œ๋‹ค.
#   - ๋ณธ๋ฌธ์—๋Š” "์–ด๋–ป๊ฒŒ" ๋ณด๋‹ค๋Š” "์™œ"์™€ "๋ฌด์—‡์„" ์„ค๋ช…ํ•œ๋‹ค.
#   - ๋ณธ๋ฌธ์— ๋ชฉ๋ก์„ ๋‚˜ํƒ€๋‚ผ๋•Œ๋Š” "-"๋กœ ์‹œ์ž‘ํ•œ๋‹ค.
# --------------------

๐Ÿ”— ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ํ…œํ”Œ๋ฆฟ ์ ์šฉ๋ฐฉ๋ฒ•


Tuist

ํ”„๋กœ์ ํŠธ ํŒŒ์ผ ๋ฐ ํด๋” ๋ณ€๊ฒฝ์‹œ .xcodeproj๊ฐ€ ์ˆ˜์‹œ๋กœ ๋ณ€๊ฒฝ๋˜์–ด ํ˜‘์—…์‹œ ๋นˆ๋ฒˆํ•œ ๊นƒ ์ถฉ๋Œ๋“ฑ์˜ ์ด์Šˆ ํ•ด๊ฒฐ์„ ์œ„ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

โ— ์ฃผ์˜! CocoaPod ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ์œผ๋กœ ์•„๋ž˜์ˆœ์„œ์— ๋”ฐ๋ผ ๋ช…๋ น์–ด ์ง„ํ–‰ ํ•„์š”

1๏ธโƒฃ tuist fetch SPM, Carthage๋กœ ์ •์˜๋œ ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

2๏ธโƒฃ TUIST_EXCLUEDED_FRAMEWORK=TRUE tuist generate && pod install CocoaPod์„ ์‚ฌ์šฉํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ œ์™ธํ•˜๊ณ  ๋นŒ๋“œ ์ง„ํ–‰ && ์ดํ›„ CocoaPod์„ ์ด์šฉํ•˜์—ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ฐ€์ ธ์˜จ๋‹ค.

3๏ธโƒฃ Xcode Close Xcode๋ฅผ ์ข…๋ฃŒํ•œ๋‹ค (๋ช…๋ น์–ด ์•„๋‹˜ ์ˆ˜๋™์œผ๋กœ ์™„์ „์ข…๋ฃŒ ์ง„ํ–‰ํ•œ๋‹ค).

4๏ธโƒฃ tuist generate ๋ชจ๋“  ์˜์กด์„ฑ์„ ํฌํ•จํ•˜์—ฌ Xcode workspace ์ƒ์„ฑํ•œ๋‹ค.

  • ๋Œ€ํ‘œ ๋ช…๋ น์–ด 3๊ฐ€์ง€

1๏ธโƒฃ tuist generate Project.swift ํŒŒ์ผ์— ์ž‘์„ฑ๋œ ๋‚ด์šฉ๋Œ€๋กœ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ๋ช…๋ น์–ด.

2๏ธโƒฃ tuist edit Tuist ์„ค์ •์„ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด Project.swift ํŒŒ์ผ ์ˆ˜์ •์‹œ ๋ช…๋ น์–ด.

3๏ธโƒฃ tuist fetch Dependencies.swift์— ์ž‘์„ฑ๋œ ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฐ˜์˜์‹œ ๋ช…๋ น์–ด. ์ตœ์ดˆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฐ˜์˜์ดํ›„ Dependencies.swift ๋‚ด์šฉ์— ๋ณ€๋™์ด ์—†๋‹ค๋ฉด ์ถ”๊ฐ€ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.

Project+Templates.swift

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ์„ ์œ„ํ•œ ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ

//  Project+Templates.swift
import ProjectDescription

extension Project {
  public static func feature(
    name: String,                      // ์„ค์ •ํ•  ๋ชจ๋“ˆ์˜ ์ด๋ฆ„
    bundleId: String = "",             // 'com.sideproj.\(name)' ๋Œ€์‹  ์‚ฌ์šฉํ•  ๋ฒˆ๋“ค Id (Option)
    products: [COProduct],             // .framework(.dynamic | .static) ํ”„๋ ˆ์ž„์›Œํฌ ์ง€์ •
    isExcludedFramework: Bool = false, // CocoaPod์‚ฌ์šฉ์œผ๋กœ Pod์— ํ•ด๋‹นํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ œ์™ธํ•˜๊ณ  ๋นŒ๋“œํ•˜๊ธฐ ์œ„ํ•œ ํ™˜๊ฒฝ๋ณ€์ˆ˜.
    infoExtension: [String: InfoPlist.Value] = [:], // ๊ธฐ๋ณธ ์ง€์ •๋œ info.plist์— ๋” ์ถ”๊ฐ€ํ•  ๋‚ด์šฉ์ด ์žˆ๋Š”๊ฒฝ์šฐ ์ž‘์„ฑ.
    settings: Settings? = .default,
    dependencies: [TargetDependency] = [],          // ํ•ด๋‹น ๋ชจ๋“ˆ์˜ ์˜์กด์„ฑ.
    testDependencies: [TargetDependency] = [],      // ํ…Œ์ŠคํŠธ์šฉ ์˜์กด์„ฑ.
    externalDependencies: [TargetDependency] = []   // isExcludedFramework์— ํ•ด๋‹นํ•˜๋Š” ์˜์กด์„ฑ.
  ) -> Project {
    
    var targets: [Target] = []
    var schemes: [Scheme] = []
    
    var infoPlist: InfoPlist = .base(name: name)
    
    if !infoExtension.isEmpty {
      infoPlist = .custom(
        name: name,
        bundleId: bundleId,
        extentions: infoExtension
      )
    }
    
    // ๋ฉ”์ธ ์•ฑ ํƒ€๊ฒŸ.
    if products.contains(.app) {
      let target: Target = .init(
        name: name,
        platform: .iOS,
        product: .app,
        bundleId: bundleId.isEmpty ? "com.sideproj.\(name)" : bundleId,
        deploymentTarget: .iOS(targetVersion: "15.0", devices: [.iphone]),
        infoPlist: infoPlist,
        sources: ["Sources/**"],
        resources: ["Resources/**"],
        entitlements: .relativeToRoot("App/connect.entitlements"),
        dependencies: isExcludedFramework ? dependencies : dependencies + externalDependencies,
        settings: settings
      )
      targets.append(target)
    }
    
    // Feature ๋ชจ๋“ˆ ๋ฐ๋ชจ ์•ฑ ํƒ€๊ฒŸ.
    if products.contains(.demoApp) {
      let appTarget: Target = .init(
        name: "\(name)DemoApp",
        platform: .iOS,
        product: .app,
        bundleId: "com.sideproj.\(name)DemoApp",
        deploymentTarget: .iOS(targetVersion: "15.0", devices: [.iphone]),
        infoPlist: .base(name: "\(name)DemoApp"),
        sources: ["Sources/**"],
        resources: ["Resources/**"],
        dependencies: [.target(name: name)],
        settings: settings
      )
      targets.append(appTarget)
      
      let scheme: Scheme = .init(
        name: "\(name)DemoApp",
        shared: true,
        hidden: false,
        buildAction: .init(targets: ["\(name)DemoApp"]),
        runAction: .runAction(executable: "\(name)DemoApp")
      )
      
      schemes.append(scheme)
    }
    
    // static, dynamic ํ”„๋ ˆ์ž„์›Œํฌ ํƒ€์ผ“.
    if products.filter({ $0.isFramework }).count != 0 {
      
      let frameworkTarget: Target = .init(
        name: name,
        platform: .iOS,
        product: products.contains(.framework(.static)) ? .staticFramework : .framework,
        bundleId: "com.sideproj.\(name)",
        deploymentTarget: .iOS(targetVersion: "15.0", devices: [.iphone]),
        infoPlist: infoPlist,
        sources: ["Sources/**"],
        resources: ["Resources/**"],
        dependencies: isExcludedFramework ? dependencies : dependencies + externalDependencies,
        settings: settings
      )
      targets.append(frameworkTarget)
    }
    
    // static, dynamic ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํƒ€์ผ“.
    if products.filter({ $0.isLibrary }).count != 0 {
      let target: Target = .init(
        name: name,
        platform: .iOS,
        product: products.contains(.library(.static)) ? .staticLibrary : .dynamicLibrary,
        bundleId: "com.sideproj.\(name)",
        deploymentTarget: .iOS(targetVersion: "15.0", devices: [.iphone]),
        infoPlist: infoPlist,
        sources: ["Sources/**"],
        resources: ["Resources/**"],
        dependencies: isExcludedFramework ? dependencies : dependencies + externalDependencies,
        settings: settings
      )
      targets.append(target)
    }
    
    // ์œ ๋‹›ํ…Œ์ŠคํŠธ ํƒ€์ผ“.
    if products.contains(.unitTests) {
      
      var dependencies: [TargetDependency] = [.target(name: name), .xctest]
      dependencies += testDependencies
      
      let target: Target = .init(
        name: "\(name)Tests",
        platform: .iOS,
        product: .unitTests,
        bundleId: "com.sideproj.\(name)Tests",
        infoPlist: .default,
        sources: ["\(name)Tests/**"],
        resources: ["\(name)Tests/**"],
        dependencies: dependencies
      )
      targets.append(target)
    }
    
    // UIํ…Œ์ŠคํŠธ ํƒ€์ผ“.
    if products.contains(.uiTests) {
      let target: Target = .init(
        name: "\(name)UITests",
        platform: .iOS,
        product: .uiTests,
        bundleId: "com.sideproj.\(name)UITests",
        sources: "\(name)UITests/**",
        dependencies: [.target(name: name)]
      )
      targets.append(target)
    }
    
    return Project(
      name: name,
      targets: targets,
      schemes: schemes
    )
  }
}

Project.swift

import Foundation
import ProjectDescription
import ProjectDescriptionHelpers

// CocoaPod ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ตœ์ดˆ install์‹œ ํ”„๋กœ์ ํŠธ ํŒŒ์ผ์ด ์—†๋Š”๊ฒฝ์šฐ ๋นŒ๋“œ์ œ์™ธ์‹œํ‚ค๊ธฐ ์œ„ํ•œ ํ™˜๊ฒฝ๋ณ€์ˆ˜.
let excludedFramework = ProcessInfo.processInfo.environment["TUIST_EXCLUEDED_FRAMEWORK"]
let isExcludedFramework = (excludedFramework == "TRUE")

let app = Project.feature(
  name: "App",
  bundleId: "com.sideproj.connect",
  products: [.app, .unitTests, .uiTests],
  isExcludedFramework: isExcludedFramework,
  infoExtension: [
    "LSApplicationQueriesSchemes": .array(
      [.string("kakaokompassauth"), .string("naversearchapp"), .string("naversearchthirdlogin")]
    ),
    "CFBundleURLTypes": .array([
      .dictionary([
        "CFBundleURLSchemes": .array(["connectIT"]),
        "CFBundleURLName": .string("connectIT")
      ]),
      .dictionary([
        "CFBundleURLSchemes": .array(["kakaoee72a7c08c0e36ae98010b8d02f646cf"])
      ])
    ]),
    "NMFClientId": .string("y5sse5c8he"),
    "NSLocationAlwaysAndWhenInUseUsageDescription": .string("์‚ฌ์šฉ์ž์˜ ์œ„์น˜๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค."),
    "NSLocationWhenInUseUsageDescription": .string("์‚ฌ์šฉ์ž์˜ ์œ„์น˜๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค."),
    "NSLocationAlwaysUsageDescription": .string("์‚ฌ์šฉ์ž์˜ ์œ„์น˜๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.")
  ],
  dependencies: [
    .project(target: "COFoundation", path: .relativeToRoot("Core/COFoundation")),
    .project(target: "COCommonUI", path: .relativeToRoot("UI/COCommonUI")),
    .project(target: "COThirdParty", path: .relativeToRoot("Core/COThirdParty")),
  ],
  externalDependencies: [
    .project(target: "Sign", path: .relativeToRoot("Features/Sign")),
    .xcframework(path: .CocoaPods.Framework.naverLogin),
    .xcframework(path: .CocoaPods.Framework.naverMaps)
  ]
)

Dependencies.swift

์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์˜์กด์„ฑ๊ด€๋ฆฌ ๋‚ด์šฉ ์—…๋ฐ์ดํŠธ ์ ์šฉ์‹œ tuist fetch ํ„ฐ๋ฏธ๋„ ๋ช…๋ น์–ด ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

import ProjectDescription

let dependencies = Dependencies(
  swiftPackageManager: [ // SPM ์‚ฌ์šฉ ์˜ˆ์‹œ
    .remote(
      url: "https://github.com/ReactorKit/ReactorKit.git", /// ์›๊ฒฉ ์ €์žฅ์†Œ ์ฃผ์†Œ์™€
      requirement: .upToNextMajor(from: "3.0.0")           /// ๋ฒ„์ „์„ ๊ธฐ์ž…ํ•œ๋‹ค.
    ),
    .remote(
      url: "https://github.com/SnapKit/SnapKit.git",
      requirement: .upToNextMajor(from: "5.0.1")
    )
  ],
  platforms: [.iOS]
)

ReactorKit

ReactorKit์€ ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ํŠน์ง•์ด๋ฉฐ, Reactor(ViewModel์˜ ์—ญํ• )์˜ ํ˜•ํƒœ๊ฐ€ ์ •ํ˜•ํ™” ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.

ํ•œ๋ฒˆ ์ฝ”๋“œํ๋ฆ„์„ ํŒŒ์•…ํ•ด๋‘๋ฉด ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž‘์„ฑํ•œ ์ฝ”๋“œ ์ฒ˜๋ฆฌ๋˜ํ•œ ๋Œ€๋ถ€๋ถ„ ๋™์ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ˜‘์—…์‹œ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž์˜ ์ฝ”๋“œ๋ฅผ ํŒŒ์•…ํ• ๋•Œ ์œ ๋ฆฌํ• ๊ฒƒ์œผ๋กœ ์ƒ๊ฐ๋˜์–ด ๋ฐ˜์˜ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

RxSwift์— ์ข…์†๋˜์–ด ์žˆ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ RxSwift๋Š” ์ž๋™์œผ๋กœ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

Flow

Flow๋Š” Action(View์— ์˜ํ•ด ์ „๋‹ฌ๋œ ์ด๋ฒคํŠธ) -> mutate(Action -> Mutation ๋ณ€๊ฒฝ) -> reduce(Mutation -> State๋กœ ์ฒ˜๋ฆฌ๋˜๋ฉฐ View์˜ ์ด๋ฒคํŠธ๋ฅผ Reactor๊ฐ€ ์ „๋‹ฌ๋ฐ›์•„ ๋ณ€๊ฒฝ๋œ State๋ฅผ View์—์„œ ๊ฐ์ง€ํ•˜์—ฌ ํ”„๋กœ๊ทธ๋ž˜๋ฐํ•˜๋Š” ๋ฐฉ์‹ ์ž…๋‹ˆ๋‹ค.

์•„๋ž˜ ๊ทธ๋ฆผ๊ณผ ์˜ˆ์‹œ์ฝ”๋“œ๋ฅผ ๊ฐ™์ด ์ฐธ๊ณ ํ•˜์‹œ๋Š”๊ฑธ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค!

์˜ˆ์‹œ์ฝ”๋“œ

ReactorKit์—์„œ ์ œ๊ณตํ•˜๋Š” ์ œ์ผ ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ ์ž…๋‹ˆ๋‹ค. ์ฐธ๊ณ  ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค. :link: Counter Example

Github ์ €์žฅ์†Œ :link: Github


Figma

https://www.figma.com/file/HIaDgCj3MSYDaExhvz8FBl/%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8?node-id=19%3A379