SwiftUI 및 GraphQL을 사용한 Instagram 클론 – 로그인

Instagram 클론 앱을 만드는 방법에 대한 이전 게시물에서는 XCode 11에서 SwiftUI를 실행하기 위해 모든 것을 구성하는 방법을 배웠고 GraphQL로 완벽하게 작동하는 Sing Up 보기를 만들었습니다.

오늘은 로그인 뷰를 생성하고 사용자가 로그아웃하도록 하는 방법을 배워보겠습니다.

이전 포스팅의 프로젝트가 필요하므로 그 포스팅을 따라 하지 않으셨다면 꼭 따라 해보시기 바랍니다.

안전벨트를 매고 출발하세요!

더 나은 학습을 위해 소스 코드가 포함된 iOS Instagram 클론 프로젝트를 다운로드하세요.

빠르게 시작하고 싶으신가요?

허브에서 이 앱을 복제하여 번거로움 없이 바로 사용해보세요!

로그인 보기 만들기

로그인 보기는 가입 보기와 매우 유사하며 실제로는 훨씬 더 간단합니다.

사용자 이름과 비밀번호라는 두 가지 매개변수만 있으면 됩니다:

쿼리 및 변이는 선택한 Parse 버전에 따라 달라집니다:

Parse 3.7.2:

mutation logInUser($username: String!, $password: String!){
  users{
    logIn(username: $username, password: $password){
      세션토큰
    }
  }
}

Parse 3.8:

변이 로그인유저($username: 문자열!, $password: 문자열!){
    logIn(username: $username, password: $password){
      sessionToken
    }
}

3.9를 Parse합니다:

변이 로그인유저($username: 문자열!, $password: 문자열!){
    logIn(username: $username, password: $password){
      세션 토큰
    }
}

를 생성했으므로 사용자에게만 요청하면 됩니다.

파일 > 새로 만들기 > 파일로 이동하여 SwiftUI 뷰를 선택하여 새 SwiftUI 뷰를 추가해 보겠습니다.

screen-shot-2019-08-26-at-11-08-58

이 뷰의 이름을 LogInView.swift로 지정하고 프로젝트에 추가해 보겠습니다:

screen-shot-2019-08-28-at-10-54-07

그리고 이미 배운 대로 필요한 컨트롤을 사용하여 VStack을 만듭니다:

  • 사용자 이름을 위한 TextField
  • 비밀번호를 위한 SecureField
  • 작업을 수행하기 위한 버튼

디자인 일관성을 유지하기 위해 사용할 색상을 AppDelegate.swift로 옮겼기 때문에 SwiftUI도 그곳으로 가져와야 했습니다:

SwiftUI 가져오기

let lightGreyColor = Color(빨강: 239.0/255.0, 초록: 243.0/255.0, 파랑: 244.0/255.0, 불투명도: 1.0)
let lightBlueColor = Color(빨강: 36.0/255.0, 녹색: 158.0/255.0, 파랑: 235.0/255.0, 불투명도: 1.0)

SignUpView.swiftLogInView.swift에서 색상 선을 제거하는 것을 잊지 마세요.

또한 컨트롤의 일관성을 유지하기 위해 SignUp 뷰에서 복사하여 붙여넣고 이메일 텍스트 필드를 제거하고 새로운 기능을 반영하도록 텍스트 필드를 변경했습니다. 코드는 다음과 같이 완성되었습니다:

구조체 LogInView: View {
    상태 변수 사용자 이름: 문자열 = ""
    State var password: String = ""
    
    State private var showingAlert = false

    var body: 일부 뷰 {
       VStack{
           Text("로그인")
               .font(.largeTitle)
               .foregroundColor(lightBlueColor)
               .fontWeight(.semibold)
               .padding(.bottom, 20)
           텍스트필드("사용자명", 텍스트: $username)
               .padding()
               .background(lightGreyColor)
               .cornerRadius(5.0)
               .padding(.bottom, 20)
           보안 필드("비밀번호", 텍스트: $password)
               .padding()
               .background(lightGreyColor)
               .cornerRadius(5.0)
               .padding(.bottom, 20)
           Button(action: {
            
           }){
               Text("로그인!")
                .font(.headline)
                .foregroundColor(.white)
                .padding()
                .frame(너비: 220, 높이: 60)
                .background(lightBlueColor)
                .cornerRadius(15.0)
           }
       }.padding()
    }
}

