[TIL] Flutter 9기 Day 9 라이브러리 & Map에 대해 뜯어보기 & dart:math & 개인 과제 트러블 슈팅

현서·2025년 12월 4일

[TIL] Flutter 9기

목록 보기
21/102
post-thumbnail

📍 튜터님과 Widget 공부

Opacity

공식 문서
자식 위젯의 투명도를 조절하는 위젯
opacity 값(0.0 ~ 1.0)으로 투명도 지정
1.0 = 완전 불투명
0.0 = 완전 투명

Opacity 언제 사용할까?

위젯 자체를 정말로 “투명하게” 보여야 할 때만
단순히 사라지는 애니메이션이면 AnimatedOpacity가 더 자연스럽고 권장

AnimatedOpacity

Opacity를 애니메이션으로 부드럽게 변화시키는 위젯
opacity 값이 바뀌면 지정한 duration 동안 자동으로 애니메이션 실행
상태 변화에 반응하는 위젯이라 StatefulWidget 안에서 사용하는 경우가 많음

AnimatedOpacity 언제 사용할까?

  • 스플래쉬 스크린
  • 요소를 부드럽게 나타내거나 사라지게 하고 싶을 때
  • Fade-in / Fade-out 연출이 필요할 때

FutureBuilder

공식 문서
비동기 작업(Future)의 상태 변화에 따라 UI를 자동으로 바꿔주는 위젯
예: 서버 요청, 파일 로딩, DB 읽기 같은 작업 처리할 때 사용

future: 실행할 비동기 함수.
builder: Future의 상태에 따라 어떤 위젯을 그릴지 결정하는 함수

FutureBuilder(
  future: fetchData(),              // Future 함수
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator(); // 로딩 상태
    }

    if (snapshot.hasError) {
      return Text('에러 발생');
    }

    if (snapshot.hasData) {
      return Text(snapshot.data.toString()); // 데이터 표시
    }

    return SizedBox(); // 기본값
  },
);

snapshot.connectionState

AsyncSnapshot의 비동기 상태를 나타내는 열거형 값
보통 FutureBuilder/StreamBuilder의 builder에서 먼저 확인함

  • ConnectionState.none
    future 아직 연결(할당) 되지 않은 상태.
    FutureBuilder에서는 future == null일 때 주로 나옴.

  • ConnectionState.waiting
    Future가 실행(요청) 되어 결과를 기다리는 중(로딩 상태)
    로딩 인디케이터 표시할 때 사용

  • ConnectionState.done
    Future가 완료되었거나(성공/에러), 스트림이 종료된 상태.
    이 시점에서 snapshot.data 또는 snapshot.error 확인.

snapshot.hasError / snapshot.error

  • snapshot.hasError: 에러가 있는지 여부 (snapshot.error != null)
  • snapshot.error: 실제 에러 객체. 에러 메시지나 타입 확인 가능

snapshot.hasData / snapshot.data

  • snapshot.hasData: 데이터가 있는지 여부. (내부적으로 snapshot.data != null 체크)
    즉, data가 null이면 hasData는 false

  • snapshot.data: Future가 반환한 실제 값


📍 라이브러리 Library

라이브러리란?

특정 기능을 쉽게 사용할 수 있도록 미리 작성된 코드 묶음

✏️ 라이브러리를 사용하는 이유

  • 효율성: 필요한 기능만 빠르게 사용 가능
  • 재사용성: 같은 코드를 반복 작성할 필요 없음
  • 가독성 향상: 코드가 간결해짐

✏️ 라이브러리 종류

크게 Dart SDK에 포함된 라이브러리 / 외부 라이브러리(pub.dev) 로 나뉨

Dart SDK 표준 라이브러리

모든 플랫폼(Native + Web) 공통

  • dart:core
    기본 타입(int, double, String), 컬렉션 등 핵심 기능
    자동 포함 → import 불필요

  • dart:async
    Future, Stream 등 비동기 기능

  • dart:collection
    추가 컬렉션 타입(Queue, Linked List, HashMap 등)

  • dart:convert
    JSON, UTF-8 인코딩/디코딩

  • dart:developer
    디버거와 상호작용

  • dart:math
    수학 함수, 상수, 랜덤

