/iOS_GIPHY

๐Ÿ“น GIPHY ๊ฒ€์ƒ‰ Rx with MVVM-C

Primary LanguageSwift

GYPHY Search App

GYPHY ๊ฒ€์ƒ‰ API๋ฅผ ํ™œ์šฉํ•œ iOS GIF ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜

Description

  • ์ตœ์†Œ ํƒ€๊ฒŸ : iOS 14.0
  • CleanArchitecture + MVVM-C ํŒจํ„ด ์ ์šฉ
  • CoreData ํ”„๋ ˆ์ž„์›Œํฌ ์‚ฌ์šฉ์œผ๋กœ ์ฆ๊ฒจ์ฐพ๊ธฐ ๋ชฉ๋ก ์œ ์ง€
  • Storyboard๋ฅผ ํ™œ์šฉํ•˜์ง€ ์•Š๊ณ  ์ฝ”๋“œ๋กœ๋งŒ UI ๊ตฌ์„ฑ
  • Pagination ๊ตฌํ˜„

Feature

  • ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ ๋ทฐ
    • ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ
    • ํŽ˜์ด์ง€๋„ค์ด์…˜ - prefetching ๋ฐฉ์‹
  • ๋””ํ…Œ์ผ ๋ทฐ
    • ์ฆ๊ฒจ์ฐพ๊ธฐ ์ถ”๊ฐ€/์ œ๊ฑฐ
    • ์•กํ‹ฐ๋น„ํ‹ฐ ์ธ๋””์ผ€์ดํ„ฐ
    • ๊ธฐ๊ธฐ ๋‚ด๋ถ€ ํŒŒ์ผ ์ €์žฅ
  • ์Šคํฌ๋žฉ ๋ทฐ
    • ์Šคํฌ๋žฉ ๋ชฉ๋ก ๊ด€๋ฆฌ
    • ์Šคํฌ๋žฉ ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ
    • Masonry layout

Getting Start

Swift, MVVM+C, CI/CD, Unit Test, CoreData, SnapKit, Alamofire, Toast-swift, RxCocoa, RxSwift, RxTest

Issue & Reflection

1. Coordinator, Router ๊ตฌ์„ฑ

๊ธฐ์กด ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ฝ”๋””๋„ค์ดํ„ฐ๋งŒ ๊ตฌ์„ฑํ•˜์—ฌ ํ™œ์šฉ์„ ํ•˜์˜€์œผ๋‚˜, ์ด๋ฅผ ๋ผ์šฐํ„ฐ๋„ ๋ถ„๋ฆฌํ•˜์—ฌ ์—ญํ• ์„ ๋ถ„๋‹ดํ•ด๋ณด๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋””๋„ค์ดํ„ฐ๋Š” ViewController ์ธ์Šคํ„ด์Šค๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ  present ๋˜๋Š” ์ˆœ์„œ(flow)๋ฅผ ๊ฒฐ์ •, ๋ผ์šฐํ„ฐ๋Š” ์‹ค์งˆ์ ์ธ present / dismiss ์‹คํ–‰ํ•˜๋ฉด์„œ ํ™”๋ฉด ์ „ํ™˜์˜ ๊ตฌ์„ฑ์„ ๋ชจ๋“ˆ๋ณ„๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ฐ์ž์˜ ์ฑ…์ž„์„ ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ตฌ์„ฑํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ, UITabBar์™€ ๊ฐ™์€ ์ „ํ™˜ ๊ด€๋ จ Basic Frame์ด ๋ผ์ด๋‹ค ๋ณด๋‹ˆ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒŒ ์–ด๋ ต๋‹ค๋Š” ํŒ๋‹จ์ด ๋“ค์—ˆ๊ณ , ์—ฌ๊ธฐ์— ๋Œ€ํ•œ ํ•ด๊ฒฐ์ฑ…์œผ๋กœ RIBs์— ๋Œ€ํ•ด์„œ ๋‹จ์ดˆ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ผ์šฐํ„ฐ์— ๊ธฐ๋ฐ˜ํ•˜์—ฌ ๋ทฐ๋ฅผ ๋ถ™์˜€๋‹ค ๋–ผ๋Š” ์•„ํ‚คํ…์ณ์™€ ๋”๋ถˆ์–ด ์ƒํƒœ ๊ด€๋ฆฌ์— ์žˆ์–ด์„œ๋„ ReactorKit์— ๊ธฐ๋ฐ˜ํ•˜์—ฌ ์ด๋ฅผ ์žฌ๊ตฌ์„ฑํ•  ๊ณ„ํš์ž…๋‹ˆ๋‹ค.

