๐Ÿ‹ Flutter

[Flutter] Riverpod์ด๋‚˜ ์ƒํƒœ๊ด€๋ฆฌ๋กœ Localization ์ฆ‰์‹œ ๋ฐ”๋€Œ๊ฒŒ ๋งŒ๋“ค๊ธฐ

Dogfoot_JW 2024. 10. 7. 00:31

๊ฐœ์š”


ํ”Œ๋Ÿฌํ„ฐ์—์„œ ํ˜„์ง€ํ™”(Localization)๋ฅผ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•œ ํŒจํ‚ค์ง€๋Š” ๋‹ค์–‘ํ•˜๋‹ค.

๊ฐ€์žฅ ๋Œ€ํ‘œ์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ํŒจํ‚ค์ง€์ธ easy_localization๊ณผ ํ”Œ๋Ÿฌํ„ฐ ๊ธฐ๋ณธ ํŒจํ‚ค์ง€์ธ flutter_localizations๊ฐ€ ์žˆ๋‹ค (flutter_localization ์•„๋‹˜)

๊ทธ๋Ÿฌ๋˜ ์ค‘ ์‹ค์‹œ๊ฐ„ ๋ฐ˜์‘ํ˜• ์•ฑ์„ ๊ฐœ๋ฐœํ•˜๋‹ค๊ฐ€ localization์„ ์ ์šฉํ–ˆ์„ ๋•Œ ๋งค๋ฒˆ ์•ฑ์„ ์žฌ์‹œ์ž‘ํ•œ๋‹ค๋Š” ์ ์ด ๋‚˜๋ฅผ ๊ดด๋กญ๊ฒŒ ํ–ˆ๋‹ค.

๋˜ํ•œ ์™ธ๋ถ€ ํŒจํ‚ค์ง€๋ฅผ ๋„์›€๋ฐ›์•„์„œ localization์„ ์ ์šฉํ•˜๋ ค๋‹ˆ ์•ฑ์ด ๋ฌด๊ฑฐ์›Œ์ง€๋Š” ๋А๋‚Œ๋„ ๋“œ๋Š” ๊ฒŒ ์ข‹์ง€ ์•Š์•˜๋‹ค.

์ด์ฐธ์— ์ƒˆ๋กญ๊ฒŒ ๊ฐœ๋ฐœ์„ ํ•˜๋Š” ๊ฒธ ๋‚˜๋Š” ์™ธ๋ถ€ํŒจํ‚ค์ง€์˜ ๋„์›€ ์—†์ด ์ง์ ‘ localization์„ ์ ์šฉํ•ด ๋ณด์ž๊ณ  ๊ฒฐ์‹ฌ์„ ํ–ˆ๊ณ , ๊น”๋”ํ•˜๊ฒŒ ๋‚˜๋งŒ์˜ ๋ฐฉ์‹์œผ๋กœ localization์„ ์ ์šฉํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. 

 