Native 플랫폼 전용 (Mobile, Desktop)

  • dart:ffi
    Dart에서 C API 사용

  • dart:io
    파일, 소켓, HTTP 등 입출력 기능

Web 플랫폼 전용

  • package:web
    가벼운 브라우저 API 연결

  • dart:js_interop
    JS 및 브라우저 API 상호운용

  • dart:html
    Web 요소 및 리소스 제어

외부 라이브러리(pub.dev)

외부 개발자가 만든 라이브러리
https://pub.dev 에서 검색 가능

cupertino_icons — Flutter Cupertino 기본 아이콘
intl — 날짜/숫자 포맷, 국제화
shared_preferences — 간단한 로컬 저장소
url_launcher — URL 실행
image_picker — 카메라·앨범 접근
firebase_core — Firebase 연동 기본
firebase_auth — Firebase 인증
google_fonts — 구글 폰트
permission_handler — 권한 처리
flutter_svg — SVG 렌더링
cached_network_image — 이미지 캐싱
flutter_local_notifications — 로컬 알림
path_provider — 파일 시스템 접근
geolocator — 위치 서비스
dio — HTTP 네트워크 라이브러리
custom_lint — Lint 규칙 설정

✏️ 라이브러리 사용법

import

기본

import 'dart:html';
import 'package:http/http.dart';
import 'src/my_utils.dart';

별칭 지정 — as

import 'package:http/http.dart' as http;

필요한 것만 가져오기 — show

import 'package:lib1/lib1.dart' show foo;

특정 것 제외 — hide

import 'package:lib2/lib2.dart' hide foo;

지연 로딩 Deferred Loading)

필요한 시점에 라이브러리를 불러오는 방식
→ Web에서만 지원

지연 로딩 - deferred as

import 'package:greetings/hello.dart' deferred as hello;

호출

Future<void> greet() async {
  await hello.loadLibrary();
  hello.printGreeting();
}

loadLibrary() 는 deferred as 를 사용하면 자동으로 제공


📍 비동기 프로그래밍

✏️ 동기 프로그래밍

순차 실행
앞 작업이 끝나야 다음 작업 실행됨
시간이 오래 걸리는 작업에서 프로그램 전체가 멈춤

✏️ 비동기 프로그래밍

기다리는 작업이 있어도 전체 코드 흐름을 멈추지 않는 방식

  • 시간이 오래 걸리는 작업에 필수:
    - 파일 읽기/쓰기
    - DB 작업
    - 네트워크 통신

✏️ Future

Future란?

비동기 작업 1개의 결과를 “한 번만” 반환하는 클래스.
Future<T> 형태 → 제네릭

즉시 값 만들기

Future<int> a = Future.value(1);

Future.delayed()

1) 지연만 발생

Future.delayed(Duration(seconds: 2));

2) 지연 후 실행할 코드 전달

Future.delayed(Duration(seconds: 2), () {
  print('2초 뒤 실행');
});

Future 기본 예시

void introduce(String name) {
  print('$name 시작');
  Future.delayed(Duration(seconds: 2), () {
    print('안녕 나는 $name');
  });
  print('$name 끝');
}

→ 끝이 먼저 찍히고, 나중에 delayed 내용 실행

async / await

await 로 순서 보장

void introduce(String name) async {
  print('$name 시작');
  await Future.delayed(Duration(seconds: 2), () {
    print('안녕 나는 $name');
  });
  print('$name 끝');
}

여러 작업을 순서대로 실행

void main() async {
  await introduce('A');
  await introduce('B');
}

introduce(A)가 끝나야 introduce(B) 실행
이때 introduce도 future로 감싸야함

future <void> introduce(String name) async {}

Future 반환값 받기

Future<String> introduce(String name) async {
  await Future.delayed(Duration(seconds: 2));
  return '이름은 $name';
}

void main() async {
  var a = await introduce('A');
  var b = await introduce('B');
  print('$a / $b');
}

✏️ Stream

Stream이란?

시간 순서대로 여러 개의 값을 비동기적으로 방출
이벤트 여러 번 발생할 때 사용
Stream<T> 형태 → 제네릭

