/iOS_News

๐Ÿ“ฐ ๋‰ด์Šค ๊ฒ€์ƒ‰ with MVP

Primary LanguageSwift

News App

Naver news ๊ฒ€์ƒ‰ API๋ฅผ ํ™œ์šฉํ•œ iOS ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜.

Description

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

Feature

  • ๊ธฐ์‚ฌ ๊ฒ€์ƒ‰ ๋ทฐ
    • ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๊ธฐ์‚ฌ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ
    • ํŽ˜์ด์ง€๋„ค์ด์…˜ - prefetching ๋ฐฉ์‹
    • refreshing ๊ธฐ๋Šฅ
  • ํƒœ๊ทธ ์„ค์ • ๋ทฐ
    • Compositional layout์„ ํ†ตํ•œ self-sizing CollectionView
    • delegate ํŒจํ„ด์„ ํ†ตํ•œ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
  • ๋””ํ…Œ์ผ ์›น ๋ทฐ
    • ์Šคํฌ๋žฉ ์ถ”๊ฐ€/์ œ๊ฑฐ
    • ์•กํ‹ฐ๋น„ํ‹ฐ ์ธ๋””์ผ€์ดํ„ฐ
    • WebView ๊ธฐ๋ฐ˜ ์—ฐ๊ฒฐ
    • ๋งํฌ ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ
  • ์Šคํฌ๋žฉ ๋ทฐ
    • ์Šคํฌ๋žฉ ๋ชฉ๋ก ๊ด€๋ฆฌ
    • ์Šคํฌ๋žฉ ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ

Getting Start

Swift, MVP, CI/CD, Unit Test, CoreData, WebKit, SnapKit, Alamofire, Toast-swift, TTGTagCollectionView

Issue

1. TableView reloadData()์—์„œ Section Header ์˜์—ญ ๋ฆฌ๋กœ๋“œ๋กœ ์ธํ•ด์„œ Tag๊ฐ€ ๊ณ„์† ์ถ”๊ฐ€์ ์œผ๋กœ ์ƒ์„ฑ๋˜๋Š” ์ƒํ™ฉ ๋ฐœ์ƒ

Header์˜ ๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ setup() ์‹คํ–‰์— ์žˆ์–ด์„œ delegate๋Š” ์œ ์ง€ํ•˜๊ณ  ์ถ”๊ฐ€์ ์ธ ์ƒ์„ฑ์€ ๋ถ„๊ธฐ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•ด ํ•ด๊ฒฐ, ์ด์ „ tags์˜ ์š”์†Œ ๋‚ด String ๊ฐ’์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์ด ์ œํ•œ์ ์ด๋ผ ๊ฐ€๋Šฅํ•œ ์ ‘๊ทผํ•˜์—ฌ ํ•ด๋‹น ๋ฐฐ์—ด์„ ์ƒ์„ฑํ•˜์—ฌ ๋น„๊ต ํ›„ ํƒœ๊ทธ ์„ค์ • ํ•˜๋„๋ก ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ

func setup(tags: [String], delegate: NewsListViewHeaderDelegate) {
        self.tags = tags
        self.delegate = delegate
        contentView.backgroundColor = .systemBackground
        setupLayout()
        let prevTags = tagCollectionView.allTags().compactMap { tag in
            "\(tag.content)"
                .replacingOccurrences(
                    of: "<TTGTextTagStringContent: self.text=",
                    with: ""
                )
                .replacingOccurrences(
                    of: ">",
                    with: ""
                )
        }
        if prevTags != tags {
            tagCollectionView.removeAllTags()
            setupTagCollectionView()
        }
    }

2. Test Coverage

Presenter์˜ unit test์— ์žˆ์–ด์„œ ์ตœ๋Œ€ํ•œ coverage๋ฅผ ๋งŒ์กฑ์‹œํ‚ฌ ๊ฒƒ์„ ๊ณ ๋ คํ•˜๊ณ  ํ•„์š”์— ๋”ฐ๋ผ BDD์ฒ˜๋Ÿผ ์กฐ๊ฑด ๋ถ„๊ธฐ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•˜์—ฌ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ๋ถ„๋ฆฌ

