๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Flutter + Dart/Flutter + Dart ๊ณต๋ถ€

๋น„๋™๊ธฐ ๋ฐ ํŠน์ˆ˜ ์œ„์ ฏ

by GREEN๋‚˜๋ฌด 2025. 5. 14.
728x90

๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ

  • ๋‹ค์–‘ํ•œ ํŠน์ˆ˜ ์œ„์ ฏ(FutureBuilder, StreamBuilder, TabBar, PageView ๋“ฑ)์˜ ๋™์ž‘ ์›๋ฆฌ ์ดํ•ด
  • ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ํ™œ์šฉํ•œ UI ๊ตฌ์„ฑ ๋Šฅ๋ ฅ ํ–ฅ์ƒ
  • ํƒญ ์ „ํ™˜ ๋ฐ ์Šฌ๋ผ์ด๋“œ UI ๊ตฌํ˜„ ๋Šฅ๋ ฅ ์Šต๋“
  • ๋ณต์žกํ•œ ์œ„์ ฏ ์กฐํ•ฉ์„ ํ†ตํ•œ ์‹ค์ „ ์•ฑ UI ์„ค๊ณ„ ๊ฒฝํ—˜

๐Ÿ”น FutureBuilder

โœ… ๊ฐœ๋… ์ •๋ฆฌ

FutureBuilder๋Š” Future ๊ฐ์ฒด์˜ ์ƒํƒœ ๋ณ€ํ™”์— ๋”ฐ๋ผ UI๋ฅผ ์ž๋™์œผ๋กœ ๊ฐฑ์‹ ํ•ด์ฃผ๋Š” ์œ„์ ฏ์ž…๋‹ˆ๋‹ค.
๋น„๋™๊ธฐ ์ž‘์—…์˜ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋‹ค๋ฆด ๋•Œ ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ, ์„ฑ๊ณต/์‹คํŒจ ๋ฉ”์‹œ์ง€๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

โš ๏ธ ์ฃผ์˜: future๋Š” build() ํ•จ์ˆ˜ ๋‚ด๋ถ€๊ฐ€ ์•„๋‹Œ, initState() ๋“ฑ ์™ธ๋ถ€์—์„œ ์„ ์–ธํ•ด์•ผ ๋ถˆํ•„์š”ํ•œ ์žฌ๋นŒ๋“œ๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”ง ์ฃผ์š” ์†์„ฑ

์†์„ฑ ์„ค๋ช…
future ๊ฐ์‹œํ•  Future ๊ฐ์ฒด
builder AsyncSnapshot์„ ์ธ์ž๋กœ ๋ฐ›์•„ UI๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜
snapshot.hasData ์ •์ƒ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ๋„์ฐฉํ–ˆ๋Š”์ง€ ์—ฌ๋ถ€
snapshot.hasError ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์—ฌ๋ถ€
snapshot.connectionState none, waiting, active, done ์ƒํƒœ ๋ฐ˜ํ™˜

๐Ÿงช ๊ธฐ๋ณธ ์˜ˆ์ œ

class FutureDemo extends StatefulWidget {
  @override
  _FutureDemoState createState() => _FutureDemoState();
}

class _FutureDemoState extends State<FutureDemo> {
  late Future<String> _calculation;

  @override
  void initState() {
    super.initState();
    _calculation = Future.delayed(Duration(seconds: 2), () => '๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ!');
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _calculation,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Center(child: CircularProgressIndicator());
        } else if (snapshot.hasError) {
          return Center(child: Text('์—๋Ÿฌ: ${snapshot.error}'));
        } else {
          return Center(child: Text(snapshot.data!));
        }
      },
    );
  }
}