간단합니다. 어떻게 보이는지 보시죠?
대신 해당 뷰를 표시하도록 ContentView.swift를 변경하세요:

구조체 ContentView : View {
    var body: 일부 View {
        LogInView()
    }
}

screen-shot-2019-08-28-at-11-16-13

깔끔해졌네요!

상단에 로고를 추가하여 더 깔끔하게 만들어 봅시다!
로고는 프로젝트에 통합할 이미지로 구성됩니다. 저는 이미지를 사용했습니다.

해당 이미지를 프로젝트의 Assets.xcassets 폴더로 끌어다 놓습니다:

screen-shot-2019-08-28-at-13-25-54

다음과 같이 보일 것입니다:

screen-shot-2019-08-28-at-13-27-30

이제부터는 로고-소셜이라는 이름으로 코드에서 참조할 수 있습니다.

지금 Back4app에 가입하고 Instagram 클론 앱 제작을 시작하세요.

로고

로고는 해당 이미지로 구성되지만 단순히 이미지를 넣는 것은 아마추어처럼 보일 수 있습니다. 원형, 고정된 크기, 테두리에 약간의 획, 그리고 물론 그림자 때문에… 그림자 때문에 빛나게 만들 것입니다.

이 모든 것을 위한 코드는 다음과 같습니다:

이미지("로고-소셜")
    .resizable()
    .aspectRatio(contentMode: .fit)
    .frame(너비: 150, 높이: 150)
    .clipShape(Circle())
    .overlay(Circle().stroke(Color.blue, lineWidth: 2))
    .shadow(radius: 5)
    .padding(.bottom, 75)

그리고 VStack의 상단에 배치합니다:

var body: some View {
       VStack{
           Image("logo-social")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(너비: 150, 높이: 150)
                .clipShape(Circle())
                .overlay(Circle().stroke(Color.blue, lineWidth: 2))
                .shadow(radius: 5)
                .padding(.bottom, 75)
           텍스트("로그인")
               .font(.largeTitle)
               .foregroundColor(lightBlueColor)
               .fontWeight(.semibold)
               .padding(.bottom, 20)
...

screen-shot-2019-08-28-at-13-33-41

자, 멋지지 않나요?

세션토큰

로그인 프로세스는 세션토큰 문자열을 반환합니다. 이 세션토큰은 작업 중에 사용되므로 안전하게 보관해야 합니다. 세션토큰이 유효한 동안에는 애플리케이션에 액세스할 수 있습니다. 세션토큰이 삭제되거나 무효화되면 호출이 거부됩니다.

이는 보안과 직결되는 문제이므로 세션토큰을 안전하게 저장해야 합니다. 이를 위한 올바른 위치는 iOS의 키체인입니다.

키체인의 한 가지 단점은 사용하기 어렵고 지루하다는 것이므로 이 래퍼를 사용하여 관리하기로 결정했습니다. 훨씬 더 쉽게 사용할 수 있고 이미 CocoaPods을 사용하고 있기 때문에 완전히 합리적입니다.

포드파일을 편집하고 이것을 포드에 추가해 보겠습니다:

파드 'SwiftKeychainWrapper'

그런 다음 다음 명령어로 파드를 업데이트해 보겠습니다.

pod install

명령으로 파드를 업데이트하고 마지막으로 xcworkspace 프로젝트를 다시 엽니다.

이제 키체인 엔진을 사용하여 다음을 수행할 준비가 되었습니다.

세션토큰 저장

새 파드의 문서에 따르면, 값을 키체인에 저장하는 방법은 다음과 같습니다.

KeychainWrapper.standard.set("SomeValue", forKey: "SomeKey")

이지만 여기에 몇 가지 로직을 추가해 보겠습니다.

먼저 래퍼를 임포트해 보겠습니다:

SwiftKeychainWrapper 가져오기

가장 먼저 logInUser 변형을 호출하고 응답이 오면 세션 토큰이 있으면 이를 저장해야 합니다. 그렇지 않은 경우 사용자에게 알림을 통해 알려야 합니다.

이전 글에서 이미 구조를 포함하여 코딩된 알림이 있다는 것을 기억하실 것입니다. 모든 뷰가 액세스할 수 있도록 SingUpView.swift에서 아래 코드를 제거하고 AppDelegate.swift 파일에 전달하여 이를 재사용해 보겠습니다:

구조체 Message {
    var alertTitle: String = ""
    var alertText: String = ""
}

var myMessage = Message()

이제 로직으로 돌아가서 가장 먼저 해야 할 일은 사용자가 사용자 이름과 비밀번호 텍스트 상자를 입력했는지 확인하는 것입니다. 그렇지 않은 경우 로그인 프로세스에 대한 정보가 없으므로 사용자에게 이를 알려야 합니다.

로그인 버튼에 대한 액션 코드가 이를 확인해야 합니다. 여기에 해당 텍스트 필드에 연결된 상태 변수의 문자열 크기를 확인하는 코드를 추가해 보겠습니다:

           Button(action: {
            // 비밀번호가 입력되었는지 확인
            if (self.password.count == 0 || self.username.count == 0){
                
                // 그렇지 않다면, 경고를 표시해야 합니다.
                myMessage.alertText = "사용자 아이디와 비밀번호를 입력해야 합니다."
                myMessage.alertTitle = "죄송합니다..."
                self.showingAlert = true
            } else {
                // 그렇다면 계속 진행할 수 있습니다.
                
            }
           }){
               Text("로그인!")
                .font(.headline)
                .foregroundColor(.white)
                .padding()
                .frame(너비: 220, 높이: 60)
                .background(lightBlueColor)
                .cornerRadius(15.0)
           }
           .alert(isPresented: $showingAlert) {
               Alert(title: Text(myMessage.alertTitle), message: Text(myMessage.alertText), dismissButton: .default(Text("OK")))
           }

그리고 테스트해 보세요…

screen-shot-2019-09-02-at-10-22-15

멋지네요!

이제 세션토큰을 검색하고, 세션토큰이 있으면 키체인에 저장하기 위해 GraphQL 변형을 호출해야 합니다.
이미 변형을 호출하는 방법을 배웠으므로 이번에는 세션 토큰을 가져와서 저장해 보겠습니다:

// 방금 텍스트 필드에서 얻은 매개변수를 전달하여 LogInUser 돌연변이를 수행합니다.
apollo.perform(mutation: LogInUserMutation(username: self.username, password: self.password)){ result in
    // 성공적인 결과와 오류를 구분할 수 있도록 결과를 전환해 보겠습니다.
    switch result {
        // 성공의 경우
        case .success(let graphQLResult):
            // 결과를 Parse해봅니다.
            if let sessionToken = graphQLResult.data?.users?.logIn.sessionToken { {
                myMessage.alertTitle = "Yay!"
                myMessage.alertText = "사용자가 로그인했습니다!"
                
                self.showingAlert = true

                인쇄 ("사용자 세션 토큰 " + 세션 토큰)
                

                // 세션토큰을 키체인에 기록합니다.
                let _: Bool = KeychainWrapper.standard.set(sessionToken, forKey: "Back4Gram.sessionToken")
            }
            // 하지만 그래프QL 에러가 발생하면 해당 메시지를 표시합니다.
            else if let errors = graphQLResult.errors {
                // 그래프QL 에러

                myMessage.alertTitle = "죄송합니다!"
                myMessage.alertText = "GraphQL 오류가 발생했습니다: " + errors.description
                self.showingAlert = true

                print(errors)
            }
        // 실패한 경우 해당 메시지를 표시합니다.
        case .failure(let error):
            // 네트워크 또는 응답 형식 오류
            myMessage.alertTitle = "죄송합니다!"
            myMessage.alertText = "오류가 발생했습니다: " + error.localizedDescription
            self.showingAlert = true
            
            print(error)
    }
}

테스트해 봅시다!

screen-shot-2019-09-02-at-13-11-02

멋지네요! 하지만 실제로 작동했는지 어떻게 알 수 있을까요?
앱의 Parse 대시보드로 이동하여 새 세션 객체가 작성되었는지 확인하면 됩니다:

screen-shot-2019-09-02-at-13-11-34

마법처럼!

그리고 여기 왔으니…

로그아웃 버튼을 추가하는 것은 어떨까요? 모든 것이 원활하게 작동하는지 확인하기 위한 테스트용입니다:

Button(action: {
    // 세션 토큰이 있는지 확인 로그인한 경우에만 로그아웃해야 합니다.
    if (KeychainWrapper.standard.string(forKey: "Back4Gram.sessionToken") != nil) {
        print("세션토큰을 찾았습니다! 로그아웃할 수 있습니다.")
        
        // 로그아웃 사용자 변이 수행하기
        apollo.perform(mutation: LogOutUserMutation()){ result in
            // 성공적인 결과와 오류를 구분할 수 있도록 결과를 전환해 보겠습니다.
            switch result {
                // 성공의 경우
                case .success(let graphQLResult):
                    // 결과를 Parse해 봅니다.
                    if let result = graphQLResult.data?.users?.logOut {
                        if (result) {
                            myMessage.alertTitle = "Yay!"
                            myMessage.alertText = "사용자가 로그아웃했습니다!"
                            
                            self.showingAlert = true
                            
                            // 저장된 세션 토큰 지우기
                            let _: Bool = KeychainWrapper.standard.set("", forKey: "Back4Gram.sessionToken")
                        } else {
                            myMessage.alertTitle = "죄송합니다!"
                            myMessage.alertText = "사용자 로그아웃 작업이 False를 반환했습니다."
                            self.showingAlert = true
                        }
                    }
                    // 하지만 GraphQL 오류가 발생하면 해당 메시지를 표시합니다.
                    else if let errors = graphQLResult.errors {
                        // 그래프QL 오류

                        myMessage.alertTitle = "죄송합니다!"
                        myMessage.alertText = "GraphQL 오류가 발생했습니다: " + errors.description
                        self.showingAlert = true

                        print(errors)
                    }
                // 실패한 경우 해당 메시지를 표시합니다.
                case .failure(let error):
                    // 네트워크 또는 응답 형식 오류
                    myMessage.alertTitle = "죄송합니다!"
                    myMessage.alertText = "오류가 발생했습니다: " + error.localizedDescription
                    self.showingAlert = true
                    
                    print(error)
            }
        }
    } else {
        // 네트워크 또는 응답 형식 오류
        myMessage.alertTitle = "죄송합니다!"
        myMessage.alertText = "사용자가 로그인하지 않은 것 같습니다."
        self.showingAlert = true
    }
}){
    텍스트("로그아웃")
        .font(.headline)
        .foregroundColor(.white)
        .padding()
        .frame(너비: 220, 높이: 60)
        .background(lightBlueColor)
        .cornerRadius(15.0)
}
.alert(isPresented: $showingAlert) {
    Alert(title: Text(myMessage.alertTitle), message: Text(myMessage.alertText), dismissButton: .default(Text("OK")))
}

다시 한 번 테스트해 봅시다!

screen-shot-2019-09-02-at-13-51-50

멋지네요!

나중에 로그아웃 버튼을 다른 곳으로 옮길 예정이지만, 지금은 흐름이 작동하고 있음을 보여주기 위해 작동합니다.

이제 로그아웃한 세션 객체는 어떻게 될까요?

screen-shot-2019-09-02-at-14-04-31

예상대로 자동으로 사라졌습니다!

결론

축하합니다! 이제 로그인 및 로그아웃 기능을 구현했습니다! 뿐만 아니라 다양한 변형을 호출하고, 결과를 검증하고, 키체인에 값을 저장하는 방법도 배웠습니다! 얼마나 멋진가요!

다음 장에서는 다중 뷰로 작업을 시작하고 메인 뷰를 구축할 것입니다!

기대해주세요!

참고자료

지금 Back4app에 가입 하고 Instagram 클론 앱 제작을 시작하세요.

SwiftUI란 무엇인가요?

SwiftUI는 Apple 플랫폼에서 애플리케이션의 사용자 인터페이스를 만드는 새로운 방법입니다. 개발자는 Swift 코드를 사용하여 UI를 결정할 수 있습니다.

세션토큰이란 무엇인가요?

개발 중인 로그인 프로세스는 세션 토큰(sessionToken) 문자열을 반환합니다. 이 문자열은 보안이 필요합니다. 세션 토큰을 유효하게 유지하면 애플리케이션에 접근할 수 있고, 그렇지 않으면 애플리케이션에 접근할 수 없게 됩니다. 이는 보안과 관련이 있습니다.

키체인이란?

세션 토큰은 앱 보안과 관련이 있습니다. 따라서 안전한 곳에 보관해야 합니다. 이 안전한 곳을 키체인이라고 합니다. 사용하기 조금 까다로울 수도 있고, 지루하게 느껴질 수도 있습니다.


Leave a reply

Your email address will not be published.