๊ตฌ์„ฑํ•˜๋ฉด์„œ ๋Š๋‚€ ์ ์€ ํ˜„์žฌ์™€ ๊ฐ™์ด UITabBar๋งŒ์œผ๋กœ ๊ตฌ์„ฑ๋œ ๋‹จ์ผํ•œ ์ƒํ™ฉ์—์„œ๋Š” modal ๋“ฑ ๋ถ€๋ชจ ์ฝ”๋””๋„ค์ดํ„ฐ์™€ ์ž์‹ ์ฝ”๋””๋„ค์ดํ„ฐ์˜ ๊ด€๊ณ„ ๋“ฑ์€ ๊ตฌ์ƒํ•˜์ง€ ์•Š์•„๋„ ๋˜์—ˆ๊ธฐ์— ๋น„๊ต์  ๋‹จ์ˆœํ•œ ๋ชจ์–‘์œผ๋กœ ๊ตฌ์„ฑ๋˜๊ฒŒ ๋˜์—ˆ์ง€๋งŒ, ๋งŒ์•ฝ ์ด๋ฅผ ํ™•์žฅํ•ด์„œ ํ™”๋ฉด ์ „ํ™˜ flow๊ฐ€ ๋ณต์žกํ•ด์ง„๋‹ค๋ฉด ํ•ด๋‹น ๋””์ž์ธ ํŒจํ„ด์„ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋”์šฑ ๋” ์ข‹์€ ๋ฐฉ์‹์œผ๋กœ ์œ ์ง€๋ณด์ˆ˜ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด๋ผ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

2. GIF Data Cache

GIF์˜ ์›€์ง์ด๋Š” ํ”„๋ ˆ์ž„ ๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ์œ„ํ•ด git repo์— ๊ณต๊ฐœ๋œ ์ƒ˜ํ”Œ ์†Œ์Šค์ฝ”๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ํ•ด๋‹น ์ฝ”๋“œ์—์„œ๋Š” ๋ฐ์ดํ„ฐ ์บ์‹ฑ์„ ๋”ฐ๋กœ ํ•ด์ฃผ์ง€ ์•Š์•„์„œ GIF ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ์ด ๋๋‚œ ๋ฐ์ดํ„ฐ๋„ ๋‚˜์ค‘์— ๋‹ค์‹œ ํ™•์ธํ•˜๋ฉด ๋กœ๋”ฉ์„ ๋˜ ๊ธฐ๋‹ค๋ ค์•ผํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๊ณผ์ •์— ์žˆ์–ด์„œ ๋ฉ”๋ชจ๋ฆฌ์˜ ์‚ฌ์šฉ๊ณผ ํ•จ๊ป˜ ์Šคํฌ๋กค ๋ฒ„๋ฒ…์ž„๋„ ์ƒ๋‹นํžˆ ์‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฌธ์ œํ•ด๊ฒฐ์„ ์œ„ํ•ด ํ•ด๋‹น ์†Œ์Šค์ฝ”๋“œ์— ์ด๋ฏธ์ง€ ์บ์‹ฑ ๊ธฐ๋Šฅ์„ ๋„ฃ์–ด์คฌ์Šต๋‹ˆ๋‹ค. ์‹ฑ๊ธ€ํ„ด์œผ๋กœ ๊ตฌ์„ฑํ•œ ์ด๋ฏธ์ง€ ์บ์‹ฑ ๋•๋ถ„์— ์ตœ์ดˆ์— ์ƒˆ๋กœ์šด GIF ๋ฐ์ดํ„ฐ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ๋งŒ ์Šคํฌ๋กค์ด ๋”œ๋ ˆ์ด๋˜๊ณ  ์ด ํ›„์—๋Š” ๋”œ๋ ˆ์ด ํ˜„์ƒ์ด ์—†์–ด์กŒ์Šต๋‹ˆ๋‹ค.

final class ImageCacheManager {
    static let shared = NSCache<NSString, UIImage>()

    private init() {}
}

์ฝœ๋ ‰์…˜ ๋ทฐ์˜ GIF ์ด๋ฏธ์ง€๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ  ์…€์„ ๊ทธ๋ฆฌ๋Š” ๊ณผ์ •์„ ๋ณธ๋ž˜๋Š” ๋ชจ๋‘ main ์Šค๋ ˆ๋“œ์—์„œ ์ฒ˜๋ฆฌํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ, ๊ทธ๋ฆฌ๋Š” ๋ฐ์— ํ•œ ๊ฐ€์ง€ ์Šค๋ ˆ๋“œ์—์„œ ๋ชจ๋“  ์ผ์„ ์ˆ˜ํ–‰ํ•˜๋‹ค ๋ณด๋‹ˆ ๋”œ๋ ˆ์ด๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, ์ถ”๊ฐ€์ ์œผ๋กœ setup์—์„œ ๋ฉ€ํ‹ฐ์“ฐ๋ ˆ๋”ฉ ์ฒ˜๋ฆฌ์„ ํ•ด์คŒ์œผ๋กœ์จ ์Šคํฌ๋กค ๋”œ๋ ˆ์ด๋ฅผ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