๐Ÿงฉ ์‹ค์Šต ๊ณผ์ œ

  • Future.delayed(Duration(seconds: 3))๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ์™„๋ฃŒ ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ ์•„์ด์ฝ˜ ํ‘œ์‹œํ•ด๋ณด๊ธฐ
  • Future.error('๋ฌธ์ œ ๋ฐœ์ƒ')์„ ์ด์šฉํ•ด ์—๋Ÿฌ ๋ฐœ์ƒ ์ƒํ™ฉ ํ…Œ์ŠคํŠธ
  • ๋”๋ณด๊ธฐ

    ์ฐธ๊ณ 

    • Future.error('๋ฌธ์ œ ๋ฐœ์ƒ')๋ฅผ Future.delayed์˜ ์ฝœ๋ฐฑ ์•ˆ์—์„œ ์ง์ ‘ ๋ฆฌํ„ดํ•˜๋ฉด snapshot.data๋กœ ์ „๋‹ฌ๋˜๊ฑฐ๋‚˜ ๋ฌด์‹œ๋  ์ˆ˜ ์žˆ์–ด.
    • throw '๋ฌธ์ œ ๋ฐœ์ƒ'์ฒ˜๋Ÿผ ์ง์ ‘ ์˜ˆ์™ธ๋ฅผ ๋˜์ ธ์•ผ FutureBuilder๊ฐ€ hasError == true๋กœ ์ธ์‹ํ•จ.
    import 'package:flutter/material.dart';
    
    void main() {
      runApp(DiaryApp());
    }
    
    class DiaryApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(title: 'Future Error Demo', home: FutureErrorDemo());
      }
    }
    
    class FutureErrorDemo extends StatefulWidget {
      @override
      _FutureErrorDemoState createState() => _FutureErrorDemoState();
    }
    
    class _FutureErrorDemoState extends State<FutureErrorDemo> {
      late Future<String> _calculation;
    
      @override
      void initState() {
        super.initState();
        // 2์ดˆ ํ›„ ์—๋Ÿฌ ๋˜์ง€๊ธฐ (์ •ํ™•ํžˆ '์—๋Ÿฌ ๋ฐœ์ƒ: ๋ฌธ์ œ ๋ฐœ์ƒ' ์ถœ๋ ฅ๋จ)
        _calculation = Future.delayed(Duration(seconds: 2), () => throw '๋ฌธ์ œ ๋ฐœ์ƒ');
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('์—๋Ÿฌ ํ…Œ์ŠคํŠธ')),
          body: FutureBuilder<String>(
            future: _calculation,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return Center(child: CircularProgressIndicator());
              } else if (snapshot.hasError) {
                return Center(
                  child: Text(
                    '์—๋Ÿฌ ๋ฐœ์ƒ: ${snapshot.error}', // ← ์—ฌ๊ธฐ์— ์ •ํ™•ํžˆ ์ถœ๋ ฅ๋จ
                    style: TextStyle(color: Colors.red, fontSize: 18),
                  ),
                );
              } else {
                return Center(child: Text(snapshot.data!));
              }
            },
          ),
        );
      }
    }

 


๐Ÿ”น StreamBuilder

โœ… ๊ฐœ๋… ์ •๋ฆฌ

StreamBuilder๋Š” ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์„ ๊ฐ์ง€ํ•˜์—ฌ UI๋ฅผ ์ž๋™์œผ๋กœ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.
์„ผ์„œ ๋ฐ์ดํ„ฐ, WebSocket ํ†ต์‹ , ํƒ€์ด๋จธ ๋“ฑ์— ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”ง ์ฃผ์š” ์†์„ฑ

์†์„ฑ ์„ค๋ช…
stream ๊ตฌ๋…ํ•  Stream ๊ฐ์ฒด
builder Stream์—์„œ ๋ฐœํ–‰๋œ ๊ฐ’์„ ๊ธฐ๋ฐ˜์œผ๋กœ UI๋ฅผ ์ƒ์„ฑ
snapshot.connectionState waiting, active, done ๋“ฑ ์ƒํƒœ ํ™•์ธ
snapshot.hasData / hasError ๋ฐ์ดํ„ฐ ๋„์ฐฉ ์—ฌ๋ถ€ ๋ฐ ์—๋Ÿฌ ์—ฌ๋ถ€