๊ตฌ์„ฑ


  • Flutter 3.10.x
  • Android Studio
  • Riverpod (๋‹ค๋ฅธ ์ƒํƒœ๊ด€๋ฆฌ ํŒจํ‚ค์ง€๋กœ๋„ ๊ฐ€๋Šฅ)
  • Android (Web, IOS, PC ๋ชจ๋‘ ๊ฐ€๋Šฅ

 

์ง„ํ–‰


ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ฐ€์žฅ ๋จผ์ € ํ•  ์ผ์€ riverpod ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•˜๋„๋ก ํ•œ๋‹ค.

 

flutter pub add riverpod

 

๋งŒ์•ฝ ๋‹ค๋ฅธ ์ƒํƒœ๊ด€๋ฆฌํˆด์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ๋ฐ”๊ฟ”๋„ ๋œ๋‹ค.

 

์ด์ œ ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ์— ๋Œ€ํ•ด ์„ค๋ช…์„ ํ•˜๋„๋ก ํ•˜๊ฒ ๋‹ค.

 

 

  • app_localizations.dart: ์‚ฌ์ „ ์ •์˜๋œ ํ˜„์ง€ํ™” ๊ด€๋ จ ์ƒ์ˆ˜ ๋ฐ ์ •์  ๋ณ€์ˆ˜๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.
  • widget_ref_extension.dart: RiverPod์—์„œ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•œ ์ฃผ์š” ํด๋ž˜์Šค์ธ WidgetRef๋ฅผ ํ™•์žฅํ•˜์—ฌ Localization๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•จ. ๋งŒ์•ฝ Riverpod์— ์˜ํ•œ ChangeNotification์„ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†๊ฑฐ๋‚˜ WidgetRef ์ž์ฒด๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด, current_language_state๋ฅผ ์‹ฑ๊ธ€ํ†ค ํŒจํ„ด์œผ๋กœ ๋ณ€๊ฒฝํ•œ ํ›„ BuildContext์˜ extension๋กœ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•ด๋„ ๋œ๋‹ค. (๋Œ€์‹  ์œ„์ ฏ์„ ์ง์ ‘ ์ƒˆ๋กœ๊ณ ์ณ์•ผ ํ•ด๋‹น ํ™”๋ฉด์—์„œ ์–ธ์–ด๊ฐ€ ๋ณ€๊ฒฝ๋จ)
  • current_language_state.dart: ํ˜„์žฌ ์–ธ์–ด์— ๋Œ€ํ•œ ์ƒํƒœ๋ฅผ ์ œ๊ณตํ•ด์ค€๋‹ค.
  • home_screen.dart: UI ๊ตฌ์„ฑ

 

์ด์ œ ์ฝ”๋“œ๋ฅผ ์ฐจ๊ทผ์ฐจ๊ทผ ์‚ดํŽด๋ณด๋ฉฐ ์ดํ•ดํ•˜๋„๋ก ํ•˜๊ฒ ๋‹ค.

 

๋จผ์ € app_localization.dart๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ฒ ๋‹ค.

 

<app_localization.dart>

//์™ธ๋ถ€์—์„œ ์ƒํƒœ๋ฅผ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•œ ์–ธ์–ด์ฝ”๋“œ
enum LanguageCode{
  ko,
  en
}

class AppLocalizations{

  //์ •์ ๋ณ€์ˆ˜๋กœ ์–ธ์–ด๋“ค์„ ๊ฐ€์ ธ์™€ state๋กœ ์ œ๊ณตํ•  ์ค€๋น„
  static final KoreanLocalization _koreanLocalization = KoreanLocalization();
  static final EnglishLocalization _englishLocalization = EnglishLocalization();

  static Localization get(LanguageCode code){
    if(code == LanguageCode.ko) {
      return _koreanLocalization;
    }else if(code== LanguageCode.en){
      return _englishLocalization;
    }else{
      return _koreanLocalization;
    }
  }
}

//๊ฐ ์–ธ์–ด๋ณ„ ๊ณตํ†ต์œผ๋กœ ์„ ์–ธํ•ด์•ผํ•  ๋ณ€์ˆ˜๋“ค์„ Localization์ด๋ผ๋Š” ์ถ”์ƒํด๋ž˜์Šค์— ์„ ์–ธ
abstract class Localization{
  //์ถ”์ƒํด๋ž˜์Šค์˜ ๋ณ€์ˆ˜๋“ค์€ ์„ ์–ธ๋งŒ ๊ฐ€๋Šฅํ•˜๊ณ  ํ• ๋‹น๋˜๋ฉด ์•ˆ๋จ.
  String get start_page_hello;
  String get start_page_change_to_kor;
  String get start_page_change_to_eng;
}

//์ถ”์ƒํด๋ž˜์Šค์˜ ๊ตฌํ˜„
class KoreanLocalization implements Localization{
  
  //์ถ”์ƒํด๋ž˜์Šค์˜ ๋ณ€์ˆ˜ ์˜ค๋ฒ„๋ผ์ด๋“œ(์žฌํ• ๋‹น)
  @override
  String get start_page_hello => '์•ˆ๋…•ํ•˜์„ธ์š”';

  @override
  String get start_page_change_lang => '์–ธ์–ด๋ณ€๊ฒฝ';

  @override
  String get start_page_change_to_kor => 'ํ•œ๊ตญ์–ด๋กœ';

  @override
  String get start_page_change_to_eng => '์˜์–ด๋กœ';
}

class EnglishLocalization implements Localization{
  
  @override
  String get start_page_hello => 'Hello';

  @override
  String get start_page_change_lang => 'Change Language';

  @override
  String get start_page_change_to_kor => 'to Korean';

  @override
  String get start_page_change_to_eng => 'to English';
}

 

ํ•ด๋‹น ์†Œ์Šค์ฝ”๋“œ ํŒŒ์ผ์—์„œ๋Š” Localization์—์„œ ์‚ฌ์šฉํ•  ์–ธ์–ด์ฝ”๋“œ๋“ค, ์ •์ ํ•จ์ˆ˜, ์–ธ์–ด๋ณ„ ์ •์ ๊ฐ์ฒด๋“ค์„ ์ œ๊ณตํ•˜๊ณ  ์žˆ๋‹ค.

์ฃผ๋กœ AppLocalizations ํด๋ž˜์Šค์—์„œ ์–ธ์–ด์— ๋Œ€ํ•œ ์ •์ ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธ ๋ฐ ์ œ๊ณต์„ ํ•ด์ฃผ๋Š”๋ฐ ํ•ด๋‹น ํด๋ž˜์Šค์˜ get๋ฉ”์„œ๋“œ๋Š” extension์—์„œ ํ˜ธ์ถœํ•ด ์•Œ๋งž์€ ์–ธ์–ด๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ฃผ๋„๋ก ํ•œ๋‹ค.

 

 

<current_language_state.dart>

import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../constants/app_localizations.dart';

final currentLanguageProvider =
    StateNotifierProvider<CurrentLanguageNotifier, LanguageCode>((ref) {
  return CurrentLanguageNotifier();
});

class CurrentLanguageNotifier extends StateNotifier<LanguageCode> {
  CurrentLanguageNotifier() : super(LanguageCode.ko);

  LanguageCode get currentLanguage => state;

  void changeLanguage(LanguageCode targetLanguage) {
    state = targetLanguage;
  }
}

 

์—ฌ๊ธฐ์„œ๋Š” ํ˜„์žฌ ์–ธ์–ด์— ๋Œ€ํ•œ ์ƒํƒœ๋ฅผ ์ œ๊ณตํ•ด์ฃผ๊ณ  ์žˆ๋‹ค.

์•ž์„œ ์–˜๊ธฐํ–ˆ๋“ฏ, ๊ตณ์ด RiverpodํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ Bloc, Singleton ๋“ฑ ๋‹ค์–‘ํ•œ ํŒจํ„ด์œผ๋กœ ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ๋‹ค. ๋Œ€์‹  UI ์—…๋ฐ์ดํŠธ์— ๊ด€ํ•œ ๋ถ€๋ถ„์€ ๊ฐ ํŒจํ„ด์— ๋งž๊ฒŒ ์ˆ˜์ •์„ ํ•ด์•ผ ํ•œ๋‹ค. (์‹ฑ๊ธ€ํ†คํŒจํ„ด์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ์ด๋ฏธ ์—ด๋ ค์žˆ๋Š” ํŽ˜์ด์ง€๋“ค์€ ์•ฑ UI๋ฅผ ์ง์ ‘ ์—…๋ฐ์ดํŠธํ•ด์ค˜์•ผ ํ•œ๋‹ค)

์œ„์˜ ์ฝ”๋“œ์—์„œ๋Š” LanguageCode๋ผ๋Š” enumํ˜•์‹์˜ ๋ณ€์ˆ˜๋ฅผ currentLanguage๋กœ ์„ ์–ธํ–ˆ์œผ๋ฉฐ changeLanguage๊ฐ€ ํ˜ธ์ถœ๋˜์–ด ์ƒ์† ๋ณ€์ˆ˜์ธ state์˜ ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๊ฒฝ์šฐ ์ž๋™์œผ๋กœ NotifyListners()๊ฐ€ ํ˜ธ์ถœ๋  ๊ฒƒ์ด๋‹ค.

 

 

<widget_ref_extension.dart>

import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../constants/app_localizations.dart';
import '../states/current_language_state.dart';

//RiverPod์˜ ํ•ต์‹ฌ ํด๋ž˜์Šค์ธ WidgetRef ํด๋ž˜์Šค๋ฅผ ํ™•์žฅ
extension WidgetRefExtension on WidgetRef{
  Localization get localizations => AppLocalizations.get(watch(currentLanguageProvider));
}

 

์ƒํƒœ, ์ •์ ๋ณ€์ˆ˜๋“ค ๋“ฑ ๋ชจ๋“  ๊ตฌ์„ฑ์š”์†Œ๋“ค์„ ๊ฐ–์ถ”์—ˆ๋‹ค๋ฉด ์ด์ œ ์‚ฌ์šฉ์„ ํ•˜๊ธฐ ์œ„ํ•œ ์ค€๋น„๋ฅผ ํ•ด์•ผ ํ•œ๋‹ค.

riverpodํŒจํ„ด์œผ๋กœ ์ง์ ‘ UI ๋‚ด์—์„œ ์–ธ์–ด ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ๋ฆฌ์Šค๋„ˆ๋“ค์„ ๋“ฑ๋กํ•ด ์ค˜๋„ ๋˜์ง€๋งŒ, ๋ชจ๋“  Widget์— ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋“ฑ๋กํ•˜๋Š” ๊ฑด ์“ธ๋ฐ์—†์ด ๊ธ€์ž๋ฅผ ๋‚ญ๋น„ํ•˜๋Š” ์…ˆ์ด๋‹ค.

์กฐ๊ธˆ์ด๋ผ๋„ ๋” ์ ์€ ์ฝ”๋“œ๋กœ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ์ตœ๋Œ€ํ•œ ๊ทธ๋ ‡๊ฒŒ ํ•˜๋Š” ๊ฒŒ ์ข‹๋‹ค.

๊ทธ๋ ‡๊ธฐ์— ๋‚˜๋Š” flutter ์•ฑ์„ ๊ฐœ๋ฐœํ•  ๋•Œ extension์„ ์ž์ฃผ ํ™œ์šฉํ•˜๋Š” ํŽธ์ด๊ณ , ์ด๋ฒˆ์—๋„ ์—ญ์‹œ WidgetRef ํด๋ž˜์Šค ๋˜ํ•œ Consumer Widget์œผ๋กœ ์„ ์–ธ๋œ ๋ชจ๋“  ๊ณณ์—์„œ ์‚ฌ์šฉ์„ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— WidgetRef๋งŒ์„ ์œ„ํ•œ extension์„ ๋งŒ๋“ค์–ด ์คฌ๋‹ค.

 

์—ฌ๊ธฐ๊นŒ์ง€๊ฐ€ Localization๋กœ์ง๊ณผ ๊ด€๋ จ๋œ ์ฝ”๋“œ์˜ ๋์ด๋‹ค.

 

๋‚จ์€ ๊ฑด UI ๊ตฌ์„ฑ๊ณผ main.dart์˜ ์ˆ˜์ •์ด๋‹ค.

 

<home_screen.dart>

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_self_localization/constants/app_localizations.dart';
import 'package:flutter_self_localization/extensions/widget_ref_extension.dart';

import '../states/current_language_state.dart';

class HomeScreen extends ConsumerWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('MyLocalization'),
      ),
      body: SizedBox(
        width: double.infinity,
        child: Column(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Text(ref.localizations.start_page_hello),
            const SizedBox(height: 50,),
            ElevatedButton(
                onPressed: () => _onPressedToKorean(ref),
                child: Text(ref.localizations.start_page_change_to_kor)),
            const SizedBox(height: 10,),
            ElevatedButton(
                onPressed: () => _onPressedToEnglish(ref),
                child: Text(ref.localizations.start_page_change_to_eng)),
          ],
        ),
      ),
    );
  }

  void _onPressedToKorean(WidgetRef ref) {
    final currentLanguageNotifier = ref.read(currentLanguageProvider.notifier);
    currentLanguageNotifier.changeLanguage(LanguageCode.ko);
  }

  void _onPressedToEnglish(WidgetRef ref) {
    final currentLanguageNotifier = ref.read(currentLanguageProvider.notifier);
    currentLanguageNotifier.changeLanguage(LanguageCode.en);
  }
}

 