func setup(gifItem: GIFItem, indexPath: Int) {
        cellView.indicatorAction(bool: true)
        DispatchQueue.global().async { [weak self] in
            let image = UIImage.gifImageWithURL(gifItem.images.preview.url)
            DispatchQueue.main.async {
                self?.cellView.imageView.image = image
                self?.cellView.indicatorAction(bool: false)
            }
        }
    }

global(background) ์Šค๋ ˆ๋“œ์—์„œ gifImageWithURL() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์šฐ์„ ์ ์œผ๋กœ ์บ์‹ฑ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ณ  ํ•ด๋‹น URL์— ๋Œ€ํ•œ animatedImage ์ด๋ฏธ์ง€๋ฅผ ํ”„๋ ˆ์ž„์— ๋”ฐ๋ผ array ํ˜•ํƒœ๋กœ ๊ตฌ์„ฑํ•˜๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  main ์Šค๋ ˆ๋“œ๋กœ ํ•ด๋‹น view๋ฅผ ๊ทธ๋ฆฌ๋ฉด์„œ ์•ˆ์ •์ ์œผ๋กœ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ๋Š” SwiftGif ๋ ˆํฌ๋ฅผ ์ฐธ์กฐํ•˜์—ฌ ๊ตฌ์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

3. Rx In/Out ํ˜•์‹์˜ ViewModel ๊ตฌ์„ฑ ๋ฐ ๋ฉ”์„œ๋“œ ๋ถ„๋ฆฌ

protocol ViewModel {
    associatedtype Input
    associatedtype Output
    func transform(input: Input) -> Output
    var disposeBag: DisposeBag { get set }
}

ํŒจํ„ด์˜ ๊ฐ€์žฅ ํฐ ์ง€ํ–ฅ์ ์€ ๋ชจ๋“  ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ๋ฅผ ViewModel๋กœ ๋„˜๊ฒจ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ViewModel์—์„œ๋งŒ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ฒ„ํŠผ์˜ ํƒญ ์ด๋ฒคํŠธ, ํ…์ŠคํŠธํ•„๋“œ์˜ ์ž…๋ ฅ ์ด๋ฒคํŠธ ๋“ฑ์„ ์ „๋ถ€ Input์— ์ •์˜ํ•˜๊ณ  View๋กœ ๋„˜๊ฒจ์ค„ ๋ฐ์ดํ„ฐ๋“ค์„ Output์— ์ •์˜ํ•˜๋Š” ๊ฑธ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ์กด์˜ rx ๊ธฐ๋ฐ˜์œผ๋กœ protocol์„ ๊ตฌ์„ฑํ•˜๊ณ  convertํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜์˜€๋Š”๋ฐ ์ž‘์—…์„ ํ•˜๋ฉด์„œ ๊ธฐ์กด rx๋กœ ๋ž˜ํ•‘๋˜์–ด ๋ฐ”์ธ๋”ฉ๋œ ๊ฒƒ์ด ์•„๋‹Œ, BehaviorSubject ๋“ฑ์œผ๋กœ ์ด๋ฃจ์–ด์ง„ ๋ณ„๋„์˜ ์ŠคํŠธ๋ฆผ ํ”„๋กœํผํ‹ฐ๋“ค์ด viewModel ๋‚ด ์กด์žฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋ž˜ํ•‘์„ ์ถ”๊ฐ€์ ์œผ๋กœ ๊ตฌ์„ฑํ•˜์—ฌ Reactiveํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ๊ฒƒ์ธ์ง€, ์•„๋‹ˆ๋ฉด ReactorKit๊ณผ ๊ฐ™์€ ์ƒํƒœ ๊ด€๋ฆฌ ์„œ๋“œํŒŒํ‹ฐ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋‹ค์Œ ํ”„๋กœ์ ํŠธ์—์„œ ๊ฐœ์„ ํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ‘‰๐Ÿป ReactorKit์œผ๋กœ ๋‹จ๋ฐฉํ–ฅ ๋ฐ˜์‘ํ˜• ์•ฑ ๋งŒ๋“ค๊ธฐ

4. ์ฝ”์–ด ๋ฐ์ดํ„ฐ์˜ immutable ํ•œ ๊ฐ์ฒด

