[iOS] 서버 통신 시도 (POST)

ohtt-iOS·2020년 11월 29일
1

iOS

목록 보기
2/24
post-thumbnail
post-custom-banner

안녕하세요 :) 오늘은 서버와 클라이언트 통신하기 라는 주제를 가지고 글을 써볼까 합니다.
처음에 이해하기가 버거웠던 마음을 가득 담아 최대한 쉽게 써보려고 합니다.

이 글은 초보 개발자 과정이의 이해를 담은 글로
오류가 있을 수 있음을 미리 알려드립니다. 🙂

📌

이 글에 나오는 코드는 로그인하기 를 구현한 것 입니다.
Alamofire가 설치되어 있어야 합니다 :)

🔨프로젝트 설정하기

서버와 HTTP 통신을 하기 위해서 엑스코드 내에서 설정을 해주어야 하는 부분이 있습니다.

Info.plist 파일 -> App Transport Security Settings 추가
-> Allow Arbitary Loads 추가 -> Value ' YES ' 로 변경



👀 어떻게 흘러가는지 알아보자 !

우선 큰 흐름이 어떻게 되는지 알아보겠습니다.

  1. VC에서 필요할 때 서버통신 메소드를 실행합니다.
  2. 서버통신 코드에서 서버와 통신을 합니다.
  3. 디코딩이 필요한 경우 미리 세팅해둔 Model에 맞게 디코딩을 해줍니다.
  4. 응답 받은 정보를 활용하여 VC에서 경우에 따라 나눠 화면에 띄울 정보를 조정해줍니다.


우선 Model을 구성하기 위해서 어떤식의 응답이 돌아오는지 확인을 해 보았습니다.
저희 동아리에서는 postman을 서버와 클라이언트 협업툴로 이용하고 있습니다

아래 응답 코드를 quicktype 에서
쉽게 swift model 형식으로 변환해줄 수 있습니다.




struct SignInData: Codable {
    var email, password, userName: String
}

우선 저는 이렇게 SignInData 라는 형식의 model 을 만들어 주었습니다.



struct GenericResponse<T: Codable>: Codable {
    var status: Int
    var success: Bool
    var message: String
    var data: T?
    enum CodingKeys: String, CodingKey {
        case status = "status"
        case success = "success"
        case message = "message"
        case data = "data"
    }
    
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        status = (try? values.decode(Int.self, forKey: .status)) ?? -1
        success = (try? values.decode(Bool.self, forKey: .success)) ?? false
        message = (try? values.decode(String.self, forKey: .message)) ?? ""
        data = (try? values.decode(T.self, forKey: .data)) ?? nil
    }
}

GenericResponse라는 구조체도 만들어 주었습니다.

여기서 CodingKey란?

내가 정해둔 모델 내 데이터의 변수 명과 서버에서 보내는 이름이
일치할 수도 있지만 아닐 때도 있습니다. 이 이름이 다르면 통신이 제대로 되지 않는데
이럴경우를 방지해서 A라는 값이 들어오면 B라고 인식해줘 ! 라고 이야기하는 부분입니다.
제가 올린 케이스는 두가지가 같기 때문에 크게 문제가 없지만 서버에서 보내는 데이터 명과
프로젝트에서 정해둔 데이터 명이 일치하지 않을 때 사용할 수 있습니다.



// 서버 통신에 대한 결과(성공, 요청에러, 경로에러, 서버내부에러, 네트워크 연결 실패)
enum NetworkResult<T> {
    case success(T)
    case requestErr(T)
    case pathErr
    case serverErr
    case networkFail
}

서버 통신의 결과를 확인 할 때 사용할 NetworkResult enum도 생성해 주었습니다.



VC부터 천천히 봐볼까요?

import UIKit
import Alamofire