3. Bitrise ์‚ฌ์šฉ CI / CD ํ™˜๊ฒฝ ๊ตฌ์ถ• ์‹œ Secrets ๊ตฌ์„ฑ ์ด์Šˆ

๊ธฐ์กด์—๋Š” .xcconfig ํ™•์žฅ์ž์˜ ํŒŒ์ผ๋กœ ๊ด€๋ฆฌํ•˜๋ฉด์„œ API Key๋ฅผ ๊ด€๋ฆฌํ–ˆ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ, CI ํ™˜๊ฒฝ์„ ๊ตฌ์„ฑํ•˜๊ณ  ๋ฆฌ๋ชจํŠธ ๋นŒ๋“œ ๋˜๋Š” ํ™˜๊ฒฝ์—์„œ ํ•ด๋‹น ํŒŒ์ผ์€ .gitignore์— ์˜ํ•ด์„œ ์—…๋กœ๋”ฉ์ด ์ œํ•œ๋˜๊ณ  ๊ฒฐ๊ตญ์—๋Š” ๋นŒ๋“œ๊ฐ€ ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

์ด๋Ÿด ๊ฒฝ์šฐ, fastLane, github action, jenkins ๋“ฑ ๋‹ค๋ฅธ ์†”๋ฃจ์…˜๋“ค๋„ secrets์™€ env var๋ฅผ ์ œ๊ณตํ•˜๋ฉด์„œ ๋‚ด๋ถ€์—์„œ ๋ณด์•ˆ์ ์ธ ๊ฒฝ์šฐ๋ฅผ ํ•ด๊ฒฐ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑ์ด ๊ฐ€๋Šฅํ•˜๋‹ค๊ณ  ํ•œ๋‹ค.

๋”ฐ๋ผ์„œ, ๊ธฐ์กด์˜ ํ™œ์šฉํ•˜๋˜ .xcconfig๋ฅผ ๊ฑท์–ด๋‚ด๊ณ  ๊ฐ„๋‹จํ•˜๊ฒŒ bitrise์ƒ์—์„œ secrets๋ฅผ ๋ณ€์ˆ˜๋กœ ์„ค์ •ํ•˜๊ณ  ํ•ด๋‹น ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ฐ”๊พธ์–ด ์šฐ์„ ์€ repo์— commit ํ•ด๋‘์—ˆ๊ณ  remote build ์ƒ ๋ฌธ์ œ์—†์ด ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

4. CoreData๋ฅผ ์ด์šฉํ•œ ๊ธฐ์‚ฌ ์Šคํฌ๋žฉ ๊ด€๋ จ ๋ชจ๋ธ ๋ฌธ์ œ

๊ธฐ์กด์— ๋‹ค๋ฃจ๋˜ News ๊ตฌ์กฐ์ฒด์™€ CoreData์—์„œ ์“ฐ๋Š” ScrapedNews ๊ตฌ์กฐ์ฒด์˜ ํ˜•ํƒœ๋Š” ๋™์ผํ•˜๋‚˜ ๋‹ค๋ฅด๊ฒŒ ์“ธ ์ˆ˜ ๋ฐ–์— ์—†์—ˆ๋‹ค. ์™ธ๋ถ€์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ์—”ํ‹ฐํ‹ฐ์˜ ํ˜•ํƒœ์™€ ๋™์ผ ํ•˜๋”๋ผ๋„ ๋‚ด๋ถ€์˜ Database์— ์“ฐ์ธ๋‹ค๋Š” ์˜๋ฏธ๋Š” ์•„๋ฌด๋ž˜๋„ ๋ถˆ๋ณ€์„ฑ์— ๊ทผ๊ฑฐํ•˜์—ฌ ์ด๋ฅผ ์ž„์˜์ ์œผ๋กœ ์ˆ˜์ •ํ•˜๋Š” ๊ฑด ์ œํ•œํ•˜๋Š” ๊ฒƒ ๊ฐ™์•˜๋‹ค. ๋”ฐ๋ผ์„œ, ์„œ๋กœ ๋‹ค๋ฅธ ๋„ค์ด๋ฐ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ํ•„์š”์— ๋”ฐ๋ผ์„œ DB์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ์—”ํ‹ฐํ‹ฐ์˜ ๋ฐ์ดํ„ฐ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์˜คํžˆ๋ ค ์•ˆ์ „ํ•˜๋‹ค๊ณ  ์ƒ๊ฐ๋˜์–ด ์šฐ์„ ์€ ๋ถ„๋ฆฌํ•œ ์ฑ„ ์ž‘์—…์„ ์ง„ํ–‰ํ•ด๋‘์—ˆ๋‹ค.

