이번 글에서는 Flutter 블로그 프로젝트에서 Dio를 활용한 서버 통신 구현을 다룹니다.
서버와 데이터를 주고받기 위한 Dio 설정, UserRepository 설계, 테스트 코드 작성까지 모든 과정을 설명과 함께 정리합니다.
1. 사전 지식: JSON과 Flutter의 데이터 처리
서버와 통신할 때 주로 사용하는 데이터 형식인 JSON을 Flutter에서 처리하는 방법을 먼저 이해해 봅시다.
JSON 데이터의 두 가지 타입
JSON Object
{
"status": "success",
"users": [
{ "username": "ssar", "password": 1234 },
{ "username": "cos", "password": 5678 }
]
}
- JSON Object는 Map 구조와 유사하며, Flutter에서 Map<String, dynamic> 타입으로 처리됩니다.
JSON Array
[
{
"username": "ssar",
"password": 1234
},
{
"username": "cos",
"password": 5678
}
]
- JSON Array는 리스트 구조와 유사하며, Flutter에서 List<Map<String, dynamic>> 타입으로 처리됩니다.
Flutter에서 JSON 데이터 처리 방법
- JSON Object → Map<String, dynamic>로 변환하여 사용.
- JSON Array → List<Map<String, dynamic>>로 변환하여 사용.
- DTO(Data Transfer Object) 또는 모델 클래스를 활용하여 구조화된 데이터로 처리할 수 있습니다.
2. Dio 클래스 선언
Dio는 HTTP 요청/응답을 처리하기 위한 강력한 Flutter 패키지입니다.
전역적으로 사용할 Dio 객체를 선언하여 프로젝트 전반에서 쉽게 HTTP 요청을 보낼 수 있습니다.
📌 Dio 객체 설정
// API 서버의 기본 URL 설정
// 전역 변수 사용
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final baseUrl = 'http://192.168.0.48:8080';
// 전역 변수 dio <--
final dio = Dio(
BaseOptions(
baseUrl: baseUrl,
contentType: 'application/json;charset=utf-8',
// 주의 ! 200 번 이외에는 모두 에러로 간주 하게 기본값 설정되어 있음
// 다른 상태코드 다 허용
validateStatus: (status) => true,
),
);
// 중요 데이터 보관소(금고 생성)
// 로컬에 민감한 데이터를 보관하는 안전한 금고 역할을 한다.
const secureStorage = FlutterSecureStorage();
📌 설명
- baseUrl: 서버의 기본 URL.
- contentType: 요청/응답을 JSON 형식으로 처리.
- validateStatus: 모든 상태 코드를 허용(기본값은 200만 허용).
- secureStorage: 민감한 데이터를 안전하게 저장할 수 있는 Flutter Secure Storage.
3. HTTP 요청 메시지 구조 이해
HTTP 요청 메시지는 다음과 같은 구조로 이루어져 있습니다.
- 시작 줄: 요청 메서드, URL, HTTP 버전을 포함.
- 예: POST /login HTTP/1.1
- 헤더: 요청 메타데이터를 포함.
- 예: Content-Type: application/json
- 바디: 요청 데이터를 포함.
- 예: { "username": "user", "password": "1234" }
4. UserRepository 구현
UserRepository는 외부 API와 데이터를 주고받는 역할을 담당하는 클래스입니다.
Dio 객체를 활용해 사용자 등록, 로그인 요청 등을 처리합니다.
- 사용자 등록 메서드
서버에 새 사용자를 등록하는 메서드입니다.
📌 UserRepository 구현: save 메서드
import 'package:dio/dio.dart';
class UserRepository {
const UserRepository();
// 사용자 등록 요청
Future<Map<String, dynamic>> save(Map<String, dynamic> reqData) async {
// HTTP POST 요청을 /join 경로로 보냅니다.
Response response = await dio.post('/join', data: reqData);
// 응답 바디 추출
Map<String, dynamic> responseBody =
response.data; // header, body 중 body만 추출
logger.i(responseBody); // 결과를 로깅
return responseBody;
}
}
- 로그인 요청 메서드
서버에 로그인 요청을 보내고 JWT 토큰과 사용자 정보를 반환합니다.
📌 UserRepository 구현: findByUsernameAndPassword 메서드
// 로그인 요청 (레코드 문법)
Future<(Map<String, dynamic>, String)> findByUsernameAndPassword(
Map<String, dynamic> reqData) async {
// HTTP 응답 메시지 전송
Response response = await dio.post('/login', data: reqData);
// JWT 토큰 추출 (Authorization 헤더에서 가져옴)
String accessToken = response.headers['Authorization']?[0] ?? '';
logger.i(accessToken); // 토큰 로깅
// 응답 바디 추출
Map<String, dynamic> responseBody = response.data; // 응답 메시지 바디
return (responseBody, accessToken); // 레코드 문법을 활용해 반환
}
5. 테스트 코드 작성
UserRepository의 메서드가 올바르게 동작하는지 확인하기 위한 테스트 코드를 작성합니다.
📌 테스트 코드
import 'package:flutter/material.dart';
import 'package:class_f_story/data/repository/user_repository.dart';
import '../utils/logger.dart';
void main() async {
UserRepository userRepository = UserRepository();
// 회원 등록 테스트
Map<String, dynamic> resBody = await userRepository.save({
"username": "hello",
"password": "1234",
"email": "hello@nate.com",
"imgBase64":
"data:image/jpg;base64,yrxsJ0Eg29JPHQXDFSZhEhYuhofZaFOzLh8wTob3JVMuFC4kX5CqWYBDubwLt23VjmQ4ZLXMMcKNly2Db2w2EHwDgeR844F1b91tUBeuALOzb2yxz0MSBMOlEhBtLGzVW0UbIicvHgOrTFGupuXW8OOQI2rzyIFQ2IwnBcXWQo72d7k9Ai88QZa6qYsd23YAn0KyvoTOKOXN6EWY1ZIVh73TbkQV4sx64WEFCLHkKqKWm16JhEhUJNzwaQQWiXFBl2JAyZ5CpBanN2UxRZcPntH2Nk9lcllPkwltiNcNwxJqcBplUMhTOS4hdfYpkJxL1it1CpsCBlFYdeMFHJn3XI8qQDgccUB0gar5F9pyyTaAE9J9ka4PRTBv5xgiPRB2Tpse69fzqNFpLJWuuy0HhKrLZ7vJCsy8Ykf226BiiA42v0WLvWtPchfLJJnuA2Y8XBICyiHFO8cBULOIqD0H0f4xn92k8bjdtJ1gw0nRiy3HevT632cGBOtvXMY4KJ9S9LFBsUkGeYjdHcTvVR4ZSvto6elYwMFHiPlcUGrI6g5y1AAYNbf2PHSXkJ0KGsqeOaVbYGP7WViL7au9GOHT9lptz49u1td7RRjqU5MKDIYoYDCpuxWoSJEHNVegho8nrmRtzLMBgm9RonQNa4rzDtdVdppE20KOIm8Hrfr7H5JPHwVjSzZsabktSbLWnVEaoALQkbgWhg2FcbLCmhg7lYDOAzYWd3rUIFNRjA4m1BQ6zVGozEIXL1FgptL5u0TRmA1ISAHUBtU2FpmQ6Vc0UQRfutLM6qNLJlKSHCQ7QPJSoAAibFLKKv2JxyKWjpVfRKKcBRS5jVzAby1y1hr3XmU22zPCPvyDT7ZqJMxHfZovUawHYQ96I3wQGO9vFXAvreDn62kOFp5A8CXHUaMBoPzugoXl8ywejlzEyz2TIR1ObRjZsHoeeDbOcMJO3SNVFs059G4ed2rdvX3ptYR00fsgZlDtwfVxVH4paCbH"
});
logger.e(resBody);
// (Map<String, dynamic>, String) userInfoTest = await userRepository
// .findByUsernameAndPassword({"username": "ssar", "password": "1234"});
// logger.e(userInfoTest);
// // 레코드 문법 사용해보기
// logger.d(userInfoTest.$1);
// logger.d(userInfoTest.$1['response']['username']);
// logger.d(userInfoTest.$1['response']['id']);
// logger.d(userInfoTest.$1['response']['password']);
// logger.d(userInfoTest.$2);
}
6. 마무리 및 정리
이번 글에서는 Dio와 UserRepository 구현을 통해 서버 통신을 처리하는 방법을 다뤘습니다.
✅ 정리
- Dio 클래스 선언:
- 기본 URL, Content-Type, validateStatus 설정.
- UserRepository 구현:
- 사용자 등록 요청(save), 로그인 요청(findByUsernameAndPassword) 메서드 구현.
- HTTP 메시지 구조 이해:
- 요청의 시작 줄, 헤더, 바디의 역할과 구성.
다음 글에서는 로그인 기능 구현의 상세 내용을 다룹니다.
특히, **상태 관리(Riverpod)**와 예외 처리를 활용해 실질적인 로그인 기능을 구현할 예정입니다. 🚀
로그인 UI 구성하는 방법이 궁금하다면??
[Flutter] 블로그 만들기 (로그인 UI 구성 하기)
이번 글에서는 로그인 UI를 구성하고, 이를 위한 위젯들을 정리해보겠습니다.디자인 시안을 확인한 후, 필요한 위젯을 만들고 LoginPage와 LoginBody를 구성하겠습니다.UI에 사용될 파일 디자인 시
seohong.tistory.com
'Flutter > App' 카테고리의 다른 글
[Flutter] 블로그 만들기 - 회원가입 기능 구현 및 UI (1) | 2025.02.09 |
---|---|
[Flutter] 블로그 만들기 - 로그인 기능 구현 (상태관리 및 예외처리) (0) | 2025.02.09 |
[Flutter] 블로그 만들기 - 로그인 UI 구성 (0) | 2025.02.08 |
[Flutter] 블로그 만들기 - 기본 프로젝트 설정 (1) | 2025.02.08 |
[Flutter] 상태 관리 앱 만들기 (라이브러리 사용) (0) | 2025.02.05 |