[Flutter] flutter_shoppingcart

최재원's avatar
Jul 30, 2025
[Flutter] flutter_shoppingcart

코드

1. 위젯

Stack

용도
  • 여러 위젯을 겹쳐서 배치할 때 사용. 자식 위젯을 Z축(앞뒤 순서) 기준으로 쌓음.
어디서 사용
  • 배경 위에 텍스트나 버튼을 겹치고 싶을 때
  • 이미지 위에 아이콘 배치
  • 커스텀 뱃지, 오버레이 UI 구현 등
기본 정렬
  • 자식 위젯들을 기본적으로 좌측 상단에 정렬함.
레이아웃 (가로/세로 제약 및 동작)
  • Stack은 자식 중 가장 큰 위젯을 기준으로 크기 결정
  • fit 속성에 따라 크기 결정 방식이 달라짐
    • StackFit.loose (기본값): 자식 위젯이 가능한 크기를 가짐
    • StackFit.expand: Stack이 주어진 영역을 모두 채우게 함
  • 자식 위젯을 정밀히 위치 지정하려면 Positioned 위젯 사용
  • 자식이 Positioned 없이 배치되면 좌상단에 고정됨
속성
  • childrenList<Widget> | Stack에 겹쳐서 배치할 자식 위젯 목록 (필수)
  • alignmentAlignmentGeometry | Positioned를 사용하지 않은 자식들의 기본 정렬 위치 (기본값: AlignmentDirectional.topStart)
  • fitStackFit | Stack이 자식 위젯들의 크기를 어떻게 반영할지 결정 (기본값: StackFit.loose)
  • clipBehaviorClip | Stack 경계를 넘는 자식을 잘라낼지 여부 (기본값: Clip.hardEdge)
  • textDirectionTextDirection? | alignment가 방향성 영향을 받을 경우 사용 (예: LTR, RTL)
import 'package:flutter/material.dart'; class ColorIcon extends StatelessWidget { double rGap; Color color; ColorIcon({this.rGap = 0.0, this.color = Colors.white}); @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(right: rGap), child: Stack( children: [ Container( width: 50, height: 50, decoration: BoxDecoration( border: Border.all(), shape: BoxShape.circle, color: Colors.white, ), ), Positioned( top: 5, left: 5, child: ClipOval( child: Container( width: 40, height: 40, decoration: BoxDecoration(color: color), ), ), ), ], ), ); } }
notion image
  • 선이 있는 컨테이너 위로(Z축) 색이 있는 컨테이너가 있다
 

Positioned

용도
  • Stack 내부에서 자식 위젯의 정확한 위치를 지정할 때 사용.
어디서 사용
  • Stack 내에서 위젯을 상단/하단/좌우에 고정할 때
  • 특정 위치(좌표)에 버튼, 텍스트 등을 배치할 때
기본 정렬
  • 별도 정렬 없음. **명시적으로 지정된 위치 값(left, right 등)**을 따름.
레이아웃 (가로/세로 제약 및 동작)
  • 반드시 Stack 위젯 내부에서만 사용 가능
  • top, left, right, bottom 값을 통해 위치 지정
  • 네 방향 값을 조합해 크기를 고정하거나 자동으로 계산 가능
  • 방향값이 없으면 부모(Stack)의 기본 alignment를 따름
속성
  • childWidget | 위치를 지정할 실제 자식 위젯
  • topdouble? | Stack 상단으로부터의 거리
  • leftdouble? | Stack 왼쪽으로부터의 거리
  • rightdouble? | Stack 오른쪽으로부터의 거리
  • bottomdouble? | Stack 하단으로부터의 거리
  • widthdouble? | 자식 위젯의 너비를 고정할 경우 사용
  • heightdouble? | 자식 위젯의 높이를 고정할 경우 사용

RichText

용도
  • 하나의 텍스트 블록에서 다양한 스타일(색상, 굵기 등)을 섞어 표현할 때 사용.
어디서 사용
  • 문장 안의 특정 단어에만 색, 굵기 등 스타일을 다르게 줄 때
  • 하이퍼링크, 강조, 다국어 조합이 있는 UI에 적합
