Flutter

Flutter Day 15: 블로그 만들기 - Dio를 활용 로그인 시스템

@leem 2025. 2. 3. 23:41
Flutter에서 Dio를 활용하여 로그인 시스템을 구축하는 방법을 정리해봤습니다.
로그인 기능을 구현할 때, JWT 기반 인증, 자동 로그인 유지, 상태 관리(ViewModel) 등을 함께 다루면 더욱 완성도가 높아집니다. 이번 글에서는 로그인 시스템 전체 흐름과 Dio + Riverpod을 활용한 구현 방법을 차근차근 알아보겠습니다.

💡 로그인 시스템의 흐름

로그인은 단순해 보이지만, 내부적으로 다음과 같은 과정을 거칩니다.

1️⃣ 사용자가 아이디와 비밀번호 입력 후 로그인 버튼 클릭
2️⃣ Dio를 사용하여 서버에 로그인 요청 (username, password 전송)
3️⃣ 서버가 로그인 성공 시 JWT 토큰 발급
4️⃣ Dio가 서버 응답(JWT 토큰, 사용자 정보) 수신
5️⃣ FlutterSecureStorage에 JWT 토큰 저장 (앱 종료 후에도 유지)
6️⃣ ViewModel이 로그인 상태(sessionUserProvider) 업데이트
7️⃣ UI에서 로그인 상태를 감지하여 화면 변경 (예: 로그인 버튼 → 로그아웃 버튼)


1. Dio 설정 (API 요청을 위한 HTTP 클라이언트)

Dio는 API 요청을 쉽게 처리할 수 있는 Flutter의 HTTP 클라이언트입니다.
전역적으로 설정해두면 여러 곳에서 편리하게 사용할 수 있어요.

import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

// API 서버 주소 설정
final baseUrl = 'http://192.168.0.48:8080';

// Dio 전역 설정
final dio = Dio(
  BaseOptions(
    baseUrl: baseUrl,
    contentType: 'application/json; charset=utf-8',
    validateStatus: (status) => true,  // 200 이외 상태 코드도 허용
  ),
);

// FlutterSecureStorage → JWT 토큰 저장용
const secureStorage = FlutterSecureStorage();​

FlutterSecureStorage를 사용하면 로그인 정보를 안전하게 보관할 수 있습니다.


 2. SessionUser 모델 (로그인한 사용자 정보 저장)

로그인에 성공하면 사용자 정보를 저장할 SessionUser 클래스를 만들어 줍니다.

class SessionUser {
  int? id;
  String? username;
  String? accessToken;
  bool? isLogin;

  SessionUser({
    required this.id,
    required this.username,
    required this.accessToken,
    required this.isLogin,
  });
}​

✅ accessToken → API 요청 시 인증을 위해 사용
✅ isLogin → 현재 로그인 상태를 나타냄


 3. UserRepository (로그인 & 회원가입 API 호출)

이제 Dio를 활용하여 서버와 통신하는 UserRepository를 만들어봅니다.
로그인과 회원가입 API를 요청하는 역할을 합니다.

import 'package:dio/dio.dart';
import '../_core/utils/my_http.dart';

class UserRepository {
  // 회원가입 요청
  Future<Map<String, dynamic>> createUser(Map<String, dynamic> reqData) async {
    Response response = await dio.post('/join', data: reqData);
    return response.data;
  }

  // 로그인 요청
  Future<(Map<String, dynamic>, String)> readUser(Map<String, dynamic> reqData) async {
    Response response = await dio.post('/login', data: reqData);
    Map<String, dynamic> responseBody = response.data;
    String accessToken = '';

    try {
      accessToken = response.headers['Authorization']![0];  // JWT 토큰 추출
      await secureStorage.write(key: 'accessToken', value: accessToken);  // 토큰 저장
    } catch (e) {
      print("JWT 파싱 오류");
    }

    return (responseBody, accessToken);
  }
}

✅ readUser() → 로그인 요청을 보내고 서버 응답에서 JWT 토큰을 가져와 저장


 4. AuthViewModel (로그인 상태 관리)

이제 ViewModel을 활용하여 로그인 상태를 관리해봅시다.
Riverpod의 Provider를 사용하여 로그인 상태를 쉽게 UI와 연결할 수 있습니다.

 
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../repository/user_repository.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

// Provider 설정
final sessionUserProvider = StateProvider<SessionUser?>((ref) => null);
final authViewModelProvider = Provider((ref) => AuthViewModel(ref));

class AuthViewModel {
  final Ref ref;
  final UserRepository userRepository = UserRepository();
  final FlutterSecureStorage secureStorage = FlutterSecureStorage();

  AuthViewModel(this.ref);

  // 로그인 기능
  Future<void> login(String username, String password) async {
    var result = await userRepository.readUser({"username": username, "password": password});
    ref.read(sessionUserProvider.notifier).state = SessionUser(
      id: result.$1['id'],
      username: result.$1['username'],
      accessToken: result.$2,
      isLogin: true,
    );
    await secureStorage.write(key: 'accessToken', value: result.$2);
  }

  // 로그아웃 기능
  Future<void> logout() async {
    await secureStorage.delete(key: 'accessToken');
    ref.read(sessionUserProvider.notifier).state = null;
  }

  // 자동 로그인
  Future<void> checkLoginStatus() async {
    String? token = await secureStorage.read(key: 'accessToken');
    if (token != null) {
      ref.read(sessionUserProvider.notifier).state = SessionUser(
        id: null,
        username: null,
        accessToken: token,
        isLogin: true,
      );
    }
  }
}

 

  • login() → 로그인 후 사용자 정보를 Provider에 저장.
  • logout() → JWT 토큰 삭제 및 상태 초기화.
  • checkLoginStatus() → 앱 실행 시 자동 로그인 상태 확인.

 


ref의 역할과 활용법 알아보기 🔍

더보기

Flutter에서 ref는 Riverpod의 핵심 객체로, Provider에서 상태를 읽고 변경하는 역할을 합니다.

✅ ref의 역할

  1. Provider 읽기 (ref.read())
    • ref.read(provider)를 사용하면 특정 Provider의 값을 가져올 수 있습니다.
    • 하지만 UI 변경 사항을 반영하지 않으므로 일반적으로 이벤트 핸들러에서 사용됩니다.
  2. Provider 구독 (ref.watch())
    • ref.watch(provider)를 사용하면 Provider의 상태를 UI에서 구독합니다.
    • 즉, Provider 값이 변경되면 UI가 자동으로 업데이트됩니다.
  3. Provider 업데이트 (ref.read(provider.notifier).state = ...)
    • StateProvider와 함께 사용하여 상태를 변경할 수 있습니다.
    • 예를 들어, 로그인 후 사용자 정보를 업데이트할 때 사용됩니다.
final counterProvider = StateProvider<int>((ref) => 0);

class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterProvider);  // 상태 구독

    return Column(
      children: [
        Text('카운트: $counter'),
        ElevatedButton(
          onPressed: () {
            ref.read(counterProvider.notifier).state++;  // 상태 변경
          },
          child: Text("증가"),
        ),
      ],
    );
  }
}

✅ ref.watch(counterProvider) → counter 값이 바뀌면 UI가 자동으로 업데이트
✅ ref.read(counterProvider.notifier).state++ → 카운트 값 증가