๐Ÿงช ๊ธฐ๋ณธ ์˜ˆ์ œ

https://youtube.com/shorts/e8BHDkDwoKY?feature=share

 

class StreamDemo extends StatefulWidget {
  @override
  _StreamDemoState createState() => _StreamDemoState();
}

class _StreamDemoState extends State<StreamDemo> {
  late Stream<int> _counterStream;

  @override
  void initState() {
    super.initState();
    _counterStream = Stream.periodic(Duration(seconds: 1), (count) => count).take(10);
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: _counterStream,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Center(child: Text('์ŠคํŠธ๋ฆผ ๋Œ€๊ธฐ ์ค‘...'));
        } else if (snapshot.hasError) {
          return Center(child: Text('์—๋Ÿฌ: ${snapshot.error}'));
        } else if (snapshot.connectionState == ConnectionState.active) {
          return Center(child: Text('์นด์šดํŠธ: ${snapshot.data}'));
        } else {
          return Center(child: Text('์ŠคํŠธ๋ฆผ ์ข…๋ฃŒ'));
        }
      },
    );
  }
}

๐Ÿงฉ ์‹ค์Šต ๊ณผ์ œ

  • Stream.periodic(Duration(seconds: 5), (_) => Random().nextInt(100))๋กœ ๋žœ๋ค ์ˆซ์ž ํ‘œ์‹œ  : ์นด์šดํŠธ ๊ฐ’์ด ๋žœ๋ค์œผ๋กœ ์ถœ๋ ฅ
  • https://youtube.com/shorts/pC3edBiMIZ8
  • 0~100์‚ฌ์ด์˜ ๋žœ๋ค ๊ฐ’ ๊นŒ์ง€ ์นด์šดํŠธ
    class _StreamDemoState extends State<StreamDemo> {
      late Stream<int> _counterStream;
      var num = Random().nextInt(100); // 0 ์ด์ƒ 100 ๋ฏธ๋งŒ์˜ ์ •์ˆ˜
    
      @override
      void initState() {
        super.initState();
        _counterStream = Stream.periodic(
          Duration(seconds: 1),
          (count) => count,
        ).take(num);
      }

if (i == 3) throw Exception('์—๋Ÿฌ ๋ฐœ์ƒ') ํ˜•ํƒœ๋กœ ์—๋Ÿฌ ํ…Œ์ŠคํŠธ


๐Ÿ”น TabBar & TabBarView

โœ… ๊ฐœ๋… ์ •๋ฆฌ

DefaultTabController๋กœ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , TabBar์™€ TabBarView๋ฅผ ์—ฐ๊ณ„ํ•˜์—ฌ ํƒญ UI๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”ง ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ

๊ตฌ์„ฑ์š”์†Œ ์„ค๋ช…
TabBar ํƒญ ๋ฒ„ํŠผ UI
TabBarView ํƒญ์— ๋”ฐ๋ฅธ ์ฝ˜ํ…์ธ  UI
length ํƒญ ๊ฐœ์ˆ˜
TabController ์ˆ˜๋™ ์ œ์–ด์‹œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ (์„ ํƒ ์ด๋ฒคํŠธ ๊ฐ์ง€ ๋“ฑ)

๐Ÿงช ๊ธฐ๋ณธ ์˜ˆ์ œ

class TabDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: Text('ํƒญ ์˜ˆ์ œ'),
          bottom: TabBar(
            tabs: [
              Tab(icon: Icon(Icons.home), text: 'ํ™ˆ'),
              Tab(icon: Icon(Icons.person), text: 'ํ”„๋กœํ•„'),
              Tab(icon: Icon(Icons.settings), text: '์„ค์ •'),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            Center(child: Text('ํ™ˆ ํ™”๋ฉด')),
            Center(child: Text('ํ”„๋กœํ•„ ํ™”๋ฉด')),
            Center(child: Text('์„ค์ • ํ™”๋ฉด')),
          ],
        ),
      ),
    );
  }
}

