안녕하세요 :) 오늘은 서버와 클라이언트 통신하기 라는 주제를 가지고 글을 써볼까 합니다.
처음에 이해하기가 버거웠던 마음을 가득 담아 최대한 쉽게 써보려고 합니다.
이 글은 초보 개발자 과정이의 이해를 담은 글로
오류가 있을 수 있음을 미리 알려드립니다. 🙂
이 글에 나오는 코드는 로그인하기 를 구현한 것 입니다.
Alamofire가 설치되어 있어야 합니다 :)
서버와 HTTP 통신을 하기 위해서 엑스코드 내에서 설정을 해주어야 하는 부분이 있습니다.
Info.plist 파일 -> App Transport Security Settings 추가
-> Allow Arbitary Loads 추가 -> Value ' YES ' 로 변경
우선 큰 흐름이 어떻게 되는지 알아보겠습니다.
우선 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라는 구조체도 만들어 주었습니다.
내가 정해둔 모델 내 데이터의 변수 명과 서버에서 보내는 이름이
일치할 수도 있지만 아닐 때도 있습니다. 이 이름이 다르면 통신이 제대로 되지 않는데
이럴경우를 방지해서 A라는 값이 들어오면 B라고 인식해줘 ! 라고 이야기하는 부분입니다.
제가 올린 케이스는 두가지가 같기 때문에 크게 문제가 없지만 서버에서 보내는 데이터 명과
프로젝트에서 정해둔 데이터 명이 일치하지 않을 때 사용할 수 있습니다.
// 서버 통신에 대한 결과(성공, 요청에러, 경로에러, 서버내부에러, 네트워크 연결 실패)
enum NetworkResult<T> {
case success(T)
case requestErr(T)
case pathErr
case serverErr
case networkFail
}
서버 통신의 결과를 확인 할 때 사용할 NetworkResult enum도 생성해 주었습니다.
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")
}
}
}
주석으로 설명을 달아두었으나, 간략하게 다시 적어보겠습니다.
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값을 선언해두면 어떤 문제때문에 통신이 되지 않고 있는지 확인 할 수 있습니다.