UI๊ตฌ์„ฑ์€ ์ด๋ ‡๊ฒŒ ํ–ˆ๊ณ  ConsumerWidget์„ ํ†ตํ•ด ์–ธ์–ด๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ๊ตฌ์„ฑํ•ด ์คฌ๋‹ค.

 

 

<main.dart>

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_self_localization/ui/home_screen.dart';

void main() {
  runApp(
    //RiverPod์˜ ํ”„๋กœ๋ฐ”์ด๋” ์‚ฌ์šฉ์„ ์œ„ํ•ด ๋ž˜ํ•‘
    const ProviderScope(child: MyApp())
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: false,
      ),
      home: const HomeScreen(),
    );
  }
}

 

main.dart์—์„œ๋Š” ๊ฐ„๋‹จํžˆ ProviderScope๋งŒ ๋ž˜ํ•‘ ํ•˜๋„๋ก ํ–ˆ๋‹ค.

 

์ด ์ƒํƒœ๋กœ ์•ฑ์„ ์‹คํ–‰ํ•ด ๋ณด์ž.

 

 

 

 

์ฒ˜์Œ ์ ์šฉ๋œ ์–ธ์–ด๊ฐ€ ํ•œ๊ตญ์–ด๋กœ ๋˜์–ด์žˆ์–ด์„œ ์ง€๊ธˆ์€ ํ•œ๊ธ€๋กœ ๋– ์žˆ๋Š” ์ƒํƒœ๋‹ค.

 

