앱 로그인 구현하기

김인범's avatar
Jan 18, 2025
앱 로그인 구현하기

로그인 진행

  1. 페이지에서 ViewModel로 로그인 진행을 위임한다.
  1. VM에서는 유저가 입력한 값을 넘겨 받아서 Map으로 파싱한다.
  1. 파싱한 데이터를 서버에 통신으로 보내는데, 이것을 Repository에 위임한다.
  1. Repo에서는 서버에 통신을 할 때 넘겨받은 데이터를 Dio를 사용해서 통신한다.
  1. 통신이 끝나면 응답받은 데이터(Response 데이터)를 가지고, VM에서 비즈니스 로직을 진행한다.
  1. VM에서는 상태를 갱신하거나, 오류가 있는 응답의 경우 처리를 한다.
 

login_page ⇒ login_body

login_body를 CostomerWidget으로 변경한다.
build 에 WidgetRef 를 추가해 준다.
로그인은 SessionGvm gvm = ref.read(sessionProvieder.Notifire) >> write를 해준다.
 
필드에 사용자가 username과 password를 입력하면,
그 값을 gvm.login(_username.text.trim(), _password.text.trim()); 로 넘긴다.
 

session_gvm (글로벌 뷰 모델)

login() 이 실행된다. 이때 통신을 하기 때문에 async를 걸어준다.
_username과 _password로 받은 String들을 Map으로 파싱 해 준다.
 
Repository에다가 파싱한 데이터를 넘겨준다.
 

repository (통신)

서버에 요청을 보낸다. (Map으로 파싱된 데이터 넘겨줌)
Response response = await dio.post("/login", data: data); Map<String, dynamic> body = response.data; // 응답 데이터
응답이 오게 되는데, 이때 정상적으로 로그인이 된다면 ResponseHeader에 토큰이 포함되어 온다.
이 토큰을 꺼내야한다.
String accessToken = ""; try { accessToken = response.headers["Authorization"]![0] ?? ""; // 토큰 꺼냄 } catch (e) {}
해당 코드로 response.headers에서 [”Authorization]의 첫번째 배열에 들어가
있는 값을 꺼내면 그것이 토큰이다.
 
플러터에서는 두 개의 데이터를 한번에 return 할 수 있다.
그래서 Map<String, dynamic> 과 String 타입 두 개를 return 할 수 있다.

통신 전체 코드

Future<(Map<String, dynamic>, String)> findByUsernameAndPassword( Map<String, String> data) async { Response response = await dio.post("/login", data: data); Map<String, dynamic> body = response.data; //Logger().d(body); // test 코드 작성 - 직접해보기 // 토큰꺼내기 String accessToken = ""; try { accessToken = response.headers["Authorization"]![0] ?? ""; //Logger().d(accessToken); } catch (e) {} return (body, accessToken); }

session_gvm (글로벌 뷰 모델) 로 돌아와서

final (responseBody, accessToken) = await userRepo.findByUsernameAndPassword(body);
통신이 끝나면 두개의 데이터가 return으로 받을 때는
final (responseBody, accessToken) << 구조 분해 할당으로 받아낸다.
이후 비즈니스를 진행한다.

실패의 경우

로그인이 정상적으로 처리되지 않았을 때 >> id또는 pw 잘못 입력의 경우
if (!responseBody["success"]) { ScaffoldMessenger.of(mContext!).showSnackBar( SnackBar(content: Text("로그인 실패 : ${responseBody["errorMessage"]}")), ); return; }
responseBody의 “success”가 아닐 경우 == 로그인 실패 처리
화면에 스넥바를 띄우고 해당 로그인 페이지에 머물게 처리를 한다.

성공의 경우

  1. 서버에서 받은 토큰을 Storage에 저장한다. << I/O에 저장
  1. SessionUser 갱신해준다.
  1. Dio에 토큰을 세팅해준다. << 메모리 저장
  1. 메인 화면으로 가게 해준다.
 
토큰을 I/O에 저장 할 때는 secureStorage를 사용해서 저장해 줄 수 있다.
await secureStorage.write( key: "accessToken", value: accessToken); // I/O << 비동기가 디폴트라서 동기로 묶어줘야 한다.
I/O 까지 도달하는 것은 시간이 걸리는 일이기 때문에 await를 걸어주어 동기화 시킨다.
 
로그인 한 유저의 정보를 SessionUser에 갱신해주어야 한다.
ResponseBody(응답데이터)의 body에 있는 유저 정보를 갱신하여 상태 update
Map<String, dynamic> data = responseBody["response"]; state = SessionUser( id: data["id"], username: data["username"], accessToken: accessToken, isLogin: true);
 
메모리에도 토큰을 세팅 해 준다. >> Dio 토큰 세팅
dio.options.headers = {"Authorization": accessToken};
 
이후 페이지 전환 >> Navigator.popAndPushNamed(mContext, "/post/list");
 

해당 메인 페이지에서 유저 정보를 보여주고 싶을 경우

CostomerWidget으로 변환
ref.watch() 또는 ref.read() 를 사용해서 Session에서 유저정보 꺼내서 사용
 

gvm 전체코드

Future<void> login(String username, String password) async { // 파싱 final body = { "username": username, "password": password, }; // 유효성 검사 // 통신 final (responseBody, accessToken) = await userRepo.findByUsernameAndPassword(body); if (!responseBody["success"]) { ScaffoldMessenger.of(mContext!).showSnackBar( SnackBar(content: Text("로그인 실패 : ${responseBody["errorMessage"]}")), ); return; } // 1. 토큰을 Storage 저장 await secureStorage.write( key: "accessToken", value: accessToken); // I/O << 비동기가 디폴트라서 동기로 묶어줘야 한다. // 2. SessionUser 갱신 Map<String, dynamic> data = responseBody["response"]; state = SessionUser( id: data["id"], username: data["username"], accessToken: accessToken, isLogin: true); // 3. Dio 토큰 세팅 , Dio << 메모리에 저장 dio.options.headers = {"Authorization": accessToken}; //Logger().d(dio.options.headers); Navigator.popAndPushNamed(mContext, "/post/list"); }
Share article

taker