본문 바로가기

IOS(Swift)

[IOS/Swift] 콘텐츠 검색 앱 구현 - 4. 로그인 UI 구현

반응형

Swift UI - 4. 로그인 UI 화면 구현

이전에 구현한 회원가입으로 회원이 등록되었다(DRF로 회원가입 API를 구현) 이제 등록된 회원으로 로그인을 진행하고 키체인을 이용해서 Access Token 및 Refresh Token을 저장하여 앱 사용 중 로그인 상태를 유지할 수 있도록 해보자.

//
//  LoginView.swift
//  mbtiPlayground
//
//  Created by 박종후 on 2022/03/16.
//

import SwiftUI

struct LoginView: View {
    
    @State private var email: String = ""
    @State private var password: String = ""
    @State private var loginStatus: Bool = false // TODO env 변수로 선언 후 토큰 계속 확인
    @State private var userAccessToken: String = ""
    
    
    var body: some View {
        
        NavigationView{
            if self.loginStatus != false {
                ProfileDetail(userEmail: self.email, userAccessToken: self.userAccessToken)
            } else {
                VStack{
                    Text("Sign In")
                        .font(.title)
                        .fontWeight(.medium)
                        .padding()
                        .foregroundColor(.black)
                    HStack{
                        Image(systemName: "person.fill")
                            .resizable()
                            .scaledToFit()
                            .frame(width: 30, height: 30)
                            .padding(.bottom)
                        
                        TextField("닉네임을 입력하세요.", text: $email)
                            .frame(width: 300, height: 10)
                            .padding()
                            .background(Color(.systemGray6))
                            .cornerRadius(5.0)
                            .padding(.bottom, 20)
                            
                    }
                    HStack{
                        Image(systemName: "lock")
                            .resizable()
                            .scaledToFit()
                            .frame(width: 30, height: 30)
                            .padding(.bottom)
                            
                        SecureField("비밀번호를 입력하세요", text: $password)
                            .frame(width: 300, height: 10)
                            .padding()
                            .background(Color(.systemGray6))
                            .cornerRadius(5.0)
                            .padding(.bottom, 20)
                    }
                    
                    HStack{
                        Button(action: {
                            print(self.email + self.password)
                            
                            let rft = readItemKeyChain(userId: self.email)
                            if rft != nil {
                                UserDefaults.standard.set(rft, forKey: self.email)
                            }else{
                                sendPostRequest("<http://localhost:8000/auth/login>", parameters: ["username": self.email, "password": self.password]){
                                    responseObject, error in guard let _ = responseObject, error == nil else {
                                        print(error ?? "Unknown error")
                                        return
                                    }
                                    self.loginStatus = true
                                    
                                    if let rftToken = responseObject{
                                        let rft = rftToken["refresh"] as? String
                                        self.userAccessToken = rftToken["access"] as? String ?? ""
                                        setItemKeyChain(userId: self.email, rft: rft!)
                                        UserDefaults.standard.set(rft, forKey: self.email)
                                    }
                                }
                            }
                        }){
                            Text("로그인")
                                .frame(width: 80, height: 10)
                                .font(.headline)
                                .foregroundColor(.white)
                                .padding()
                                .background(Color(.systemBlue))
                                .cornerRadius(10)
                                
                        }
                        .padding()
                        
                        NavigationLink(destination: SignUpView()){
                            Text("회원가입")
                                .frame(width: 80, height: 10)
                                .font(.headline)
                                .foregroundColor(.white)
                                .padding()
                                .background(Color(.systemBlue))
                                .cornerRadius(10)
                        }
                        
                    }
                    
                }
                .padding(.all, 30)
                .navigationBarTitle("", displayMode: .inline)
                .navigationBarHidden(true)
            }
        }
    }
}

struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginView()
    }
}

우선 21번 째 줄에서 유저의 로그인 상태에 따라 프로필 상태(로그인 후 유저의 정보를 보여줄 페이지)를 보여줄지 아니면 로그인 화면을 보여줄지에 대해 조건문을 넣었다.

UI에서는 닉네임과 비밀번호를 입력하여 로그인하는 화면을 구현하였다. (로그인 시 이메일 또는 닉네임을 사용하도록 구현할 수 있는데, 현재 DRF에서는 닉네임만 사용하여 로그인하도록 설정한 상태)

여기서 로그인 성공 후 토큰을 발급받는 과정에서 리프레시 토큰을 키체인에 저장하고 나중에 유저가 다시 접속했을 때에도 다시 로그인 하지 않도록 구현하였다.

setItemKeyChain(userId: self.email, rft: rft!)
UserDefaults.standard.set(rft, forKey: self.email)

setItemKeyChain 함수는 로그인한 유저의 리프레시 토큰 정보를 저장하기 위해 작성하였다.

func setItemKeyChain(userId: String, rft: String){
    let previousQuery: [CFString: Any] = [kSecClass: kSecClassIdentity, kSecAttrAccount: userId]
    
    let updateQuery: [CFString: Any] = [kSecValueData: rft]
    let status = SecItemUpdate(previousQuery as CFDictionary, updateQuery as CFDictionary)
    
    if status == errSecSuccess {
        print("Update!")
    } else {
        print("Update fail")
    }
}

추가적으로 유저의 데이터를 저장하기 위해 UserDefaults 도 사용하였다. UserDefaults 는 Redis 사용과 유사하다

위 두 코드를 통해 사용자가 로그인 후 발급받은 액세스 토큰 및 리프레시 토큰을 사용자의 기기에 저장하여 로그아웃 없이 계속 사용할 수 있다.

반응형