[TIL] Day 34 Riverpod Consumer Widgets & Open-Meteo API

ํ˜„์„œยท2026๋…„ 1์›” 12์ผ

[TIL] Flutter 9๊ธฐ

๋ชฉ๋ก ๋ณด๊ธฐ
46/65
post-thumbnail

๐Ÿ“ Riverpod Consumer Widgets ๊ฐœ๋… ์ •๋ฆฌ

โœ๏ธ Consumer Widgets๋ž€?

Provider๊ฐ€ ์ œ๊ณตํ•˜๋Š” ์ƒํƒœ๋ฅผ โ€œ๊ตฌ๋…(consume)โ€ํ•ด์„œ UI๋กœ ๊ทธ๋ฆฌ๋Š” ์œ„์ ฏ
Provider โ†” UI๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ์ ‘์  ์—ญํ• ์„ ํ•˜๋Š” ์œ„์ ฏ

โœ๏ธ ์™œ Consumer Widget์ด ํ•„์š”ํ• ๊นŒ?

Flutter ๊ธฐ๋ณธ ์œ„์ ฏ์˜ ํ•œ๊ณ„

์ผ๋ฐ˜ StatelessWidget / StatefulWidget
Provider ์ƒํƒœ๋ฅผ ์ง์ ‘ ๊ฐ์ง€ํ•  ์ˆ˜ ์—†์Œ
์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ๋ฆฌ๋นŒ๋“œ โŒ

๊ทธ๋ž˜์„œ Riverpod๊ฐ€ ์ œ๊ณตํ•˜๋Š” Consumer ๊ณ„์—ด ์œ„์ ฏ์ด ํ•„์š”ํ•œ ๊ฒƒ

โœ๏ธ ConsumerWidget / Consumer / ConsumerStatefulWidget ์ฐจ์ด

1๏ธโƒฃ ConsumerWidget

๊ฐœ๋…

StatelessWidget + Consumer ํ•ฉ์ณ์ง„ ํ˜•ํƒœ
๊ฐ€์žฅ ๋งŽ์ด ์“ฐ๋Š” ๊ธฐ๋ณธํ˜•

class MyWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(counterProvider);
    return Text('$state');
  }
}

ํŠน์ง•

์ƒํƒœ ๊ตฌ๋… ๊ฐ€๋Šฅ (ref.watch)
๋‚ด๋ถ€์— State ์—†์Œ
์ฝ”๋“œ ์งง๊ณ  ๊ฐ€๋…์„ฑ ์ข‹์Œ

์–ธ์ œ ์“ฐ๋‚˜?

UI๊ฐ€ ์™ธ๋ถ€ ์ƒํƒœ(provider) ์—๋งŒ ์˜์กดํ•  ๋•Œ
๋ฒ„ํŠผ ํด๋ฆญ, ์•„์ด์ฝ˜ ํ† ๊ธ€, ๋ฆฌ์ŠคํŠธ ์•„์ดํ…œ ๋“ฑ

์žฅ์  / ๋‹จ์ 

โœ… ๋‹จ์ˆœ, ๊น”๋”
โŒ initState, dispose ์‚ฌ์šฉ ๋ถˆ๊ฐ€

2๏ธโƒฃ Consumer

๊ฐœ๋…

์ผ๋ฐ˜ ์œ„์ ฏ ์•ˆ์—์„œ ์ผ๋ถ€๋งŒ ์ƒํƒœ ๊ตฌ๋…ํ•  ๋•Œ ์‚ฌ์šฉ
๋นŒ๋“œ ๋ฒ”์œ„๋ฅผ ์ตœ์†Œํ™”ํ•˜๋Š” ์šฉ๋„

class MyWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('๊ณ ์ • UI'),
        Consumer(
          builder: (context, ref, child) {
            final state = ref.watch(counterProvider);
            return Text('$state');
          },
        ),
      ],
    );
  }
}

ํŠน์ง•

๋ถ€๋ชจ๋Š” StatelessWidget ์œ ์ง€
Consumer ๋‚ด๋ถ€๋งŒ ๋ฆฌ๋นŒ๋“œ

