안녕하세요!
오늘은 쇼핑몰 앱 코드 뜯어보기 두번째 시간입니다 ㅎㅎ
홈 화면을 구성하는 코드를 가져와봤는데요,
하나씩 뜯어보면서 차근차근 공부해 보아요~~

홈 화면은 카드 형식으로 제품리스트를 구성하여
카드 코드와 리스트 코드를 분리해서 작업했는데요!
오늘은 리스트 코드 먼저 정리했습니다 :)
[product_list.dart 파일 전체 코드]
import 'package:flutter/material.dart';
import '../models/product.dart';
import '../widgets/product_card.dart';
import 'product_detail_page.dart';
import 'product_create_page.dart';
import 'product_shopping_cart.dart';
class ProductListPage extends StatefulWidget {
const ProductListPage({super.key});
@override
State<ProductListPage> createState() => _ProductListPageState();
}
class _ProductListPageState extends State<ProductListPage> {
// 임시 상품 데이터
List<Product> products = [
const Product(
name: 'Creeper Twin Baby Epoxy Case',
price: 35000,
imageUrl: 'https://img.29cm.co.kr/item/202501/11efce3938a527d2ad1b2dc32f03e40b.jpg?width=700&format=webp',
description: '에폭시 범퍼 케이스',
),
const Product(
name: 'MINT & WHITE Case',
price: 29000,
imageUrl: 'https://img.29cm.co.kr/item/202501/11efce5619910b2cad1bcfec99e7dd75.jpg?width=700&format=webp',
description: '폴리카보네이트 소재의 케이스',
),
const Product(
name: '(B) Love, Romantic, Success Case',
price: 35000,
imageUrl: 'https://img.29cm.co.kr/item/202501/11efce3b278d52658521bb0f49478188.jpg?width=700&format=webp',
description: '러브 에폭시 범퍼 케이스',
),
const Product(
name: 'Baby dog case',
price: 35000,
imageUrl: 'https://img.29cm.co.kr/item/202501/11efcd7680e37552ad1b413fee643a03.jpg?width=700&format=webp',
description: '강아지 에폭시 케이스',
),
const Product(
name: 'White horse case',
price: 35000,
imageUrl: 'https://img.29cm.co.kr/item/202501/11efcd754ee8c586ad1bfd60beba8629.jpg?width=700&format=webp',
description: '백마 에폭시 케이스',
),
];
// 장바구니 리스트 (상품과 수량을 함께 관리)
final List<Product> cart = [];
final Map<String, int> cartQuantities = {}; // 상품명: 수량
void _addToCart(Product product, int quantity) {
setState(() {
// 이미 장바구니에 있는 상품인지 확인
if (cartQuantities.containsKey(product.name)) {
// 이미 있으면 수량만 증가
cartQuantities[product.name] = cartQuantities[product.name]! + quantity;
} else {
// 없으면 새로 추가
cart.add(product);
cartQuantities[product.name] = quantity;
}
});
}
void _navigateToDetail(Product product, int index) async {
final shouldDelete = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProductDetailPage(product: product, onAddToCart: _addToCart),
),
);
// 상품 상세 페이지에서 삭제를 선택한 경우
if (shouldDelete == true) {
setState(() {
products.removeAt(index);
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${product.name}이(가) 삭제되었습니다.'),
duration: const Duration(seconds: 2),
),
);
}
}
}
void _navigateToCreate() async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ProductCreatePage()),
);
// 상품 등록 페이지에서 돌아올 때 새 상품이 있으면 추가
if (result != null && result is Product) {
setState(() {
products.add(result);
});
}
}
void _navigateToCart() async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProductShoppingCart(cart: cart, cartQuantities: cartQuantities),
),
);
// 장바구니에서 돌아왔을 때 화면 새로고침
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 8),
const Text(
'Case Shop',
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 35,
fontFamily: 'Schoolbell'),
),
],
),
centerTitle: true,
elevation: 0,
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
actions: [
// 장바구니 아이콘
Stack(
children: [
IconButton(
icon: const Icon(Icons.shopping_cart_outlined),
onPressed: _navigateToCart,
tooltip: '장바구니',
),
if (cart.isNotEmpty)
Positioned(
right: 8,
top: 8,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.red,
),
constraints: const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
'${cartQuantities.values.fold(0, (sum, qty) => sum + qty)}',
style: const TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
),
const SizedBox(width: 8),
],
),
body: SafeArea(
child: products.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 80, color: Colors.grey),
SizedBox(height: 16),
Text(
'상품이 없습니다.',
style: TextStyle(
fontSize: 18,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
Text(
'하단의 + 버튼을 눌러 상품을 등록해보세요.',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: ProductCard(
product: product,
onTap: () => _navigateToDetail(product, index),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
shape: const CircleBorder(),
onPressed: _navigateToCreate,
tooltip: '상품 등록',
child: const Icon(Icons.add),
),
);
}
}
1) ProductListPage (화면 선언)
class ProductListPage extends StatefulWidget {
const ProductListPage({super.key});
@override
State<ProductListPage> createState() => _ProductListPageState();
}
- 상품의 상태에 따라 변화값을 갖기 때문에 StatefulWidget 클래스를 사용합니다!
- ProductListPage라는 클래스 안에서 상품 리스트를 관리
2) _ProductListPageState (화면의 ‘상태 + 기능’을 담는 본체)
class _ProductListPageState extends State<ProductListPage> {
- 화면이 실제로 움직이는 컨트롤 센터라고 생각!
- 여기 안에 화면에서 실제로 구성하는 상태들이 전부 들어있다
3) products (상품 목록 데이터)
List<Product> products = [ ... ];
- 홈 화면에 기본 값으로 들어갈 상품들을 리스트 형태로 구성
- 임시 데이터라서 하드 코딩으로 해두었으며, 나중에는 서버나 DB에서 받아오는 자리
-> 하드 코딩이란?
프로그래밍을 할 때 데이터를 코드 내부에 직접 고정해서 입력하는 방식
쉽게 말해서 나중에 바뀔 가능성이 있는 값을 변수나 외부 파일에서 불러오지 않고 소스 코드에 못 박아 버리는 것!
4) cart / cartQuantities (장바구니 상태)
final List<Product> cart = [];
final Map<String, int> cartQuantities = {}; // 상품명: 수량
- cart -> 장바구니에 "어떤 상품이 들어있는지" 목록
- cartQuantities -> 상품별 수량을 저장하는 맵 형태의 목록
5) _addToCart (장바구니 담기 로직)
void _addToCart(Product product, int quantity) {
setState(() {
if (cartQuantities.containsKey(product.name)) { // 이미 장바구니에 해당 상품이 들어있는지 확인
cartQuantities[product.name] = cartQuantities[product.name]! + quantity; // 이미 있으면 수량만 추가
} else { // 없으면 새로 추가
cart.add(product);
cartQuantities[product.name] = quantity;
}
});
}
- 이 상품을 quantity만큼 장바구니에 담아 달라는 코드
- containsKey(product.name): cartQuantities라는 저장소에 해당 상품 이름이 이미 등록되어 있는지 체크
- cartQuantities[product.name]: 상품 이름을 '키(Key)'로 사용하여 현재 담긴 '수량(Value)'을 가져옴
- + quantity: 새로 전달받은 수량만큼 더해줌
- ! (Null Safety): 프로그래밍 언어(주로 Dart/Flutter)에서 "내가 방금 확인했으니까 이 값은 절대 비어있지(null) 않아, 안심하고 계산해!"라고 확신을 주는 표시
6) _navigateToDetail (상세 페이지로 이동 + 삭제 결과 처리)
void _navigateToDetail(Product product, int index) async {
final shouldDelete = await Navigator.push(...);
if (shouldDelete == true) {
setState(() {
products.removeAt(index);
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(...);
}
}
}
- await Navigator.push(...) : 상세 페이지로 전환하면서, 그 페이지가 닫힐 때까지 함수의 실행을 잠시 멈추고 기다림
상세 페이지에서 Navigator.pop(context, true)와 같이 값을 돌려주면, 그 값이 shouldDelete 변수에 저장
'플러터 앱 개발' 카테고리의 다른 글
| Chapter 13. Flutter 데이터 통신 기초와 JSON (0) | 2026.01.21 |
|---|---|
| [트러블 슈팅] 1차 MVP 제작: Make를 사용하여 모듈 연결 (0) | 2026.01.19 |
| Chapter 12. 예외 처리 (1) | 2026.01.13 |
| Chapter 11. 객체 지향 프로그래밍과 Dart C - 객체 지향과 상속 (1) | 2026.01.12 |
| Chapter 10. 객체 지향 프로그래밍과 Dart B - 클래스와 메서드 (1) | 2026.01.08 |