๐Ÿงฉ ์‹ค์Šต ๊ณผ์ œ

  • ํƒญ์— ์•„์ด์ฝ˜๊ณผ ํ…์ŠคํŠธ๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
  • TabController๋ฅผ ์ˆ˜๋™์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ , ํƒญ ๋ณ€๊ฒฝ ์‹œ index๋ฅผ ์ถœ๋ ฅํ•ด๋ณด๊ธฐ


๐Ÿ”น PageView

โœ… ๊ฐœ๋… ์ •๋ฆฌ

PageView๋Š” ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€๋ฅผ ์Šค์™€์ดํ”„๋กœ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ๋Š” ์œ„์ ฏ์ž…๋‹ˆ๋‹ค.
PageController๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ”„๋กœ๊ทธ๋žจ์ ์œผ๋กœ ํŽ˜์ด์ง€ ์ „ํ™˜๋„ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ํŽ˜์ด์ง€ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ๋„ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”ง ์ฃผ์š” ์†์„ฑ

์†์„ฑ ์„ค๋ช…
children ํŽ˜์ด์ง€๋กœ ํ‘œ์‹œํ•  ์œ„์ ฏ ๋ฆฌ์ŠคํŠธ
controller PageController๋ฅผ ํ†ตํ•ด ์ดˆ๊ธฐ ํŽ˜์ด์ง€, ์ด๋™ ์ œ์–ด
onPageChanged ํŽ˜์ด์ง€ ์ „ํ™˜ ์‹œ ์ฝœ๋ฐฑ ์‹คํ–‰

๐Ÿงช ๊ธฐ๋ณธ ์˜ˆ์ œ

class PageDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return PageView(
      children: [
        Container(color: Colors.red),
        Container(color: Colors.green),
        Container(color: Colors.blue),
      ],
    );
  }
}

๐Ÿงฉ ์‹ค์Šต ๊ณผ์ œ

  • PageController(initialPage: 1) ์„ค์ •
  • ํŽ˜์ด์ง€ ์ด๋™ ๋ฒ„ํŠผ ๊ตฌํ˜„ (controller.jumpToPage, animateToPage)
  • onPageChanged๋ฅผ ํ†ตํ•ด ํ˜„์žฌ ํŽ˜์ด์ง€ ์ธ๋ฑ์Šค๋ฅผ ์ถœ๋ ฅ

๐Ÿ”น ๋ณตํ•ฉ UI ์‹ค์Šต ํ”„๋กœ์ ํŠธ

โœจ ์‹ค์Šต: ์ผ๊ธฐ์žฅ ์•ฑ ํ™”๋ฉด ์„ค๊ณ„

  1. ๋ฉ”์ธ ํ™”๋ฉด
    • PageView๋กœ ์›”๋ณ„ ๋‹ฌ๋ ฅ ํ™”๋ฉด์„ ์ขŒ์šฐ ์Šค์™€์ดํ”„
    • GridView๋กœ ๋‹ฌ๋ ฅ ๊ตฌ์„ฑ
  2. ์ผ๊ธฐ ์ž‘์„ฑ ํ™”๋ฉด
    • FutureBuilder๋กœ ์ด๋ฏธ์ง€ ์ฒจ๋ถ€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ตฌํ˜„
    • Future.delayed๋กœ 2์ดˆ ํ›„ ์ด๋ฏธ์ง€ URL ๋กœ๋”ฉ, Image.network๋กœ ํ‘œ์‹œ
  3. ์ผ์ • ์ถ”๊ฐ€/์ˆ˜์ • ํ™”๋ฉด
    • TabBar ๋˜๋Š” PageView๋กœ ๋ฐ˜๋ณต ์„ค์ • ํ™”๋ฉด ๊ตฌ์„ฑ
    • ์˜ˆ: ํƒญ "๋งค์ผ / ๋งค์ฃผ / ๋งค์›”" ์„ ํƒ ์‹œ ๊ฐ๊ธฐ ๋‹ค๋ฅธ ์˜ต์…˜ ํ‘œ์‹œ