์–ธ์ œ ์“ฐ๋‚˜?

UI ์ผ๋ถ€๋งŒ ์ƒํƒœ์— ๋ฐ˜์‘ํ•ด์•ผ ํ•  ๋•Œ
์„ฑ๋Šฅ ์ตœ์ ํ™” ํ•„์š”ํ•  ๋•Œ

์žฅ์  / ๋‹จ์ 

โœ… ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋นŒ๋“œ ๋ฐฉ์ง€
โŒ ์ฝ”๋“œ๊ฐ€ ์ค‘์ฒฉ๋˜๋ฉด ๊ฐ€๋…์„ฑ ๋–จ์–ด์ง

3๏ธโƒฃ ConsumerStatefulWidget

๊ฐœ๋…

StatefulWidget + Consumer
๋ผ์ดํ”„์‚ฌ์ดํด + provider ๋‘˜ ๋‹ค ํ•„์š”ํ•  ๋•Œ

class MyWidget extends ConsumerStatefulWidget {
  
  ConsumerState<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends ConsumerState<MyWidget> {
  
  void initState() {
    super.initState();
    ref.read(counterProvider.notifier).init();
  }

  
  Widget build(BuildContext context) {
    final state = ref.watch(counterProvider);
    return Text('$state');
  }
}

ํŠน์ง•

initState, dispose ์‚ฌ์šฉ ๊ฐ€๋Šฅ
ref๋ฅผ State ์•ˆ์—์„œ ์ง์ ‘ ์‚ฌ์šฉ

์–ธ์ œ ์“ฐ๋‚˜?

ํ™”๋ฉด ์ง„์ž… ์‹œ API ํ˜ธ์ถœ
AnimationController
ScrollController
Timer, Stream ๊ด€๋ฆฌ

์žฅ์  / ๋‹จ์ 

โœ… ์ œ์–ด๋ ฅ ์ตœ๊ณ 
โŒ ๊ตฌ์กฐ๊ฐ€ ๊ฐ€์žฅ ๋ฌด๊ฑฐ์›€


๐Ÿ“ Open-Meteo API

Open-Meteo๋Š” ๋ฌด๋ฃŒ(๋น„์ƒ์—…์ )๋กœ ์ „ ์„ธ๊ณ„ ๋‚ ์”จ ๋ฐ์ดํ„ฐ๋ฅผ JSON์œผ๋กœ ์ œ๊ณตํ•˜๋Š” REST API