์˜์–ด๋กœ ๋ฒ„ํŠผ์„ ํด๋ฆญํ–ˆ์„ ๋•Œ UI๊ฐ€ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ ๋˜๋Š” ๋ชจ์Šต์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

 

๊ฒฐ๋ก 


 

๋งŒ์•ฝ ์•ฑ์˜ ๊ทœ๋ชจ๊ฐ€ ์ปค์ง€๊ฒŒ ๋  ๊ฒฝ์šฐ app_localization ์•ˆ์—์„œ ๋ชจ๋“  ๊ฒƒ์„ ์ œ๊ณตํ•˜๊ธฐ์—” ์ฝ”๋“œ์˜ ์–‘์ด ๋งŽ์•„์งˆ๊ฒŒ ๋ป”ํ•˜๋‹ค.

์ด ๊ธ€์˜ ์˜ˆ์ œ๋Š” ์ตœ๋Œ€ํ•œ ์ดํ•ด๋ฅผ ๋•๊ธฐ ์œ„ํ•ด ํ•œ ํŒŒ์ผ ์•ˆ์— ๋ชจ๋“  ๊ฒƒ์„ ๋‹ด์•˜์œผ๋ฉฐ, ์‹ค์ œ๋กœ ์‚ฌ์šฉ์„ ํ•  ๋•Œ๋Š” ์—ญํ• ๋ณ„๋กœ ์ฝ”๋“œ๋“ค์„ ์ž˜ ๋‚˜๋ˆ„์–ด์•ผ ๋œ๋‹ค.