yield

값을 하나씩 방출하는 키워드

Stream<String> names() async* {
  yield 'A';
  yield 'B';
  yield 'C';
}

listen()

Stream에서 방출되는 값을 실시간으로 받음

names().listen((value) {
  print(value);
});

반복문 + yield

Stream<String> emitNames(List<String> names) async* {
  for (var i = 0; i < names.length; i++) {
    yield '${i+1}번째 ${names[i]}';
  }
}

Stream 예제

10 → 0 방출

Stream<int> emitNumbers(int first) async* {
  for (var i = first; i >= 0; i--) {
    yield i;
  }
}

비동기임을 확인

print('시작');
emitNumbers(10).listen(print);
print('끝');

“끝”이 먼저 출력됨 → 비동기
1초 간격으로 숫자 방출

Stream<int> emitNumbers(int first) async* {
  for (var i = first; i >= 0; i--) {
    yield i;
    await Future.delayed(Duration(seconds: 1));
  }
}

동시에 두 개 실행하면?

emitNumbers(10).listen((n) => print('타이머1 $n'));
emitNumbers(10).listen((n) => print('타이머2 $n'));

→ 두 Stream이 동시에 독립적으로 동작

정리

  • Future
    결과 1번
    async/await 사용
    네트워크 요청, 파일 읽기 같은 단발성 작업에 적합

  • Stream
    결과 여러 번
    listen()으로 실시간 구독
    버튼 클릭, 센서 데이터, 타이머 등 지속적 이벤트에 적합

                                   

📍 Map

Dart 공식 문서 - Map
Dart Programming Tutorial

개인 과제를 진행하다가 Map 키랑 값 사용하는 부분이 궁금해서 찾아봤다.

✏️ Map 특징

  • key/value 쌍으로 데이터 저장
    Map은 키(key)와 값(value)을 한 쌍으로 저장
: { 'name': '현서', 'age': 24 }'name'이 키, '현서'가 값.
키로 값을 조회하고(map['name']), 키로 값을 변경하거나 추가(map['city'] = 'Seoul')할 수 있음
  • key는 하나당 하나의 value만 가짐 (중복 x)
    같은 키가 여러 번 존재할 수 없음 같은 키로 넣으면 마지막에 넣은 값이 덮어씀
var m = {'a': 1, 'b': 2};
m['a'] = 3;           // {'a':3, 'b':2}
var m2 = {'a':1, 'a':4}; // 실제 리터럴에서는 마지막 값만 남음 -> {'a':4}

여러 값을 저장하려면 값 쪽을 리스트나 다른 컬렉션으로 만들어야 함 ('a': [1,2])

  • key/value 모두 iterable

    Iterable = for문으로 돌릴 수 있는 자료 구조
    List, Set, Map의 keys, Map의 values, Map의 entries
    이런 것들이 모두 Iterable

for (var k in map.keys) print(k);
for (var v in map.values) print(v);
for (var e in map.entries) print('${e.key}: ${e.value}');
  • 수정 중에 동시에 변경하면 오류 가능
    Map을 순회하면서(예: forEach, for (k in map.keys))
    바로 그 Map에 항목을 추가하거나 제거하면 ConcurrentModificationError 같은 문제가 발생
var m = {'a':1, 'b':2};
for (var k in m.keys) {
  if (k == 'a') m.remove('b'); // 잘못된 사용 -> 에러 발생 가능
}

✏️ Map 속성

entries

Iterable<MapEntry<K, V>> 형태
키와 값을 한 쌍으로 묶은 MapEntry들을 반복해서 읽을 수 있음
읽기 전용이지만, 내부 값은 수정할 수 있음

var map = {'a': 1, 'b': 2};

for (var entry in map.entries) {
  print('${entry.key}: ${entry.value}');
}

직접 map.entries.add() 불가능

hashCode

객체의 해시값
Map 그 자체의 해시코드이며, 키나 값의 해시코드와는 별개
주로 Map이 동일한 객체인지 비교에 사용됨
읽기 전용

isEmpty / isNotEmpty

맵이 비어 있으면 true / !isEmpty

var m = {};
print(m.isEmpty); // true