기본 정렬
  • 텍스트는 기본적으로 왼쪽 정렬, 위에서 아래로 흐름.
레이아웃 (가로/세로 제약 및 동작)
  • 상위 위젯이 주는 제약 안에서 텍스트 크기에 맞게 줄바꿈하며 확장
  • 내부 텍스트는 TextSpan으로 구성되어 스타일별로 분할 표현
속성
  • textInlineSpan | 표시할 텍스트와 스타일 정보. 보통 TextSpan 사용
  • textAlignTextAlign | 텍스트 정렬 방식 (기본값: TextAlign.start)
  • textDirectionTextDirection? | 텍스트의 방향 (예: LTR, RTL). 필요 시 명시
  • maxLinesint? | 최대 줄 수. 초과 시 잘림
  • overflowTextOverflow | 텍스트가 넘칠 때 처리 방식 (기본값: TextOverflow.clip)
  • softWrapbool | 줄바꿈 여부 설정 (기본값: true)
  • strutStyleStrutStyle? | 줄 간격 및 높이 정렬 설정
  • textScaleFactordouble | 텍스트 크기 배율 (기본값: 1.0)
RichText( text: TextSpan( style: TextStyle(fontSize: 18), children: [ TextSpan(text: "review"), WidgetSpan(child: SizedBox(width: 5)), TextSpan( text: "(26)", style: TextStyle(color: Colors.blue), ), ], ), ),
notion image

TextSpan

용도
  • RichText에서 스타일이 다른 텍스트 조각을 구성할 때 사용.
  • 텍스트와 텍스트 스타일을 정의하는 객체.
어디서 사용
  • RichTexttext 속성에 사용
  • 여러 TextSpan을 중첩하여 다양한 텍스트 스타일을 한 줄/블록에 표현 가능
기본 정렬
  • 정렬은 RichTexttextAlign 속성을 따름
레이아웃 (가로/세로 제약 및 동작)
  • 자체적으로 레이아웃 제약 없음
  • RichText가 제약 조건을 제시하며, 내부에서는 TextStyle에 따라 표현
속성
  • textString? | 표시할 실제 문자열
  • styleTextStyle? | 텍스트에 적용할 스타일 (색상, 폰트, 크기 등)
  • childrenList<InlineSpan>? | 중첩된 하위 텍스트 조각들 (재귀적으로 다른 TextSpan을 포함 가능)
  • recognizerGestureRecognizer? | 탭 등 제스처 인식기 (예: 링크처럼 클릭 가능하게 만들기)
  • semanticsLabelString? | 접근성(스크린 리더)용 대체 라벨
RichText( text: TextSpan( style: TextStyle(fontSize: 18), children: [ TextSpan(text: "review"), WidgetSpan(child: SizedBox(width: 5)), TextSpan( text: "(26)", style: TextStyle(color: Colors.blue), ), ], ), ),
notion image

WidgetSpan

용도
  • RichText 안에서 **텍스트 줄에 위젯(아이콘, 버튼 등)**을 텍스트처럼 삽입할 때 사용.
어디서 사용
  • 텍스트 중간에 아이콘, 이미지, 버튼 등을 자연스럽게 넣고 싶을 때
  • RichTexttext 속성 내에서 TextSpan과 함께 사용 가능
기본 정렬
  • 기본적으로 텍스트 라인 기준으로 정렬되며, alignment로 조절 가능
레이아웃 (가로/세로 제약 및 동작)
  • RichText의 레이아웃 제약 안에서, child 위젯이 텍스트 줄과 함께 흐름에 맞게 배치됨
  • 텍스트처럼 줄바꿈에 포함되며, 위젯의 크기에 따라 줄 높이가 조절될 수 있음
  • 텍스트처럼 inline 흐름을 가짐 (단, 복잡한 레이아웃은 Row 등 사용 추천)
속성
  • childWidget | 텍스트 사이에 삽입할 위젯 (예: Icon, Image, Container 등)
  • alignmentPlaceholderAlignment | 텍스트 줄 내에서 위젯의 수직 정렬 위치
    • (기본값: PlaceholderAlignment.bottom)
      → 주요 옵션: baseline, middle, top, bottom, aboveBaseline, belowBaseline
  • baselineTextBaseline? | alignmentbaseline 관련일 때 기준이 되는 베이스라인 설정
    • (예: TextBaseline.alphabetic)
  • styleTextStyle? | 위젯이 차지하는 영역에 적용되는 텍스트 스타일 (공백 등 처리에 영향 있음)
