๋ฆฌํํ ๋ง์ SUWIKI ๋๋ ํฐ๋ฆฌ์์ ์งํ์ค์
๋๋ค.
App Store - SUWIKI
- UI
- ๋ด๋น ๊ธฐ๋ฅ
- ์ฌ์ฉ ๊ธฐ์
- ๊ฐ๋ฐ์ผ์ง ๋ฐ ํธ๋ฌ๋ธ ์ํ
- 3,600๋ช ์ ์ฌ์ฉ ์ ์ , ๋ค์ด๋ก๋ ์ฝ 6,700ํ, ์ ๋ฐ์ดํธ 16ํ
- iOS 1์ธ ๊ฐ๋ฐ ๋ฐ ๋์์ธ ์งํ, ๊ธฐํ ์ฐธ์ฌ
- ๋๋ ๋ฐ์ดํฐ ์ ์ฅ ์๋ ์ฝ 3์ด๋์์ 0.6์ด ๋ฏธ๋ง์ผ๋ก ๊ฐ์ (์ฝ 83%)
- 1,000์ค ์ด์์ ์คํ๊ฒํฐ ์ฝ๋ ์ฝ 240์ค๋ก ๊ฐ์
- ์ํคํ ์ฒ๊ฐ ์๋ ๊ตฌ์กฐ์์ MVC, ํ์ฌ๋ MVVM + Clean Architecture ์ ์ฉ ์งํ ์ค
- 2๋ ๊ฐ ์ด์ํ๋ฉฐ ์ฝ 12๋ง์ค ์ด์์ ์ฝ๋ ์์ฑ
ํผ์ณ๋ณด๊ธฐ
- ์ฑ ๊ตฌ์กฐ ์ค๊ณ - Clean Architecture, MVVM
- ์๊ฐํ, ์๊ฐํ ์์ ฏ ๊ตฌํ(Core Data, Firebase)
- ๊ฐ์ํ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ(Alamofire, Network Layer ์ค๊ณ)
- ํ ํฐ ์ธํฐ์ ํฐ ๊ตฌํ
- UIKit(Storyboard & Code Base), SwiftUI, SnapKit, Then, WidgetKit
- Swift Concurrency, Actor, Combine
- Core Data, Firebase
- Alamofire, JWT, Clean Architecture, MVVM
- ์ ์ ๋ ๊ฐ์๋ช ์ ๋ชจ๋ฅผ ๊ฒฝ์ฐ ๊ฐ์๋ฅผ ์ถ๊ฐํ๊ธฐ ์ด๋ ค์ด UX ๋ฐ์
ํด๊ฒฐ๋ฐฉ์
- ํ๊ณผ๋ฅผ ์ฐ์ ์ ํํ ์ ์๋๋ก UI ์์ , ์ ์ ์ ํ๊ณผ ์ฆ๊ฒจ์ฐพ๊ธฐ ๊ธฐ๋ฅ ์ถ๊ฐ
- ๊ฐ์, ๊ต์๋ช
๊ฒ์์ผ๋ก ๊ฐ์ ๊ฒ์ ๊ฐ๋ฅ. ๋์ด์ฐ๊ธฐ ๊ณ ๋ คํ ๋ฐ์ดํฐ ํํฐ๋ง ๊ธฐ๋ฅ ๊ตฌํ
- iOS์ UX์๋ ๋๋จ์ด์ง UI / UX, ๊ฐ์ ์ ๋ณด ์์ ์ ๋ถํ์ํ UX ๋ฐ์
ํด๊ฒฐ๋ฐฉ์
- ์ ํ์ UX์ ์ ์ฌํ ํํ๋ก UI ์์
- ๊ฐ์ ์ ๋ณด ์์ ๊ธฐ๋ฅ ์ญ์
๊ธฐ์กด SUWIKI ์ฝ๋์ ๋ฌธ์
- ์คํ ๋ฆฌ๋ณด๋ ์์ฃผ์ UI ๊ตฌํ
- ๋ณ๋์ ์ํคํ ์ฒ ํจํด์ด ์ ์ฉ๋์ง ์์
- ํ ํ์ผ์ ์ฝ๋๊ฐ 1์ฒ์ค์ด ๋์ด๊ฐ๊ณ , ๋น์ทํ ๊ธฐ๋ฅ์ ํจ์๊ฐ ์ฌ๋ฌ๊ฐ ์ ์๋จ
- ์ฝ๋๊ฐ์ ๊ฒฐํฉ๋๊ฐ ๋๊ณ ์์ง๋๊ฐ ๋ฎ์ ์ฌ์ฌ์ฉ์ฑ์ด ๋จ์ด์ง
ํ์ฌ
- UI๋ SwiftUI์ UIKit(Code Base)์ ์ฌ์ฉํ์ฌ ๊ตฌํ ์ค
- ๋ฐ์ดํฐ ์ ์ฅ ๊ณต๊ฐ์ Realm์์ Core Data๋ก ์ ํ
- MVC ํจํด์ ์ ์ฉํ์์ผ๋, ViewController์ ์ฝ๋๊ฐ ๋งค์ฐ ๊ธธ์ด์ง๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ณ ์ MVVM์ ์ ์ฉ
- SwiftUI์ UIKit์ MVVM ํจํด ๊ธฐ๋ฐ์ผ๋ก ๊ตฌํ ์งํ ์ค
- ๋ฐ์ดํฐ ์ ์ฅ ๊ณต๊ฐ์ ์ ํ๊ณผ ๋ค๋ฅธ UI ํ๋ ์์ํฌ์ ๋์ํ๊ณ ์ ํด๋ฆฐ ์ํคํ ์ฒ๋ฅผ ์ ์ฉํ์ฌ ๋์์ค
App Group ์ ์ฉ ์ด์
- ๊ทธ๋ฆผ๊ณผ ๊ฐ์ด, Core Data์ ์ ์ฅ๋๋ ๋ฐ์ดํฐ๋ฅผ App๊ณผ Widget Extension์์ ๊ณต์ ๋์ด์ผ ํจ
- ๊ธฐ์กด ๋ฐฉ์์ Core Data๋ App ๋ด๋ถ์์๋ง ์ ๊ทผ์ด ๊ฐ๋ฅ
์ ์ฉ ๋ฐฉ์
- Core Data์ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ฅผ ์์ ฏ์์๋ ์ ๊ทผํ ์ ์์
- ์ ํ๋ ์๊ฐํ์ ID๋ฅผ ์ถ์ ํ๊ธฐ ์ํด UserDefaults ๋ํ App Group์ ์ ์ฉํจ
func saveFirebaseCourse(course: [[String: Any]]) throws {
try deleteFirebaseCourse()
guard let entity = NSEntityDescription.entity(forEntityName: "FirebaseCourse", in: context) else {
throw CoreDataError.entityError
}
let batchInsertRequest = NSBatchInsertRequest(entity: entity, objects: course)
if let fetchResult = try? context.execute(batchInsertRequest),
let batchInsertResult = fetchResult as? NSBatchInsertResult,
let success = batchInsertResult.result as? Bool,
success {
return
}
print(CoreDataError.batchInsertError.localizedDescription)
}
๋ฐ์ดํฐ ์ ์ฅ ์ ๋ฐ์ํ๋ ๋ฌธ์ ์ ์
- ๋งค ํ๊ธฐ๋ง๋ค Firebase์ ์ ์ฅ๋ 2,000์ฌ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ Core Data์ ์ ์ฅํด์ผํจ
- ๊ธฐ์กด์ ๋ฐฉ์์ ๋ฐ์ดํฐ์ ๊ฐฏ์๋งํผ ๋ฐ๋ณตํ์ฌ ์ ์ฅํ์์ผ๋ ๋ฌธ์ ๋ ๋ค์๊ณผ ๊ฐ์
- ๋ฐ์ดํฐ ์ ์ฅ ์ ์ฝ 3์ด ์์
- ๊ฐํ์ ์ผ๋ก ์๋ฌ ๋ฐ์(Cocoa 132001). ๋๋์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋ณต๋ฌธ์ ํตํด ์ฒ๋ฆฌํ๋ ๊ณผ์ ์์ context์ entity๋ฅผ ๊ณ์ ์ ๊ทผํ๊ธฐ ๋๋ฌธ์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค๊ณ ์ถ๋ก
๋ฐ๋ผ์ NSBatchInsertRequest๋ฅผ ์ ์ฉํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐ
- ๋ฐ์ดํฐ ์ ์ฅ์ ์ฝ 0.5์ด ์์
- ๋จ์ผ ํธ์ถ๋ง ์คํ ํ๋ ์์ ์ฌ๋ผ๊ฐ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์๊ตฌ ์ ์ฅ์์ ์ฝ์ ํ ์ ์๊ธฐ์ ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ฑ ์ฆ๊ฐ
class APIProvider {
static func request<T: Decodable>(
_ object: T.Type,
target: TargetType
) async throws -> T {
return try await AlamofireManager
.shared
.session
.request(target)
.serializingDecodable()
.value
}
}
/// func search: ๊ฐ์ํ๊ฐ๋ฅผ ๊ฒ์ํ ํ, ๊ฒ์ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ์์ ๋ด๋ ค๋ฐ์ต๋๋ค.
/// ๋ฌดํ์คํฌ๋กค ๊ธฐ๋ฅ์ ์ํด ํ์ด์ง๊ฐ 1์ผ ๊ฒฝ์ฐ search ๋ฐ์ดํฐ๋ก ์ด๊ธฐํ, ์๋ ๊ฒฝ์ฐ append ํฉ๋๋ค.
func search() async throws {
guard !searchText.isEmpty else { return }
do {
if self.searchPage == 1 {
searchLecture = try await searchUseCase.excute(searchText: searchText,
option: option,
page: searchPage,
major: major)
await MainActor.run {
lecture = searchLecture
}
} else {
let searchData = try await searchUseCase.excute(searchText: searchText,
option: option,
page: searchPage,
major: major)
await MainActor.run {
searchLecture.append(contentsOf: searchData)
lecture.append(contentsOf: searchData)
}
}
} catch {
fatalError(error.localizedDescription)
}
}
responseDecodable๊ณผ serializingDecodable์ ์ฐจ์ด
- ํ์ฌ ์ฝ๋ ์์ฑ ์ completion handler๋ฅผ ์ง์ํ๊ณ async / await๋ฅผ ์งํฅํ๋ ์ฝ๋๋ฅผ ์์ฑ ์ค
- responseDecodable ๋์ serializingDecodable์ ์ฌ์ฉ(serializingDecodable์ DataTask ๋ฐํ)
- responseDecodable์ ํธ๋ค๋ฌ๊ฐ ๋ฉ์ธ ์ค๋ ๋์์ ํธ์ถ๋์ง๋ง, serializngDecodable์ ์ค๋ ๋ ๊ด๋ฆฌ๊ฐ ๋ฐ๋ก ๋์ง ์์(responseDecodable์ ๋ด๋ถ์ ์ผ๋ก ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๊ธฐ๋ฅ ์คํ ์์น๊ฐ ๋ฉ์ธํ๋ก ์ง์ ๋์ด ์์)
ํด๊ฒฐ ๋ฐฉ์
- ํธ์ถ๋ถ ๋ฉ์๋ ์ ์ฒด๊ฐ ์๋ ๋ฐํ๋ ๋ฐ์ดํฐ์ Main Actor ์ ์ฉํ์ฌ ํ๋กํผํฐ ์ ๋ฐ์ดํธ
func getDetailPage(){
let url = "https://api.kr"
let headers: HTTPHeaders = [
"Authorization" : String(keychain.get("AccessToken") ?? "")
]
AF.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: BaseInterceptor()).validate().responseJSON { (response) in
let data = response.value
let json = JSON(data ?? "")["data"]
let totalAvg = String(format: "%.1f", round(json["lectureTotalAvg"].floatValue * 1000) / 1000)
let totalSatisfactionAvg = String(format: "%.1f", round(json["lectureSatisfactionAvg"].floatValue * 1000) / 1000)
let totalHoneyAvg = String(format: "%.1f", round(json["lectureHoneyAvg"].floatValue * 1000) / 1000)
let totalLearningAvg = String(format: "%.1f", round(json["lectureLearningAvg"].floatValue * 1000) / 1000)
let detailLectureData = detailLecture(id: json["id"].intValue, semesterList: json["semesterList"].stringValue, professor: json["professor"].stringValue, majorType: json["majorType"].stringValue, lectureType: json["lectureType"].stringValue, lectureName: json["lectureName"].stringValue, lectureTotalAvg: totalAvg, lectureSatisfactionAvg: totalSatisfactionAvg, lectureHoneyAvg: totalHoneyAvg, lectureLearningAvg: totalLearningAvg, lectureTeamAvg: json["lectureTeamAvg"].floatValue, lectureDifficultyAvg: json["lectureDifficultyAvg"].floatValue, lectureHomeworkAvg: json["lectureHomeworkAvg"].floatValue)
self.detailLectureArray.append(detailLectureData)
self.lectureViewUpdate()
}
}
- ๋์ผํ๊ฒ ์๋ํ๋ ๋ฉ์๋๋ฅผ ๋งค๋ฒ ์์ฑํ๋ ๋ฌธ์ ๋ฐ์
- DTO๋ฅผ ๋ฐ๋ก ์ ์ํ์ง ์๊ณ , SwiftyJSON์ ์ฌ์ฉํด์ ๋งค๋ฒ ํ๋์ฉ ํ์ฑํจ
- ์ฝ๋์ ์ฌ์ฌ์ฉ์ฑ, ๊ฐ๋ ์ฑ์ด ๋งค์ฐ ๋จ์ด์ง
- ๋ฉ์๋์์ URL์ด ๋ ธ์ถ๋จ
extension DTO {
struct DetailLectureResponse: Codable {
/// ๊ฐ์ ID
let id: Int
/// ํด๋น ๊ฐ์ ๋
๋ + ํ๊ธฐ -> "2021-1,2022-1"
let semesterList: String
/// ๊ต์๋ช
let professor: String
/// ๊ฐ์ค ํ๊ณผ
let majorType: String
/// ์ด์ ๊ตฌ๋ถ
let lectureType: String
/// ๊ฐ์๋ช
let lectureName: String
/// ๊ฐ์ํ๊ฐ ํ๊ท ์ง์
let lectureTotalAvg: Float
/// ๊ฐ์ํ๊ฐ ๋ง์กฑ๋ ์ง์
let lectureSatisfactionAvg: Float
/// ๊ฐ์ํ๊ฐ ๊ฟ๊ฐ ์ง์
let lectureHoneyAvg: Float
/// ๊ฐ์ํ๊ฐ ๋ฐฐ์ ์ง์
let lectureLearningAvg: Float
/// ๊ฐ์ํ๊ฐ ํํ ์ง์
let lectureTeamAvg: Float
/// ๊ฐ์ํ๊ฐ ๋์ด๋ ์ง์
let lectureDifficultyAvg: Float
/// ๊ฐ์ํ๊ฐ ๊ณผ์ ์ง์
let lectureHomeworkAvg: Float
}
}
extension APITarget.Lecture {
var targetURL: URL {
URL(string: APITarget.baseURL + "lecture")!
}
var method: Alamofire.HTTPMethod {
switch self {
case .getHome:
return .get
case .search:
return .get
case .detail:
return .get
}
}
var path: String {
switch self {
case .getHome:
"/all"
case .search:
"/search"
case .detail:
""
}
}
var parameters: RequestParameter {
switch self {
case let .getHome(allLectureRequest):
return .query(allLectureRequest)
case let .search(searchLectureRequest):
return .query(searchLectureRequest)
case let .detail(detailLectureRequest):
return .query(detailLectureRequest)
}
}
}
func fetchDetail(
id: Int
) async throws -> DetailLecture {
let target = APITarget.Lecture.detail(
DTO.DetailLectureRequest(lectureId: id)
)
let dtoDetailLecture = try await APIProvider.request(
DTO.DecodingDetailLectureResponse.self,
target: target
)
return dtoDetailLecture.detailLecture.entity
}
- ๋คํธ์ํฌ ๊ณ์ธต ์ค๊ณ
- ์ถ์ํ๋ฅผ ํตํ ๊ณตํต๋ ๊ธฐ๋ฅ๋ค์ ์ธํฐํ์ด์ค๋ก ์ ์, ์ค๋ณต ๊ธฐ๋ฅ์ ํ์ฅ์ผ๋ก ๊ตฌํ
- ํ์ฅ์ฑ๊ณผ ์ฌ์ฌ์ฉ์ฑ์ ๊ณ ๋ คํ DTO ์ค๊ณ
- ํด๋ฆฐ์ํคํ ์ฒ์ ๋ฐ์ดํฐ ํ๋ฆ์ ์งํฅ
๋ฌธ์ ์ํฉ
- ๊ธฐ์กด์ ์๊ฐํ ์ค๋ณต ๊ฒ์ฆ ๋ก์ง์ ์ฝ ์ฒ ์ค๊ฐ๋์ ์ฝ๋๋ก ๋ณต์กํ๊ฒ ๊ตฌํ๋์ด ์์
- ๋๋ถ๋ถ์ ๋ฒ๊ทธ๊ฐ ์๊ฐํ ์ค๋ณต ๊ฒ์ฆ์ด ์ ์์ ์ผ๋ก ์ด๋ฃจ์ด์ง์ง ์์ ๋ฒ๊ทธ ํด๊ฒฐ์ด ์ด๋ ค์
- ํฌ๋กค๋ง ํ์ฌ ์ป์ด์ค๋ ์๊ฐํ ๋ฐ์ดํฐ๊ฐ ๊ท์น์ ์ด์ง ์์ ๋ฌธ์
- ๊ฐ๋ ์ฑ์ด ๋งค์ฐ ๋จ์ด์ง๋ ์ฝ๋
ํด๊ฒฐ ๋ฐฉ์
- ์๊ฐํ ์ค๋ณต ๊ฒ์ฆ ํด๋์ค ์์ฑ
- ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ฉ์๋ ์ฌ์ฉ, ์ค๋ณต ์ฝ๋ ์ต์ํ๋ก ๊ฐ๋ ์ฑ ์ฆ๊ฐ
- 2์ฒ์ฌ๊ฐ์ ์๊ฐํ ๋ฐ์ดํฐ์ ์ผ์ด์ค ์ ์, ์ผ์ด์ค ๋ณ๋ก ์๊ฐํ ์ค๋ณต ๊ฒ์ฆ ๋ก์ง์ ๋์ํจ
(์ผ๋ฐ ๊ฐ์ - ํ๋์ ๊ฐ์์ ํ๋์ ๊ฐ์์ค, ๊ฐ์ ์๊ฐ 1 : ๊ฐ์์ค N, ๊ฐ์์ค 1 : ๊ฐ์ ์๊ฐ N, ์จ๋ผ์ธ ๊ฐ์) - ์ฝ 240์ค๋ก ๊ฐ์
- ํด๊ฒฐํด๋ณด๊ธฐ!