CoreData ํ™œ์šฉ๊ณผ ์œ ๋‹› ํ…Œ์ŠคํŠธ

MockData๋ฅผ ๊ตฌ์„ฑํ•จ์— ์žˆ์–ด์„œ ๋ถ„๋ฆฌ๊ฐ€ ์žˆ๋‹ค๋ณด๋‹ˆ ๋งˆ์Œ๋Œ€๋กœ ์กฐ์ž‘๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ์ž…ํ•˜๊ธฐ๊ฐ€ ๊นŒ๋‹ค๋กœ์› ๋‹ค. ๊ฐ„๋‹จํ•˜๊ฒŒ ์ „๋‹ฌ๋˜๋Š” ํ•จ์ˆ˜๋งŒ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ ์‹ค์ œ๋กœ DB ๋‚ด๋ถ€์— ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ์ž…ํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ์‹์„ ๊ณ ๋ คํ•˜๊ฒŒ ๋˜๋ฉด์„œ ๊ธฐ์กด์— ์žˆ๋˜ ๊ฐ€๋ฒผ์šด ์ˆ˜์ค€์˜ Test Mock์„ ๋œ์–ด๋‚ด๊ณ  ๋‹ค์‹œ ๊ตฌ์„ฑ์„ ์‹œ์ž‘ํ–ˆ๋‹ค. ๊ตฌ์ฒด์ ์œผ๋กœ CoreData๋ฅผ ๊ฒฝ์œ ํ•˜์—ฌ News ๊ฐ’์„ ์ „๋‹ฌํ•˜๊ณ  ์‚ญ์ œํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ตฌ์„ฑํ•˜์˜€๋‹ค.

์ฐธ๊ณ  ๋ ˆํผ๋Ÿฐ์Šค
์ฐธ๊ณ  ๋ ˆํผ๋Ÿฐ์Šค ๋ ˆํฌ

5. LargeTitle ๊ด€๋ จ rightBarButton ์œ„์น˜ ๋ฌธ์ œ

Large ํƒ€์ดํ‹€์€ ์œ ์ง€ํ•˜๋ฉด์„œ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฒ„ํŠผ์˜ ์œ„์น˜๋ฅผ ์ ์ ˆํ•œ ๊ณณ์— ์œ„์น˜ ์‹œํ‚ค๊ณ  ์‹ถ์—ˆ์œผ๋‚˜, ํ™•์‹คํžˆ ์กฐ์ •ํ•˜๋Š” ๊ฒŒ ์ƒ๊ฐ๋ณด๋‹ค ์–ด๋ ค์› ๋‹ค. ์ผ๋ฐ˜์ ์ธ UI button์œผ๋กœ ๊ตฌ์„ฑํ•˜์—ฌ bar์— ์˜ฌ๋ฆฌ๋Š” ๋ฐฉ์‹์„ ์ถ”์ฒœํ•˜์—ฌ ์ƒ๋ช…์ฃผ๊ธฐ์— ๋”ฐ๋ผ์„œ ํ•ด๋‹น ๋ทฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค. ์˜คํ† ๋ ˆ์ด์•„์›ƒ์ด ์ƒ๊ฐ๋ณด๋‹ค ๊นŒ๋‹ค๋กญ๊ณ  ๋ฒ„ํŠผ์˜ ์‚ฌ์ด์ฆˆ์— ๊ด€๊ณ„ํ•˜์—ฌ ๋ทฐ ์ž์ฒด๊ฐ€ ๋ง๊ฐ€์ง€๋Š” ๊ฒฝ์šฐ๋„ ์ƒ๊ธฐ๋ฏ€๋กœ ์ฃผ์˜ํ•˜๋ฉด์„œ ์„ธํŒ…ํ•ด์•ผํ•  ๊ฑฐ ๊ฐ™์•˜๋‹ค.

6. ๋˜๋„๋ก ํด๋ฆฐํ•˜๊ฒŒ

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

