Swift 기반의 푸드 딜리버리 앱 구축 전략 🍔🚀
안녕하세요, 미래의 앱 개발자 여러분! 오늘은 정말 흥미진진한 주제로 여러분과 함께 할 예정입니다. 바로 Swift를 사용하여 푸드 딜리버리 앱을 만드는 전략에 대해 알아볼 거예요. 🍕📱
여러분, 배달 음식을 주문해 본 적 있나요? 아마 대부분이 "네!"라고 대답하실 것 같아요. 요즘은 스마트폰 하나로 맛있는 음식을 집 앞까지 배달받을 수 있는 세상이 되었죠. 그런데 이런 편리한 앱은 어떻게 만들어질까요? 오늘 우리는 그 비밀을 파헤쳐볼 거예요!
Swift라는 프로그래밍 언어를 들어보셨나요? 애플이 만든 이 강력한 언어로 우리는 iOS 기기에서 동작하는 멋진 앱을 만들 수 있어요. 그리고 오늘 우리가 만들 앱은 바로 여러분이 좋아하는 그 배달 앱! 😋
자, 이제 우리의 흥미진진한 여정을 시작해볼까요? 푸드 딜리버리 앱 개발의 세계로 함께 떠나봅시다!
🌟 흥미로운 사실: 여러분이 알고 계신 재능넷(https://www.jaenung.net)과 같은 재능 공유 플랫폼에서는 앱 개발 관련 재능을 거래할 수 있어요. 어쩌면 여러분이 만든 푸드 딜리버리 앱 개발 노하우를 공유하며 수익을 올릴 수도 있겠죠?
1. Swift와 친해지기: 우리의 새로운 친구 🤝
자, 여러분! Swift라는 이름을 들어보셨나요? 혹시 테일러 스위프트(Taylor Swift)를 떠올리셨다면, 음... 조금 방향을 바꿔야 할 것 같아요. 😅 우리가 이야기할 Swift는 가수가 아니라 프로그래밍 언어예요!
Swift는 2014년에 애플이 만든 프로그래밍 언어로, iOS, macOS, watchOS, tvOS 앱을 개발하는 데 사용됩니다. 그럼 왜 Swift를 사용해야 할까요? 여기 몇 가지 이유를 소개해드릴게요:
- 🚀 빠른 성능: Swift는 이름처럼 정말 '빠릅니다'. C언어만큼 빠른 성능을 자랑해요.
- 🔒 안전성: Swift는 타입 안전(Type-safe) 언어로, 많은 프로그래밍 오류를 미리 방지할 수 있어요.
- 😊 읽기 쉬운 문법: 다른 언어에 비해 배우기 쉽고 읽기 쉬운 문법을 가지고 있어요.
- 🍎 애플의 전폭적인 지원: 애플이 직접 만들고 지원하는 언어이기 때문에, iOS 개발에 최적화되어 있어요.
Swift를 처음 접하는 분들을 위해, 간단한 "Hello, World!" 프로그램을 만들어볼까요?
print("Hello, World!")
정말 간단하죠? 이렇게 Swift는 복잡한 것을 단순하게 만들어주는 마법 같은 언어랍니다! 🎩✨
💡 꿀팁: Swift를 배우기 시작했다면, 애플의 공식 Swift 책 "The Swift Programming Language"를 꼭 읽어보세요. 무료로 제공되며, Swift의 기초부터 고급 기능까지 모두 다루고 있어요.
자, 이제 우리는 Swift라는 새로운 친구를 만났어요. 이 친구와 함께 우리의 푸드 딜리버리 앱 여행을 떠나볼까요? 다음 섹션에서는 우리 앱의 기본 구조를 설계해볼 거예요. 준비되셨나요? Let's Swift! 🚀
2. 앱 구조 설계: 우리의 푸드 딜리버리 앱 청사진 그리기 📐
여러분, 집을 지을 때 설계도 없이 바로 벽돌을 쌓기 시작하면 어떻게 될까요? 아마도 삐뚤빼뚤한 집이 되겠죠? 앱 개발도 마찬가지예요. 우리의 푸드 딜리버리 앱을 만들기 전에 먼저 전체적인 구조를 설계해야 해요. 이것을 '앱 아키텍처'라고 부른답니다. 🏗️
우리의 푸드 딜리버리 앱은 다음과 같은 주요 기능들을 가질 거예요:
- 🍽️ 레스토랑 목록 보기
- 🍕 메뉴 선택하기
- 🛒 장바구니에 담기
- 💳 주문 및 결제하기
- 🚚 배달 현황 추적하기
이런 기능들을 효과적으로 구현하기 위해, 우리는 MVVM(Model-View-ViewModel) 아키텍처를 사용할 거예요. MVVM이 뭔지 모르겠다고요? 걱정 마세요! 제가 쉽게 설명해드릴게요.
MVVM은 앱을 세 가지 주요 부분으로 나눕니다:
- Model (모델): 앱의 데이터와 비즈니스 로직을 담당해요. 예를 들면, 레스토랑 정보, 메뉴 항목, 주문 데이터 등이 여기에 해당돼요.
- View (뷰): 사용자에게 보이는 UI 부분이에요. 레스토랑 목록, 메뉴 화면, 장바구니 등 모든 화면이 뷰에 해당해요.
- ViewModel (뷰모델): 뷰와 모델 사이의 중개자 역할을 해요. 뷰에 표시될 데이터를 준비하고, 사용자의 입력을 처리해요.
이렇게 나누면 무슨 장점이 있을까요?
- 🧩 모듈화: 각 부분을 독립적으로 개발하고 테스트할 수 있어요.
- 🔄 재사용성: 같은 모델이나 뷰모델을 다른 뷰에서 재사용할 수 있어요.
- 🧪 테스트 용이성: 특히 뷰모델은 UI 없이도 테스트하기 쉬워요.
- 🎨 UI와 로직의 분리: 디자이너와 개발자가 각자의 영역에 집중할 수 있어요.
자, 이제 우리 앱의 큰 그림을 그렸어요. 이 구조를 바탕으로 각 부분을 하나씩 개발해 나갈 거예요. 예를 들어, 레스토랑 목록을 보여주는 부분은 이렇게 구성될 수 있어요:
- Model: Restaurant 구조체 (이름, 주소, 평점 등의 정보 포함)
- ViewModel: RestaurantListViewModel (레스토랑 목록을 가져오고 필터링하는 로직 포함)
- View: RestaurantListView (실제로 화면에 레스토랑 목록을 표시)
이런 식으로 앱의 각 기능을 MVVM 구조에 맞춰 설계하고 구현해 나갈 거예요.
🌟 재능넷 팁: 앱 구조 설계는 매우 중요한 단계예요. 만약 이 부분에서 도움이 필요하다면, 재능넷에서 경험 많은 iOS 개발자의 조언을 구해보는 것도 좋은 방법이에요. 전문가의 insight는 여러분의 프로젝트를 한 단계 업그레이드시킬 수 있답니다!
우리의 앱 청사진이 완성되었어요! 이제 이 설계를 바탕으로 실제 코딩을 시작할 준비가 되었습니다. 다음 섹션에서는 우리 앱의 핵심 기능 중 하나인 레스토랑 목록 화면을 구현해볼 거예요. 코딩의 세계로 뛰어들 준비 되셨나요? Let's code! 💻✨
3. 레스토랑 목록 구현: 맛집을 한눈에! 👀🍽️
자, 이제 우리 앱의 첫 번째 주요 기능인 레스토랑 목록을 만들어볼 거예요. 사용자들이 가장 먼저 보게 될 화면이니 정말 중요하죠! 맛있는 음식점들을 어떻게 보여줄지 함께 고민해봐요. 🤔
먼저, 우리의 MVVM 구조에 따라 Model, ViewModel, View를 차례대로 만들어볼게요.
1. Model: Restaurant 구조체 만들기 🏠
레스토랑의 정보를 담을 구조체를 만들어봅시다.
struct Restaurant: Identifiable {
let id = UUID()
let name: String
let cuisine: String
let rating: Double
let imageURL: String
}
여기서 Identifiable 프로토콜을 채택한 이유는 뭘까요? SwiftUI에서 리스트를 만들 때 각 항목을 구분할 수 있는 고유 ID가 필요해요. Identifiable을 사용하면 SwiftUI가 자동으로 id 프로퍼티를 인식하고 사용합니다. 편리하죠? 😎
2. ViewModel: RestaurantListViewModel 클래스 만들기 🧠
이제 레스토랑 목록을 관리할 ViewModel을 만들어봐요.
import Combine
class RestaurantListViewModel: ObservableObject {
@Published var restaurants: [Restaurant] = []
func fetchRestaurants() {
// 여기서 실제로는 네트워크 요청을 통해 데이터를 가져올 거예요.
// 지금은 예시 데이터로 채워볼게요.
restaurants = [
Restaurant(name: "맛있는 피자", cuisine: "이탈리안", rating: 4.5, imageURL: "pizza_image_url"),
Restaurant(name: "정통 한식당", cuisine: "한식", rating: 4.8, imageURL: "korean_food_image_url"),
Restaurant(name: "신선한 초밥", cuisine: "일식", rating: 4.3, imageURL: "sushi_image_url")
]
}
}
@Published 속성 래퍼를 사용한 이유가 궁금하신가요? 이렇게 하면 restaurants 배열이 변경될 때마다 SwiftUI가 자동으로 화면을 업데이트해줘요. 마법 같죠? ✨
3. View: RestaurantListView 만들기 👁️
마지막으로, 사용자에게 보여질 실제 화면을 만들어봐요.
import SwiftUI
struct RestaurantListView: View {
@StateObject private var viewModel = RestaurantListViewModel()
var body: some View {
NavigationView {
List(viewModel.restaurants) { restaurant in
RestaurantRow(restaurant: restaurant)
}
.navigationTitle("맛집 리스트 🍽️")
.onAppear {
viewModel.fetchRestaurants()
}
}
}
}
struct RestaurantRow: View {
let restaurant: Restaurant
var body: some View {
HStack {
AsyncImage(url: URL(string: restaurant.imageURL)) { image in
image.resizable()
} placeholder: {
ProgressView()
}
.frame(width: 50, height: 50)
.cornerRadius(8)
VStack(alignment: .leading) {
Text(restaurant.name)
.font(.headline)
Text(restaurant.cuisine)
.font(.subheadline)
.foregroundColor(.gray)
}
Spacer()
Text(String(format: "%.1f", restaurant.rating))
.foregroundColor(.orange)
.font(.headline)
}
}
}
와우! 우리가 만든 첫 번째 화면이에요. 🎉 여기서 몇 가지 흥미로운 점을 살펴볼까요?
- @StateObject를 사용해 ViewModel의 수명주기를 View에 맞췄어요.
- NavigationView로 감싸서 나중에 상세 페이지로의 이동을 준비했어요.
- AsyncImage를 사용해 이미지를 비동기적으로 로드해요. 네트워크 상태가 안 좋아도 앱이 멈추지 않아요!
🍳 요리 팁: 실제 앱에서는 이미지 URL을 하드코딩하지 않고, 서버에서 받아오는 것이 좋아요. 또한, 이미지 캐싱을 구현하면 앱의 성능을 크게 향상시킬 수 있어요!
이제 우리의 레스토랑 목록 화면이 완성되었어요! 사용자들은 이 화면을 통해 다양한 맛집들을 한눈에 볼 수 있게 되었죠. 하지만 아직 끝이 아니에요. 다음 단계에서는 이 목록에 몇 가지 흥미로운 기능을 추가해볼 거예요.
4. 추가 기능: 검색과 필터링 🔍
사용자들이 원하는 레스토랑을 더 쉽게 찾을 수 있도록 검색 기능과 요리 종류별 필터링 기능을 추가해볼까요?
먼저 ViewModel에 새로운 메서드들을 추가해봐요:
class RestaurantListViewModel: ObservableObject {
@Published var restaurants: [Restaurant] = []
@Published var searchText: String = ""
@Published var selectedCuisine: String?
var filteredRestaurants: [Restaurant] {
restaurants.filter { restaurant in
(searchText.isEmpty || restaurant.name.lowercased().contains(searchText.lowercased())) &&
(selectedCuisine == nil || restaurant.cuisine == selectedCuisine)
}
}
func selectCuisine(_ cuisine: String?) {
selectedCuisine = cuisine
}
// ... 기존 코드 ...
}
이제 View를 업데이트해볼까요?
struct RestaurantListView: View {
@StateObject private var viewModel = RestaurantListViewModel()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $viewModel.searchText)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
CuisineFilterButton(title: "전체", isSelected: viewModel.selectedCuisine == nil) {
viewModel.selectCuisine(nil)
}
CuisineFilterButton(title: "한식", isSelected: viewModel.selectedCuisine == "한식") {
viewModel.selectCuisine("한식")
}
CuisineFilterButton(title: "일식", isSelected: viewModel.selectedCuisine == "일식") {
viewModel.selectCuisine("일식")
}
CuisineFilterButton(title: "이탈리안", isSelected: viewModel.selectedCuisine == "이탈리안") {
viewModel.selectCuisine("이탈리안")
}
}
.padding()
}
List(viewModel.filteredRestaurants) { restaurant in
RestaurantRow(restaurant: restaurant)
}
}
.navigationTitle("맛집 리스트 🍽️")
.onAppear {
viewModel.fetchRestaurants()
}
}
}
}
struct SearchBar: View {
@Binding var text: String
var body: some View {
HStack {
TextField("레스토랑 검색", text: $text)
.padding(7)
.padding(.horizontal, 25)
.background(Color(.systemGray6))
.cornerRadius(8)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 8)
}
)
}
.padding(.horizontal)
}
}
struct CuisineFilterButton: View {
let title: String
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(isSelected ? Color.blue : Color.gray.opacity(0.2))
.foregroundColor(isSelected ? .white : .black)
.cornerRadius(20)
}
}
}
우와! 우리의 앱이 점점 더 멋져지고 있어요! 🎨✨ 이제 사용자들은 원하는 레스토랑을 쉽게 찾을 수 있게 되었어요. 검색창에서 레스토랑 이름을 입력하거나, 요리 종류별로 필터링할 수 있죠.
💡 개발자 꿀팁: 성능 최적화를 위해 List 대신 LazyVStack을 사용할 수도 있어요. 레스토랑 목록이 매우 길어질 경우, LazyVStack은 화면에 보이는 항목만 렌더링하므로 메모리 사용량을 줄일 수 있답니다.
여기까지 오느라 수고 많으셨어요! 우리는 이제 멋진 레스토랑 목록 화면을 가지게 되었어요. 사용자들은 이 화면을 통해 다양한 맛집들을 쉽게 찾아볼 수 있게 되었죠. 하지만 우리의 여정은 여기서 끝나지 않아요. 다음 단계에서는 레스토랑 상세 정보 화면을 만들어볼 거예요. 맛있는 메뉴들을 구경할 준비 되셨나요? 🍕🍣🍜
그리고 잊지 마세요! 개발 과정에서 어려움을 겪는다면, 재능넷에서 다른 개발자들의 도움을 받을 수 있어요. 함께 배우고 성장하는 것, 그것이 바로 개발의 묘미랍니다! 💪😊
4. 레스토랑 상세 정보 화면: 맛있는 메뉴의 세계로! 🍽️🌟
자, 이제 우리의 앱에서 가장 맛있는(?) 부분을 만들어 볼 차례예요! 바로 레스토랑 상세 정보 화면이죠. 사용자들이 레스토랑을 선택하면 볼 수 있는 이 화면은 메뉴, 가격, 영업 시간 등 더 자세한 정보를 제공할 거예요. 맛있는 코드를 작성해볼까요? 😋💻
1. Model: Menu 구조체 추가하기 🍔
먼저, 메뉴 정보를 담을 새로운 구조체를 만들어봐요.
struct MenuItem: Identifiable {
let id = UUID()
let name: String
let description: String
let price: Double
let imageURL: String
}
// Restaurant 구조체에 메뉴 정보 추가
struct Restaurant: Identifiable {
let id = UUID()
let name: String
let cuisine: String
let rating: Double
let imageURL: String
let address: String
let openingHours: String
let menu: [MenuItem]
}
여기서 Restaurant 구조체에 address, openingHours, menu를 추가했어요. 이제 레스토랑에 대한 더 많은 정보를 저장할 수 있게 되었죠!
2. ViewModel: RestaurantDetailViewModel 만들기 🧠
이제 레스토랑 상세 정보를 관리할 ViewModel을 만들어볼게요.
import Combine
class RestaurantDetailViewModel: ObservableObject {
@Published var restaurant: Restaurant
init(restaurant: Restaurant) {
self.restaurant = restaurant
}
var formattedPrice: (Double) -> String {
return { price in
return String(format: "₩%.0f", price)
}
}
}
formattedPrice 클로저를 사용해 가격을 예쁘게 포맷팅했어요. 이렇게 하면 1000.0 같은 숫자가 "₩1,000"처럼 보기 좋게 표시돼요.
3. View: RestaurantDetailView 만들기 👁️
드디어 우리의 맛있는 상세 화면을 만들 시간이에요!
import SwiftUI
struct RestaurantDetailView: View {
@StateObject var viewModel: RestaurantDetailViewModel
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
// 레스토랑 이미지
AsyncImage(url: URL(string: viewModel.restaurant.imageURL)) { image in
image.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
ProgressView()
}
.frame(height: 200)
.clipped()
// 레스토랑 정보
VStack(alignment: .leading, spacing: 10) {
Text(viewModel.restaurant.name)
.font(.title)
.fontWeight(.bold)
HStack {
Text(viewModel.restaurant.cuisine)
Spacer()
Text("⭐ \(String(format: "%.1f", viewModel.restaurant.rating))")
}
.font(.subheadline)
.foregroundColor(.gray)
Text(viewModel.restaurant.address)
Text("영업시간: \(viewModel.restaurant.openingHours)")
}
.padding(.horizontal)
// 메뉴 섹션
Text("메뉴")
.font(.title2)
.fontWeight(.bold)
.padding(.horizontal)
ForEach(viewModel.restaurant.menu) { item in
MenuItemView(item: item, formattedPrice: viewModel.formattedPrice)
}
}
}
.navigationTitle("상세 정보")
.navigationBarTitleDisplayMode(.inline)
}
}
struct MenuItemView: View {
let item: MenuItem
let formattedPrice: (Double) -> String
var body: some View {
HStack(spacing: 15) {
AsyncImage(url: URL(string: item.imageURL)) { image in
image.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
ProgressView()
}
.frame(width: 80, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 10))
VStack(alignment: .leading, spacing: 5) {
Text(item.name)
.font(.headline)
Text(item.description)
.font(.subheadline)
.foregroundColor(.gray)
.lineLimit(2)
Text(formattedPrice(item.price))
.font(.subheadline)
.fontWeight(.bold)
}
}
.padding(.horizontal)
}
}
와우! 정말 맛있어 보이는 화면이 완성되었어요! 🎉 이 화면에서 몇 가지 흥미로운 점을 살펴볼까요?
- ScrollView를 사용해 긴 내용을 스크롤할 수 있게 했어요.
- AsyncImage로 레스토랑과 메뉴 이미지를 비동기적으로 로드해요.
- ForEach로 모든 메뉴 항목을 동적으로 표시해요.
🍳 요리 팁: 실제 앱에서는 이미지 캐싱을 구현하면 좋아요. 같은 이미지를 반복해서 다운로드하지 않아도 되니 앱의 성능이 훨씬 좋아질 거예요!
4. 네비게이션 연결하기 🔗
이제 레스토랑 목록에서 상세 화면으로 이동할 수 있게 만들어볼까요? RestaurantListView를 다음과 같이 수정해주세요:
struct RestaurantListView: View {
@StateObject private var viewModel = RestaurantListViewModel()
var body: some View {
NavigationView {
VStack {
// ... 기존 코드 ...
List(viewModel.filteredRestaurants) { restaurant in
NavigationLink(destination: RestaurantDetailView(viewModel: RestaurantDetailViewModel(restaurant: restaurant))) {
RestaurantRow(restaurant: restaurant)
}
}
}
.navigationTitle("맛집 리스트 🍽️")
.onAppear {
viewModel.fetchRestaurants()
}
}
}
}
NavigationLink를 사용해 레스토랑 목록의 각 항목을 탭하면 해당 레스토랑의 상세 정보 화면으로 이동하게 만들었어요.
이제 우리의 푸드 딜리버리 앱이 정말 그럴듯해졌어요! 사용자들은 맛있어 보이는 레스토랑 목록을 둘러보고, 마음에 드는 레스토랑을 선택해 더 자세한 정보를 볼 수 있게 되었죠. 🍽️✨
💡 개발자 꿀팁: 앱의 사용성을 높이기 위해 레스토랑 상세 화면에 '주문하기' 버튼을 추가하는 것은 어떨까요? 이 버튼을 누르면 장바구니 화면으로 이동하게 만들 수 있어요. 다음 단계에서 이 기능을 구현해볼 거예요!
여기까지 정말 수고 많으셨어요! 우리는 이제 멋진 레스토랑 목록 화면과 상세 정보 화면을 가진 앱을 만들었어요. 하지만 우리의 여정은 여기서 끝나지 않아요. 다음 단계에서는 장바구니 기능과 주문 프로세스를 구현해볼 거예요. 맛있는 음식을 주문할 준비 되셨나요? 🛒🍕🍣
그리고 기억하세요! 개발 과정에서 어려움을 겪거나 새로운 아이디어가 필요하다면, 재능넷에서 다른 개발자들과 소통해보는 것도 좋은 방법이에요. 함께 배우고 성장하는 것, 그것이 바로 개발의 즐거움이랍니다! 💪😊
5. 장바구니와 주문 프로세스: 맛있는 주문의 완성! 🛒🍽️
드디어 우리 앱의 핵심 기능을 만들 시간이 왔어요! 사용자가 음식을 선택하고 주문할 수 있는 장바구니와 주문 프로세스를 구현해볼 거예요. 이 기능들로 우리 앱은 진정한 푸드 딜리버리 앱으로 거듭나게 될 거예요. 준비되셨나요? 맛있는 코드를 작성해봅시다! 😋💻
1. Model: CartItem과 Order 구조체 만들기 🧺
먼저, 장바구니 아이템과 주문을 위한 새로운 구조체를 만들어볼게요.
struct CartItem: Identifiable {
let id = UUID()
let menuItem: MenuItem
var quantity: Int
}
struct Order: Identifiable {
let id = UUID()
let items: [CartItem]
let totalPrice: Double
let restaurantName: String
let orderDate: Date
var status: OrderStatus = .pending
}
enum OrderStatus: String {
case pending = "준비중"
case preparing = "조리중"
case onTheWay = "배달중"
case delivered = "배달완료"
}
CartItem은 장바구니에 담긴 각 메뉴 항목을, Order는 최종 주문을 나타내요. OrderStatus로 주문 상태를 추적할 수 있어요.
2. ViewModel: CartViewModel 만들기 🧠
이제 장바구니를 관리할 ViewModel을 만들어볼게요.
import Combine
class CartViewModel: ObservableObject {
@Published var items: [CartItem] = []
@Published var restaurant: Restaurant?
var totalPrice: Double {
items.reduce(0) { $0 + $1.menuItem.price * Double($1.quantity) }
}
func addToCart(menuItem: MenuItem, from restaurant: Restaurant) {
if self.restaurant == nil {
self.restaurant = restaurant
} else if self.restaurant?.id != restaurant.id {
// 다른 레스토랑의 메뉴를 추가하려고 할 때
// 여기서 경고를 표시하거나 처리할 수 있어요
return
}
if let index = items.firstIndex(where: { $0.menuItem.id == menuItem.id }) {
items[index].quantity += 1
} else {
items.append(CartItem(menuItem: menuItem, quantity: 1))
}
}
func removeFromCart(menuItem: MenuItem) {
items.removeAll { $0.menuItem.id == menuItem.id }
if items.isEmpty {
restaurant = nil
}
}
func updateQuantity(for menuItem: MenuItem, quantity: Int) {
if let index = items.firstIndex(where: { $0.menuItem.id == menuItem.id }) {
items[index].quantity = max(0, quantity)
if items[index].quantity == 0 {
items.remove(at: index)
}
}
if items.isEmpty {
restaurant = nil
}
}
func clearCart() {
items.removeAll()
restaurant = nil
}
func placeOrder() -> Order {
let order = Order(items: items, totalPrice: totalPrice, restaurantName: restaurant?.name ?? "", orderDate: Date())
clearCart()
return order
}
}
이 ViewModel은 장바구니에 아이템을 추가하고, 제거하고, 수량을 업데이트하는 등의 모든 기능을 관리해요. placeOrder() 메서드는 주문을 완료하고 장바구니를 비우는 역할을 해요.
3. View: CartView 만들기 👁️
이제 사용자가 장바구니를 볼 수 있는 화면을 만들어볼게요.
import SwiftUI
struct CartView: View {
@ObservedObject var viewModel: CartViewModel
@State private var showingOrderConfirmation = false
@State private var order: Order?
var body: some View {
NavigationView {
List {
if let restaurant = viewModel.restaurant {
Section(header: Text(restaurant.name)) {
ForEach(viewModel.items) { item in
CartItemRow(item: item, viewModel: viewModel)
}
}
Section {
HStack {
Text("총 주문금액")
Spacer()
Text("₩\(viewModel.totalPrice, specifier: "%.0f")")
.fontWeight(.bold)
}
}
Section {
Button(action: {
order = viewModel.placeOrder()
showingOrderConfirmation = true
}) {
Text("주문하기")
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
} else {
Text("장바구니가 비어있습니다.")
.foregroundColor(.gray)
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("장바구니")
.sheet(isPresented: $showingOrderConfirmation) {
if let order = order {
OrderConfirmationView(order: order)
}
}
}
}
}
struct CartItemRow: View {
let item: CartItem
@ObservedObject var viewModel: CartViewModel
var body: some View {
HStack {
Text(item.menuItem.name)
Spacer()
Stepper("\(item.quantity)", value: Binding(
get: { self.item.quantity },
set: { viewModel.updateQuantity(for: item.menuItem, quantity: $0) }
), in: 0...99)
Text("₩\(item.menuItem.price * Double(item.quantity), specifier: "%.0f")")
}
}
}
struct OrderConfirmationView: View {
let order: Order
var body: some View {
VStack(spacing: 20) {
Image(systemName: "checkmark.circle.fill")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(.green)
Text("주문이 완료되었습니다!")
.font(.title)
.fontWeight(.bold)
Text("주문번호: \(order.id)")
Text("레스토랑: \(order.restaurantName)")
Text("총 금액: ₩\(order.totalPrice, specifier: "%.0f")")
Text("주문 상태: \(order.status.rawValue)")
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(10)
}
.padding()
}
}
우와! 정말 맛있어 보이는 장바구니 화면이 완성되었어요! 🎉 이 화면에서 몇 가지 흥미로운 점을 살펴볼까요?
- Stepper를 사용해 메뉴 항목의 수량을 쉽게 조절할 수 있어요.
- sheet를 사용해 주문 확인 화면을 모달로 표시해요.
- Section을 사용해 정보를 깔끔하게 구분했어요.
💡 개발자 꿀팁: 실제 앱에서는 주문 정보를 서버에 저장하고, 주문 상태 업데이트를 실시간으로 받아오는 기능을 구현하면 좋아요. 푸시 알림을 통해 사용자에게 주문 상태 변경을 알려줄 수도 있죠!
4. 레스토랑 상세 화면에 '장바구니에 담기' 버튼 추가하기 🛒
이제 레스토랑 상세 화면에서 메뉴를 장바구니에 담을 수 있게 만들어볼게요. RestaurantDetailView를 다음과 같이 수정해주세요:
struct RestaurantDetailView: View {
@StateObject var viewModel: RestaurantDetailViewModel
@ObservedObject var cartViewModel: CartViewModel
@State private var showingCart = false
var body: some View {
ScrollView {
// ... 기존 코드 ...
ForEach(viewModel.restaurant.menu) { item in
MenuItemView(item: item, formattedPrice: viewModel.formattedPrice) {
cartViewModel.addToCart(menuItem: item, from: viewModel.restaurant)
}
}
}
.navigationTitle("상세 정보")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
Button(action: { showingCart = true }) {
Image(systemName: "cart")
.badge(cartViewModel.items.count)
}
)
.sheet(isPresented: $showingCart) {
CartView(viewModel: cartViewModel)
}
}
}
struct MenuItemView: View {
let item: MenuItem
let formattedPrice: (Double) -> String
let addToCart: () -> Void
var body: some View {
HStack(spacing: 15) {
// ... 기존 코드 ...
Button(action: addToCart) {
Image(systemName: "plus.circle.fill")
.foregroundColor(.blue)
}
}
.padding(.horizontal)
}
}
장바구니 아이콘을 네비게이션 바에 추가하고, 각 메뉴 항목에 '담기' 버튼을 추가했어요. 이제 사용자들은 쉽게 메뉴를 장바구니에 담고 주문할 수 있어요!
축하드려요! 🎉 우리는 이제 완전한 기능을 갖춘 푸드 딜리버리 앱을 만들었어요. 사용자들은 레스토랑을 둘러보고, 메뉴를 선택하고, 장바구니에 담아 주문할 수 있게 되었죠. 정말 맛있는 앱이 되었어요! 🍔🍕🍣
🍳 요리 팁: 실제 앱에서는 결제 기능, 주문 히스토리, 리뷰 시스템 등을 추가하면 더욱 완성도 높은 앱이 될 거예요. 또한, 백엔드 서버와의 연동, 실시간 주문 추적 등의 기능도 고려해볼 수 있어요!
여기까지 정말 수고 많으셨어요! 우리는 Swift와 SwiftUI를 사용해 멋진 푸드 딜리버리 앱을 만들었어요. 이 과정에서 여러분은 MVVM 아키텍처, 상태 관리, 네비게이션, 비동기 이미지 로딩 등 다양한 iOS 앱 개발 기술을 배웠을 거예요.
앱 개발은 여기서 끝이 아니에요. 계속해서 새로운 기능을 추가하고, 사용자 피드백을 반영하며 앱을 개선해 나가는 것이 중요해요. 그리고 기억하세요! 개발 과정에서 어려움을 겪거나 새로운 아이디어가 필요하다면, 재능넷에서 다른 개발자들과 소통해보는 것도 좋은 방법이에요. 함께 배우고 성장하는 것, 그것이 바로 개발의 즐거움이랍니다! 💪😊
여러분의 앱 개발 여정에 행운이 함께하기를 바랄게요. 맛있는 코딩 되세요! 🍽️💻