  • API ํ‚ค ์—†์ด๋„ ์ด์šฉ ๊ฐ€๋Šฅ โœ”
  • ๊ธ€๋กœ๋ฒŒ ์ปค๋ฒ„๋ฆฌ์ง€ & ๊ณ ํ•ด์ƒ๋„ ๋ชจ๋ธ ๊ธฐ๋ฐ˜ ์˜ˆ๋ณด
  • HTTP ์š”์ฒญ โ†’ JSON ์‘๋‹ต ๊ตฌ์กฐ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • ๊ธฐ์ƒ์ฒญ/๊ตญ๊ฐ€ ๋ชจ๋ธ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ๊ฐ€์žฅ ์ ํ•ฉํ•œ ์˜ˆ๋ณด ์ œ๊ณต

โœ๏ธ ์ œ๊ณตํ•˜๋Š” ์ฃผ์š” API ์ข…๋ฅ˜

API ์ด๋ฆ„์„ค๋ช…
Weather Forecast API์‹ค์‹œ๊ฐ„ + ์˜ˆ๋ณด(์ตœ๋Œ€ 16์ผ) ๋‚ ์”จ ๋ฐ์ดํ„ฐ (Open Meteo)
Historical Weather API๊ณผ๊ฑฐ ๋‚ ์”จ(์ตœ๋Œ€ 1940๋…„๋Œ€~ํ˜„์žฌ) (Open Meteo)
Climate API์žฅ๊ธฐ ๊ธฐํ›„ ๋ฐ์ดํ„ฐ/๋ชจ๋ธ ๊ธฐ๋ฐ˜ ํ†ต๊ณ„ (Open Meteo)
Marine Weather APIํ•ด์–‘ ๊ธฐ์ƒ/ํŒŒ๋„/์กฐ์œ„ ๋ฐ์ดํ„ฐ (Open Meteo)
ECMWF API์œ ๋Ÿฝ์ค‘๊ธฐ์˜ˆ๋ณด์„ผํ„ฐ ๋ชจ๋ธ ๊ธฐ๋ฐ˜ ์˜ˆ๋ณด (Open Meteo)
MET Norway API๋ถ์œ ๋Ÿฝ ์ง€์—ญ 1km๊ธ‰ ๋‹จ๊ธฐ ์˜ˆ๋ณด (Open Meteo)

โœ๏ธ Weather Forecast API

๊ธฐ๋ณธ ์—”๋“œํฌ์ธํŠธ

GET https://api.open-meteo.com/v1/forecast

๋ชจ๋“  ๋ฐ์ดํ„ฐ๋Š” Query Parameter๋กœ ์ „๋‹ฌํ•จ

ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ

ํŒŒ๋ผ๋ฏธํ„ฐ์„ค๋ช…
latitude์œ„๋„
longitude๊ฒฝ๋„
?latitude=37.57&longitude=126.98

์ขŒํ‘œ๊ณ„๋Š” WGS84 (์ผ๋ฐ˜์ ์ธ GPS ์ขŒํ‘œ)

์ฃผ์š” ๋ฐ์ดํ„ฐ ํƒ€์ž…

Weather Forecast API๋Š” ํฌ๊ฒŒ 3์ข…๋ฅ˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณต

1. Current (ํ˜„์žฌ ๋‚ ์”จ)

ํ˜„์žฌ ์‹œ๊ฐ ๊ธฐ์ค€์˜ ์‹ค์‹œ๊ฐ„ ๋‚ ์”จ
current= ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฌ์šฉ

&current=temperature_2m,weathercode,wind_speed_10m
๋ณ€์ˆ˜์„ค๋ช…
temperature_2mํ˜„์žฌ ๊ธฐ์˜จ (2m ๋†’์ด)
weathercode๋‚ ์”จ ์ƒํƒœ ์ฝ”๋“œ
wind_speed_10mํ’์†
is_day๋‚ฎ/๋ฐค ์—ฌ๋ถ€ (1 / 0)

2. Hourly (์‹œ๊ฐ„๋ณ„ ์˜ˆ๋ณด)

๋ณดํ†ต 48~72์‹œ๊ฐ„ ์ด์ƒ
1์‹œ๊ฐ„ ๋‹จ์œ„ ๋ฐ์ดํ„ฐ

&hourly=temperature_2m,precipitation_probability,wind_speed_10m
๋ณ€์ˆ˜์„ค๋ช…
temperature_2m๊ธฐ์˜จ
relative_humidity_2m์Šต๋„
apparent_temperature์ฒด๊ฐ ์˜จ๋„
precipitation๊ฐ•์ˆ˜๋Ÿ‰
precipitation_probability๊ฐ•์ˆ˜ ํ™•๋ฅ 
weathercode๋‚ ์”จ ์ฝ”๋“œ
cloudcover๊ตฌ๋ฆ„๋Ÿ‰
wind_speed_10mํ’์†
wind_direction_10mํ’ํ–ฅ

3. Daily (์ผ๋ณ„ ์˜ˆ๋ณด)

์ตœ๋Œ€ 16์ผ
ํ•˜๋ฃจ ์š”์•ฝ ๋ฐ์ดํ„ฐ (์ตœ๊ณ /์ตœ์ € ๋“ฑ)

&daily=temperature_2m_max,temperature_2m_min,weathercode
๋ณ€์ˆ˜์„ค๋ช…
temperature_2m_max์ตœ๊ณ  ๊ธฐ์˜จ
temperature_2m_min์ตœ์ € ๊ธฐ์˜จ
precipitation_sum์ผ ๊ฐ•์ˆ˜๋Ÿ‰
precipitation_probability_max์ตœ๋Œ€ ๊ฐ•์ˆ˜ ํ™•๋ฅ 
sunrise / sunset์ผ์ถœ / ์ผ๋ชฐ
uv_index_max์ตœ๋Œ€ ์ž์™ธ์„  ์ง€์ˆ˜
weathercode์ผ๋ณ„ ๋‚ ์”จ ์ƒํƒœ

Weather Code

์ฝ”๋“œ์˜๋ฏธ
0๋ง‘์Œ
1~3๋Œ€์ฒด๋กœ ๋ง‘์Œ / ๊ตฌ๋ฆ„
45, 48์•ˆ๊ฐœ
51~57์ด์Šฌ๋น„
61~67๋น„
71~77๋ˆˆ
80~82์†Œ๋‚˜๊ธฐ
95์ฒœ๋‘ฅ๋ฒˆ๊ฐœ

โœ๏ธ ์ง์ ‘ ์‚ฌ์šฉํ•ด๋ณด์ž

์ฝ”๋“œ ์ „์ฒด ๊ตฌ์กฐ

static const String _baseUrl = 'https://api.open-meteo.com/v1/forecast';

final uri = Uri.parse(
  '$_baseUrl?latitude=$latitude&longitude=$longitude'
  '&timezone=auto&current=temperature_2m,is_day,wind_speed_10m,weather_code',
);

_baseUrl

static const String _baseUrl = 'https://api.open-meteo.com/v1/forecast';

Open-Meteo Weather Forecast API ์—”๋“œํฌ์ธํŠธ
๋ชจ๋“  ๋‚ ์”จ ์˜ˆ๋ณด ์š”์ฒญ์€ ์ด URL์—์„œ ์‹œ์ž‘ํ•จ
โ€œ์–ด๋””์— ์š”์ฒญํ• ์ง€โ€ ์ •์˜

latitude=$latitude

์š”์ฒญํ•  ์œ„์น˜์˜ ์œ„๋„
๋ถ์œ„: ์–‘์ˆ˜ / ๋‚จ์œ„: ์Œ์ˆ˜
latitude = 37.57 โ†’ ์„œ์šธ ๊ทผ์ฒ˜
๐Ÿ“Œ ๋‚ ์”จ๋ฅผ ์กฐํšŒํ•  ์„ธ๋กœ ์œ„์น˜

longitude=$longitude

์š”์ฒญํ•  ์œ„์น˜์˜ ๊ฒฝ๋„
๋™๊ฒฝ: ์–‘์ˆ˜ / ์„œ๊ฒฝ: ์Œ์ˆ˜
longitude = 126.98 โ†’ ์„œ์šธ ๊ทผ์ฒ˜
๐Ÿ“Œ ๋‚ ์”จ๋ฅผ ์กฐํšŒํ•  ๊ฐ€๋กœ ์œ„์น˜

timezone=auto

&timezone=auto

ํ•ด๋‹น ์ขŒํ‘œ์— ๋งž๋Š” ํ˜„์ง€ ์‹œ๊ฐ„๋Œ€ ์ž๋™ ์ ์šฉ
์˜ˆ: ํ•œ๊ตญ โ†’ Asia/Seoul

์™œ ์ค‘์š”ํ• ๊นŒ?

๋‚ ์งœ / ์‹œ๊ฐ„ ๊ณ„์‚ฐ์„ ์„œ๋ฒ„๊ฐ€ ์ž๋™์œผ๋กœ ๋งž์ถฐ์คŒ
current.time, daily.time ๋“ฑ์ด ๋กœ์ปฌ ์‹œ๊ฐ„ ๊ธฐ์ค€
์•ฑ์—์„œ ์‹œ๊ฐ„ ๋ณ€ํ™˜ ์•ˆ ํ•ด๋„ ๋ผ์„œ ๋งค์šฐ ํŽธํ•จ

current=...

&current=temperature_2m,is_day,wind_speed_10m,weather_code

ํ˜„์žฌ ์‹œ๊ฐ์˜ ๋‚ ์”จ ๋ฐ์ดํ„ฐ๋งŒ ์š”์ฒญ
Open-Meteo๋Š” current, hourly, daily๋ฅผ ๋ช…์‹œํ•˜์ง€ ์•Š์œผ๋ฉด ์•ˆ ์คŒ

temperature_2m

์ง€๋ฉด ์œ„ 2m ๊ธฐ์ค€ ๊ธฐ์˜จ
์šฐ๋ฆฌ๊ฐ€ ์ผ๋ฐ˜์ ์œผ๋กœ ๋งํ•˜๋Š” โ€œํ˜„์žฌ ๊ธฐ์˜จโ€