๋˜ํ•œ ์™€๋‹ฟ์€ ๊ฑด DI์˜ ๋ถ„๋ฆฌ์™€ ํ™•์‹คํ•œ ๋ ˆ์ด์–ด ๊ตฌ๋ถ„์ด์—ˆ๋‹ค. Data ๋ ˆ์ด์–ด, ์—”ํ‹ฐํ‹ฐ ๋“ฑ ์„œ๋กœ๊ฐ€ ์ ˆ๋Œ€ ์•Œ์•„์„  ์•ˆ๋˜๋Š” ๊ฒฝ์šฐ์— ๋Œ€ํ•ด์„œ๋Š” ํ™•์‹คํ•˜๊ฒŒ ๊ตฌ๋ถ„ํ•˜๊ณ  ํ˜„์žฌ ๊ตฌ์„ฑํ•œ Presenter, View์˜ ๊ด€๊ณ„์— ์žˆ์–ด์„œ๋„ ์ฐธ์กฐ ๊ด€๊ณ„์— ๋Œ€ํ•ด์„œ๋„ ์—ญํ• ์„ ๋ช…ํ™•ํžˆ ํ•˜๋Š”๊ฒŒ ์ค‘์š”ํ–ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ตฌ์„ฑํ•˜๋ฉด์„œ ๋Š๋‚€ ๊ฑด๋ฐ View์—์„œ ํ™”๋ฉด ์ „ํ™˜ ๋กœ์ง์— ์žˆ์–ด์„œ ์˜์กด์„ฑ ์ฃผ์ž…์ด ์žˆ๋Š”๋ฐ ํ•ด๋‹น ๋ถ€๋ถ„์€ Cordinator, Container ๋“ฑ์œผ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์ง์ ‘์ ์œผ๋กœ View์—์„œ ๋ชจ๋ธ์„ ์ธ์ ์…˜ ํ•˜๋Š” ๊ฑด ๋งž์ง€ ์•Š์•„ ๋ณด์˜€๋‹ค.

7. ์›น ๋ทฐ์—์„œ ์ „๋‹ฌ๋œ ์ธํ„ฐ๋ ‰์…˜์˜ ๊ฒฐ๊ณผ๋กœ ๋„ค์ดํ‹ฐ๋ธŒ์˜ ์ธํ„ฐ๋ ‰์…˜์ด ๋™์ž‘ํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ œ

swift์˜ ๋Œ€๋ถ€๋ถ„์˜ UI ์š”์†Œ์—๋Š” UIResponder๊ฐ€ ์žˆ๋‹ค. ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ์ด๋ฒคํŠธ๋Š” ๋ฆฌ์Šคํฐ๋” ์ฒด์ธ์„ ํ†ตํ•ด ๋žฉํ•‘๋œ view๋กœ ์ „๋‹ฌ๋˜๊ฒŒ ๋˜๋Š”๋ฐ, ๋‚ด ์ƒ๊ฐ์— WKWebView๋Š” ์ฐฝ์ด ํ™œ์„ฑํ™”๋˜๋ฉด ๋ชจ๋“  ํ„ฐ์น˜ ์ด๋ฒคํŠธ๋ฅผ ํก์ˆ˜ํ•œ๋‹ค. ์•„๋ฌด๋ฆฌ ํ„ฐ์น˜ํ•ด๋„ ํ”„๋ฆฐํŠธ๊ฐ€ ๋„์ €ํžˆ ๋™์ž‘ํ•˜์ง€ ์•Š์•„ ๊ณ ๋ฏผํ•˜๋‹ค๊ฐ€ webView ๋‚ด์˜ ์ธํ„ฐ๋ ‰์…˜์„ ์‹œ๋„ํ•œ ํ›„์—์•ผ ์ •์ƒ์ ์œผ๋กœ ์ด๋ฒคํŠธ ์ „๋‹ฌ์ด ๋ฐœ์ƒํ–ˆ๋‹ค. ์ด ํ›„ ๋ฆฌ์Šคํฐ๋”๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ž‘์„ฑํ•˜๋ฏ€๋กœ์จ ํ•ด๊ฒฐ!

8. Compositional layout์„ ํ†ตํ•œ self-sizing

ScreenShot

IMG_C017886C83C0-1