@objc(GIFItem_CoreData)
public class GIFItem_CoreData: NSManagedObject, Codable {
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        do {
            try container.encode(id ?? "", forKey: .id)
            try container.encode(type ?? "", forKey: .type)
            try container.encode(webPageURL ?? "", forKey: .webPageURL)
            try container.encode(title ?? "", forKey: .title)
            try container.encode(images, forKey: .images)
            try container.encode(user, forKey: .user)
            try container.encode(isFavorite, forKey: .isFavorite)
        } catch {
            print("error")
        }
    }

    required convenience public init(from decoder: Decoder) throws {
        guard let contextUserInfoKey = CodingUserInfoKey.context,
              let managedObjectContext = decoder.userInfo[contextUserInfoKey] as? NSManagedObjectContext,
              let entity = NSEntityDescription.entity(forEntityName: "GIFItem_CoreData", in: managedObjectContext)
        else {
            fatalError("decode failure")
        }

        self.init(entity: entity, insertInto: managedObjectContext)
        let values = try decoder.container(keyedBy: CodingKeys.self)

        do {
            type = try values.decode(String.self, forKey: .type)
            id = try values.decode(String.self, forKey: .id)
            webPageURL = try values.decode(String.self, forKey: .webPageURL)
            title = try values.decode(String.self, forKey: .title)
            images = try values.decode(GIFCategory_CoreData.self, forKey: .images)
            user = try values.decode(UserData_CoreData.self, forKey: .user)
            isFavorite = try values.decode(Bool.self, forKey: .isFavorite)
        } catch {
            print ("error")
        }
    }

    func convertToGIFItem() -> GIFItem {
        return GIFItem(
            type: self.type!,
            id: self.id!,
            webPageURL: self.webPageURL!,
            title: self.title!,
            images: GIFCategory(
                original: GIFSize(
                    height: (self.images?.original?.height)!,
                    width: (self.images?.original?.width)!,
                    size: (self.images?.original?.size)!,
                    url: (self.images?.original?.url)!
                ),
                preview: GIFSize(
                    height: (self.images?.preview?.height)!,
                    width: (self.images?.preview?.width)!,
                    size: (self.images?.preview?.size)!,
                    url: (self.images?.preview?.url)!
                )
            ),
            user: UserData(
                avatarURL: (self.user?.avatarURL)!,
                name: (self.user?.name)!
            ),
            isFavorite: self.isFavorite
        )
    }

    enum CodingKeys: String, CodingKey {
        case type = "type"
        case id = "id"
        case webPageURL = "webPageURL"
        case title = "title"
        case images = "images"
        case user = "user"
        case isFavorite = "isFavorite"
    }
}

extension GIFItem_CoreData {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<GIFItem_CoreData> {
        return NSFetchRequest<GIFItem_CoreData>(entityName: "GIFItem_CoreData")
    }

    @NSManaged public var type: String?
    @NSManaged public var id: String?
    @NSManaged public var isFavorite: Bool
    @NSManaged public var webPageURL: String?
    @NSManaged public var title: String?
    @NSManaged public var user: UserData_CoreData?
    @NSManaged public var images: GIFCategory_CoreData?
}

extension GIFItem_CoreData : Identifiable {}

๊ธฐ์กด์˜ CoreData๋ฅผ ํ™œ์šฉํ•˜๋ฉด์„œ ๊ฐ์ฒด๋ฅผ ๋‹จ์ˆœํžˆ NSManagedObject๋กœ ์ „ํ™˜ํ•˜์—ฌ ์ €์žฅํ•˜๋Š” ์šฉ๋„๋กœ ํ™œ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ์—๋Š” ๋‚˜์•„๊ฐ€ ๊นŠ๊ฒŒ ๊ณ„์ธตํ™”๋œ JSON ๊ฐ์ฒด๋ฅผ ํŒŒ์‹ฑํ•˜๋Š” ๊ฒƒ๊ณผ NSManagedObject์˜ relationship์„ ๊ตฌ์ฒดํ™”ํ•˜์—ฌ ์›ํ•˜๋Š” ํ˜•ํƒœ๋กœ์˜ ๊ฐ์ฒด๋ฅผ ์ €์žฅํ•ด๋ณด๊ณ ์ž ํ•˜์˜€๊ณ , ๊ฐœ๋ณ„์ ์ธ class์™€ ๊ทธ property๋ฅผ ๊ตฌ์กฐํ™”ํ•˜์—ฌ ๊ธฐ์กด์˜ ๋กœ์ง์—์„œ๋„ ๋ฌด๋ฆฌ์—†์ด ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