var m = {'a': 1};
print(m.isNotEmpty); // true

keys

Iterable<K>
키만 모아둔 반복 가능한 객체
읽기 전용 (추가/삭제 불가)

var map = {'a':1, 'b':2};
for (var k in map.keys) print(k);

values

Iterable<V>
값만 모아서 반복해서 읽을 수 있음
읽기 전용

for (var v in map.values) print(v);

length

Map 안의 key/value 쌍의 개수

var m = {'a':1, 'b':2};
print(m.length); // 2

runtimeType

런타임에 어떤 타입의 Map인지 알려줌
읽기 전용

print({'a':1}.runtimeType);
// _Map<String, int>

정리

속성설명
entriesMapEntry 반복자, 읽기 전용
hashCode해시 코드, 읽기 전용
isEmpty맵이 비어있는지 여부
isNotEmpty맵에 값이 있는지 여부
keys키 반복자, 읽기 전용
values값 반복자, 읽기 전용
length키/값 쌍의 개수
runtimeType객체의 런타임 타입, 읽기 전용

✏️ Map 메서드

addAll(Map<K,V> other)

다른 Map의 모든 key/value를 현재 Map에 추가
같은 키가 있으면 기존 값을 덮어씀

map.addAll({'b': 20, 'c': 30});

addEntries(Iterable<MapEntry<K,V>> newEntries)

여러 MapEntry 객체를 한 번에 추가
entries는 map.entries 형태처럼 key/value 묶음

map.addEntries([MapEntry('x', 10), MapEntry('y', 20)]);

cast<RK, RV>()

Map을 다른 타입(Map<RK, RV>)으로 보는(view) 기능
실제 데이터를 바꾸지는 않음
타이프 캐스팅 오류는 사용 시점에 발생함

Map<dynamic, dynamic> m = {'a': 1};
Map<String, int> m2 = m.cast<String, int>();

clear()

모든 key/value 삭제 → 비어 있는 Map이 됨

map.clear();

containsKey(Object? key)

특정 키가 Map에 존재하는지 확인

map.containsKey('a'); // true/false

containsValue(Object? value)

특정 값이 있는지 확인

map.containsValue(100); // true/false

forEach(void action(K key, V value))

모든 key/value에 대해 콜백 실행
반환값 없음

map.forEach((k, v) => print('$k: $v'));

map<K2,V2>(MapEntry<K2,V2> convert(K key, V value))

각 엔트리를 변환해서 새로운 Map을 생성
원본 Map을 수정하지 않음

var result = map.map((k, v) => MapEntry(k.toUpperCase(), v * 2));

putIfAbsent(K key, V ifAbsent())

키가 없을 때만 값을 추가
키가 이미 있으면 아무것도 안 함

remove(Object? key)

특정 키 삭제
삭제된 값(value)을 반환
키 없으면 null 반환

map.remove('a'); 

removeWhere(bool test(K key, V value))

조건(test)에 맞는 key/value 모두 삭제

map.removeWhere((k, v) => v < 10);

update(K key, V update(V value), {V ifAbsent()?})

특정 키가 있을 때 값 업데이트
키가 없다면 ifAbsent가 있으면 새로 추가

map.update('a', (v) => v + 1);  
map.update('b', (v) => v, ifAbsent: () => 100);

updateAll(V update(K key, V value))

Map의 모든 value를 업데이트.

map.updateAll((k, v) => v * 2);

메서드 정리

메서드 종류대표 메서드특징
추가addAll, addEntries, putIfAbsent키 중복 처리 다름
삭제remove, removeWhere, clear부분 삭제/전체 삭제
조회containsKey, containsValue존재 판단
변환map, cast새 Map 생성 / 타입 view
업데이트update, updateAll특정 값 혹은 전체 값 변경
순회forEach읽기만 가능

📍 dart:math 라이브러리

수학 상수, 함수들, 랜덤 생성기를 제공

import 'dart:math';

✏️ 상수

상수의미
pi원주율 π ≈ 3.14159
e자연상수 e ≈ 2.71828
sqrt2√2
sqrt1_21/√2
ln10ln(10)
log2elog₂(e)
log10elog₁₀(e)