RichText( text: TextSpan( style: TextStyle(fontSize: 18), children: [ TextSpan(text: "review"), WidgetSpan(child: SizedBox(width: 5)), TextSpan( text: "(26)", style: TextStyle(color: Colors.blue), ), ], ), ),
notion image

상태 변경 riverpod

💡
데이터 박스가 불변해야 하는 이유 박스의 내부가 변경되면 플러터 엔진이 뭐가 바뀌었는지 확인을 해야 한다 그냥 박스를 통으로 바꾸면 뭐가 바뀌었는지 찾을 필요 없고 그냥 박스가 통으로 바뀌었다는 것을 바로 알 수 있다 원래 박스와 내가 변경한 내용을 비교할 수 없다
같은 박스에서 내부 상태만 변경하면 원래 값이 사라지기 때문에 비교가 불가능하다 하지만 박스를 그냥 통으로 바꾸면 그냥 박스가 바뀌었다는 것을 알 수 있기 때문에 변경을 감지할 수 있다
요약 기존 데이터가 없으면 변경 감지가 불가능하다 그래서 기존 데이터를 수정하는 방법을 사용하면 안된다
깊은 복사 버전을 사용하자

뷰 모델을 통해 상태와 뷰를 따로 분리해서 다뤄야 한다

뷰 모델

import 'package:flutter_riverpod/flutter_riverpod.dart'; // 1. 창고 데이터 타입 (원시타입이면 안만들어도 됨) class SelectorModel { final List<String> images; // 불변!! final int selectedId; SelectorModel(this.images, this.selectedId); SelectorModel copyWith({ List<String>? images, int? selectedId, }) { return SelectorModel( images ?? this.images, selectedId ?? this.selectedId, ); } // 선택된 이미지 반환 String selectedImage() => images[selectedId]; // 현재 index가 선택된 index인지 여부 반환 bool isSelected(int id) => selectedId == id; } /// 2. 창고 (상태와 행위(변경로직)를 가진다) /// - Notifier를 상속하여 상태를 관리하고 변경할 수 있음 /// - build(): 초기 상태 설정 /// - onClick(): 새로운 상태로 갱신 class HomeVM extends Notifier<SelectorModel> { @override SelectorModel build() { return SelectorModel(["assets/p1.jpeg", "assets/p2.jpeg", "assets/p3.jpeg", "assets/p4.jpeg"], 0); } void onClick(int id) { print(id); // 1. 기존 값 변경 (rebuild 안됨) final 지우고 테스트 // state.selectedId = id; // 2. 기존 값 불변 유지 -> 깊은 복사 (rebuild 됨) - copyWith 라는 메서드를 일반적으로 사용 state = state.copyWith(selectedId: id); // SelectorModel prevModel = state; // 100번지 // SelectorModel nextModel = SelectorModel(prevModel.images, id); // 200번지 // state = nextModel; } } // 3. 창고 관리자 final homeProvider = NotifierProvider<HomeVM, SelectorModel>(() { return HomeVM(); });
  • 모델에 있는 필드 값들은 나중에 서버의 DTO 와 동일하게 만들면 된다
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../page/home_vm.dart'; import 'selector_button.dart'; class SelectorHeader extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { SelectorModel model = ref.watch(homeProvider); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(5), child: AspectRatio( aspectRatio: 5 / 3, child: Image.asset("${model.selectedImage()}", fit: BoxFit.cover), ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ SelectorButton(id: 0, icon: Icon(Icons.directions_bike)), SelectorButton(id: 1, icon: Icon(Icons.motorcycle)), SelectorButton(id: 2, icon: Icon(Icons.directions_car_filled)), SelectorButton(id: 3, icon: Icon(Icons.airplanemode_on)), ], ), ), ], ), ); } }
  • 뷰에서는 데이터를 읽거나, 변경하는 코드만 가지고 있는다
 
Share article

jjack1