Realm๊ณผ ๋น„๊ตํ•ด์„œ ์•„์‰ฌ์› ๋˜ ์ ์€ ๋ณ„๋„์˜ NSManagedObject ๊ฐ์ฒด๋Š” ์ˆ˜์ •์ด ๋ฒˆ๊ฑฐ๋กญ๋‹ค๋Š” ์ ๊ณผ ๊ตณ์ด ๋น„์Šทํ•œ ํ˜•ํƒœ์˜ ๊ฐ์ฒด๋“ค์„ ๋ณ„๋„๋กœ ์ถ”์ƒํ™”ํ•˜์—ฌ ๊ฐ์ž์˜ useCase ์˜์—ญ์—์„œ ํ™œ์šฉํ•˜๋Š” ์ ์—์„œ ๋น„ํšจ์œจ์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ, immutableํ•˜๊ฒŒ ๊ฐ์ฒด๋ฅผ ์ €์žฅํ•œ๋‹ค๋Š” ์ ์—์„œ ๊ธฐ๋ฐ€์„ฑ๊ณผ ์ˆ˜์ •์ด ์–ด๋ ต๋‹ค๋Š” ์ ์€ ๋ณด์•ˆ์ ์ธ ์ธก๋ฉด์—์„œ ๋ฉ”๋ฆฌํŠธ๊ฐ€ ์žˆ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๋ฌผ๋ก  Cloud๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋ถ€๋ถ„๋„ ํ™•์‹คํ•œ ์žฅ์ ์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉํ•˜๋Š” ์ธก๋ฉด์—์„œ ๊ณ ๋ คํ•˜์—ฌ DB๋ฅผ ๋ฌด์—‡์„ ์‚ฌ์šฉํ• ์ง€๋Š” ํ•ญ์ƒ ๊ณ ๋ฏผ๋˜๋Š” ์  ์ž…๋‹ˆ๋‹ค.

5. ํด๋ฆฐ ์•„ํ‚คํ…์ณ

๊ตฌ์ฒด์ ์œผ๋กœ ํด๋ฆฐ ์•„ํ‚คํ…์ณ๋ผ๋Š” ๊ฐœ๋…์€ ํ™•์‹คํ•˜๊ฒŒ ์ฒด๊ฐํ•  ์ˆ˜์ค€์€ ์•„๋‹ˆ์—ˆ์ง€๋งŒ ํ™•์‹คํžˆ ๋ ˆ์ด์–ด์˜ ๊ตฌ๋ถ„๊ณผ ์š”์ฒญ์— ๋”ฐ๋ฅธ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ์€ ์™€๋‹ฟ๋Š” ๋ถ€๋ถ„์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  Network ๋ ˆ์ด์–ด์—์„œ ๋ฐ›์€ ๊ฐ์ฒด๋ฅผ ๊ทธ๋Œ€๋กœ ์“ฐ๋Š” ๊ฒŒ ์•„๋‹Œ ํ•„์š”์— ๋”ฐ๋ผ ์š”์ฒญํ•˜๊ณ  ์›ํ•˜๋Š” ํ˜•ํƒœ๋กœ ๊ฐ€๊ณตํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์€ ๋ฐ์ดํ„ฐ์˜ ๋ณดํ˜ธ์™€ ๋”๋ถˆ์–ด ์บก์Šํ™”์— ์žˆ์–ด์„œ ๋งŽ์€ ์ด์ ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฆฌ์•กํ‹ฐ๋ธŒํ•œ ๊ตฌ์„ฑ๊ณผ ํ•ฉ์ณ์กŒ์„ ๋•Œ์™€ ํŒŒ๊ธ‰๋ ฅ๋„ ๊ทธ๋ ‡๊ณ  ๋งŽ์ด ๋ถ€์กฑํ•œ Clean์ด์ง€๋งŒ ์ฐจ์ธฐ์ฐจ์ธฐ ์•„ํ‚คํ…์ณ๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ  ํด๋”๋ง๊ณผ ๋”๋ถˆ์–ด ์ฃผ์ž…๊ณผ ๋ถ„๋ฆฌ์˜ ๊ฐœ๋…์„ ์ดํ•ดํ•ด๋‚˜๊ฐ€๋Š” ๊ฑฐ ๊ฐ™์Šต๋‹ˆ๋‹ค ์‹ค์ œ๋กœ ๋ชจ๋“ˆํ™”๋ฅผ ํ†ตํ•œ ๋ถ„๋ฆฌ๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ๊ณ„์† ํ•ด๋ด์•ผ๊ฒ ์Šต๋‹ˆ๋‹ค

ScreenShot