✏️ 수학 함수

함수설명
sqrt(x)x의 제곱근
pow(x, y)x의 y제곱
sin(x)x 라디안의 사인
cos(x)x 라디안의 코사인
tan(x)x 라디안의 탄젠트
asin(x)사인 역함수 (라디안)
acos(x)코사인 역함수 (라디안)
atan(x)탄젠트 역함수 (라디안)
atan2(y, x)y/x 의 역탄젠트, 사분면 정보 포함
log(x)자연로그 (ln)
exp(x)e^x
min(a, b)a와 b 중 작은 값
max(a, b)a와 b 중 큰 값
abs(x)x의 절댓값
clamp(x, lower, upper)lower ≤ x ≤ upper 로 제한

✏️ Random (랜덤)

숫자, 불린 랜덤값 생성

Random(seed) → 시드를 지정하면 항상 같은 난수 시퀀스를 생성할 수 있음.
nextInt(max)0부터 max-1 사이 정수
nextDouble()0.0 ~ 1.0 사이 실수
nextBool()true/false 랜덤

✏️ 기타

Point 클래스: 2D 좌표를 표현할 때 사용
Rectangle 클래스: 사각형 영역 표현
Vector 관련 기능은 별도 라이브러리 필요

Point (점)

2D 좌표 표시용 클래스

const Point(0, 0);
const Point(200, 400);

Rectangle (직사각형, 불변)

좌상단(left, top)과 우하단(right, bottom)으로 직사각형 생성

Rectangle.fromPoints(Point(20, 50), Point(300, 600));

또는 (left, top, width, height)로 선언

const Rectangle(20, 50, 300, 600);

MutableRectangle (직사각형, 변경 가능)

너비, 높이 수정 가능

var rect = MutableRectangle(20, 50, 300, 600);
rect.width = 200;
rect.height = 100;

사실 무슨 소리인지 어떻게 써야할지는 잘 모르겠지만..
랜덤값 출력하려고 뜯어 봤다,.


📍 트러블 슈팅

개인 과제로 Dart를 이용해 점수에 따른 등급 계산, 장바구니 계산, 로또 발급 프로그램을 만들어봤다. 장바구니 총 금액을 계산할 때 장바구니에 담긴 상품 리스트(List)와 상품별 가격표(Map)를 연결해서 합계를 구하는 방법을 처음에는 어떻게 구현해야 할지 감이 잡히지 않았다. 어떤 반복문을 사용할지, Map에서 값을 가져올 때 null 안전성을 어떻게 보장해야 하는지 고민이 많았다. 그래서 Dart 공식 문서를 참고하며 Map의 다양한 속성과 메서드를 분석했고 priceList[item]!처럼 값을 가져오는 방식으로 문제를 해결했다. 이 과정을 통해 Map과 List를 함께 활용하는 패턴을 익힐 수 있었고, 컬렉션 자료형에 대한 이해가 많이 깊어졌다.

또한 프로그램에서 어떤 상황에서 예외가 발생할지 고민하는 과정도 의미 있었다. 장바구니가 비어 있을 때, 존재하지 않는 상품이 포함되었을 때, 점수가 0~100 범위를 벗어났을 때 등 다양한 예외 상황을 생각하며 만들었다. 단순히 주어진 코드를 구현하는 것이 아니라 사용자가 잘못된 입력을 했을 때 프로그램이 안전하게 동작하도록 하는 방법을 고민하면서 예외 처리의 중요성을 다시 한 번 느낄 수 있었다.

이번 과제를 통해 컬렉션 활용, 반복문, 조건문, 예외 처리, 랜덤 데이터 처리 등 Dart에서 자주 쓰이는 기능들을 실제 상황에 적용해 보는 연습을 충분히 할 수 있었다. 또한 문제를 분석하고, 공식 문서를 참고하며 해결 방안을 찾는 과정에서 프로그래밍 문제 해결 능력도 한층 향상되었다고 느꼈다. 앞으로도 이렇게 하나씩 구현하면서 코드를 점점 더 안전하고 효율적으로 작성하는 연습을 계속해 나가고 싶다.

0개의 댓글