  • ๋‹จ์œ„: ยฐC (๊ธฐ๋ณธ๊ฐ’)

is_day

ํ˜„์žฌ ์‹œ๊ฐ์ด ๋‚ฎ์ธ์ง€ ๋ฐค์ธ์ง€

๊ฐ’์˜๋ฏธ
1๋‚ฎ
0๋ฐค
  • ๋‹คํฌ๋ชจ๋“œ / ๋‚ฎ๋ฐค ์•„์ด์ฝ˜ ์ „ํ™˜์— ์ž์ฃผ ์‚ฌ์šฉ

wind_speed_10m

์ง€๋ฉด ์œ„ 10m ๊ธฐ์ค€ ํ’์†
์ฒด๊ฐ ์˜จ๋„, ๋ฐ”๋žŒ UI์— ํ™œ์šฉ

  • ๋‹จ์œ„: km/h (๊ธฐ๋ณธ)

weather_code

ํ˜„์žฌ ๋‚ ์”จ ์ƒํƒœ๋ฅผ ์ˆซ์ž๋กœ ํ‘œํ˜„

String _convertWeatherCode(num code) {
  final int c = code.toInt();
  if (c == 0) return '๋ง‘์Œ';
  if ([1, 2, 3].contains(c)) return '๋ถ€๋ถ„ ํ๋ฆผ ๋˜๋Š” ๊ตฌ๋ฆ„ ๋งŽ์Œ';
  if ([45, 48].contains(c)) return '์•ˆ๊ฐœ';
  if ([51, 53, 55].contains(c)) return '์ด์Šฌ๋น„';
  if ([56, 57].contains(c)) return '์–ธ ์ด์Šฌ๋น„';
  if ([61, 63, 65].contains(c)) return '๋น„';
  if ([66, 67].contains(c)) return '์–ธ ๋น„';
  if ([71, 73, 75].contains(c)) return '๋ˆˆ';
  if (c == 77) return '๋ˆˆ์†ก์ด';
  if ([80, 81, 82].contains(c)) return '์†Œ๋‚˜๊ธฐ';
  if ([85, 86].contains(c)) return '๋ˆˆ ์†Œ๋‚˜๊ธฐ';
  if (c == 95) return '์ฒœ๋‘ฅ ๋ฒˆ๊ฐœ';
  if ([96, 99].contains(c)) return '๋ฒˆ๊ฐœ์™€ ์šฐ๋ฐ•';
  return '์•Œ ์ˆ˜ ์—†์Œ';
}

๊ณต๋ถ€ ์†Œ๊ฐ

๊ณผ์ œ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๊ฐœ์ธ์ ์œผ๋กœ ๋ถ€์กฑํ•œ ๋ถ€๋ถ„๊ณผ ์ƒˆ๋กœ์šด api๋ฅผ ๊ณต๋ถ€ํ–ˆ๋‹ค. ์ด์ œ ๊ณผ์ œ ๋งˆ๋ฌด๋ฆฌํ•˜๊ณ  ๋‚จ์€ ๊ฐ•์˜ ๋“ค์–ด๋ด์•ผ๊ฒ ๋‹ค!

0๊ฐœ์˜ ๋Œ“๊ธ€