class LoginVC: UIViewController {
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    // 알림 창 띄우는 함수
    func simpleAlert(title: String, message: String) {
        let alert = UIAlertController(title: title, message: message,
                                      preferredStyle: .alert)
        let okAction = UIAlertAction(title: "확인"
                                     
                                     ,style: .default)
        
        alert.addAction(okAction)
        present(alert, animated: true)
    }
    
    
    // 로그인 버튼 눌렀을 때
    @IBAction func loginClicked(_ sender: Any) {
        
        // 각각의 텍스트 필드에 담겨 있던 값을 저장
        guard let emailText = emailTextField.text,
              let passwordText = passwordTextField.text else {
            return
        }
        
        // 서버 통신 함수 호출 -> 결과는 아까 만들어 준 networkResult로 반환됨
        AuthService.shared.signIn(email: emailText,
                                  password:passwordText) { (networkResult) -> (Void) in
            // 결과에 따라 알림창에 뜨는 메세지를 다르게 설정
            switch networkResult {
            case .success(let data):
                if let signInData = data as? SignInData {
                    self.simpleAlert(title: "로그인 성공", message:
                                        
                                        "\(signInData.userName)님 로그인 성공!")
                                        
                    // 성공 시 userName이 UserDefaults에 저장된다.
                    UserDefaults.standard.set(signInData.userName, forKey: "userName")
                }
            case .requestErr(let msg):
                if let message = msg as? String {
                    self.simpleAlert(title: "로그인 실패", message: message)
                }
            case .pathErr:
                print("pathErr")
            case .serverErr:
                print("serverErr")
            case .networkFail:
                print("networkFail")
            }
            
        }
    }
    

주석으로 설명을 달아두었으나, 간략하게 다시 적어보겠습니다.

  1. 로그인 버튼을 누르면 버튼을 눌렀을 때 텍스트 필드에 담겨 있던 값을 저장해줍니다.
  2. 서버 통신 함수를 호출해줍니다. ( 이 함수에 대한 설명은 아래에 있음)
  3. 함수가 실행되고 나면 networkResult 형태의 값이 반환됩니다.
  4. 결과에 따라 화면에 표시하고 싶은 것들을 구성해줍니다.


위에서 호출된 AuthService

import Foundation
import Alamofire

struct AuthService {
    static let shared = AuthService()
    // 로그인 통신에 대한 함수 정의
    func signIn(email: String,
                password: String,
                completion: @escaping (NetworkResult<Any>) -> (Void)) {
        
        // 현재 APIConstants라는 구조체 내의 usersSingInURL에 값이 있는 상태임.
        let url = APIConstants.usersSignInURL
        let header: HTTPHeaders = [
            "Content-Type":"application/json"
        ]
        let body: Parameters = [
            "email": email,
            "password":password
        ]
        
        // 요청하기
        let dataRequest = AF.request(url,
                                     method: .post,
                                     parameters: body,
                                     encoding: JSONEncoding.default,
                                     headers: header)
        
        dataRequest.responseData {(response) in
            switch response.result {
            case .success:
                guard let statusCode = response.response?.statusCode else {
                    return
                }
                guard let data = response.value else {
                    return
                }
                // 응답 상태와 정보를 입력으로 하는 judgeSingInData 함수 실행
                completion(judgeSignInData(status: statusCode, data: data))
                
            case .failure(let err):
                print(err)
                completion(.networkFail)
            }
            
        }
    }
    
    
    // 상태에 따라 어떤 것을 출력해줄지 결정
    // 이 곳에 print문을 삽입해두면 어떤 문제 때문에 서버와 연결이 잘 안되고 있는지 알 수 있다.
    private func judgeSignInData(status: Int, data: Data) -> NetworkResult<Any> {
    
    	// 우리가 원하는 형태로 디코딩 해준다.
        let decoder = JSONDecoder()
        guard let decodedData = try? decoder.decode(GenericResponse<SignInData>.self, from: data) else {
            return .pathErr
        }
        switch status {
        case 200:
            return .success(decodedData.data)
        case 400..<500:
            return .requestErr(decodedData.message)
        case 500:
            return .serverErr
        default:
            return .networkFail
        }
    }
    
}

위에서 호출된 AuthService 함수 입니다.
이 곳에서 case문 내에 print값을 선언해두면 어떤 문제때문에 통신이 되지 않고 있는지 확인 할 수 있습니다.



📱 작동 화면

profile
오뜨 삽질 🔨 블로그
post-custom-banner

0개의 댓글