-
struct란?
- 여러 변수를 담을 수 있는 컨테이너
- 데이터를 용도에 맞게 묶어 표현하고자 할 때 용이
- 프로퍼티와 메서드를 사용하여 구조화된 데이터와 기능을 가짐
- 하나의 새로운 사용자정의 데이터 타입을 만들어주는 것
- 초기화를 정의하여 초기 상태를 설정할 수 있음
- 확장 가능
- 프로토콜 사용이 가능
- subscript 사용이 가능
-
struct 와 class의 차이?
- 구조체는 값(value) 타입, 클래스는 참조 타입
- 참조타입 : 값 타입과 달리 값이 복사되는 것이 아닌 메모리를 참조
- 구조체는 상속할 수 없다.
- 타입캐스팅은 클래스의 인스턴스에만 허용됨 (상위 또는 하위 클래스 타입으로 형변환 가능)
- 디이녈라이저는 클래스의 인스턴스에만 활용가능
- 참조 횟수 계산은 클랫의 인스턴스에만 적용
- 구조체는 값(value) 타입, 클래스는 참조 타입
-
- APIContants.swift
struct APIConstants { static let baseURL = "http://13.209.144.115:3333" static let signinURL = APIConstants.baseURL + "/user/signin" static let signupURL = APIConstants.baseURL + "/user/signup" }
// 변하지 않는 API의 값을 모아둔 구조체
먼저 baseURL이 되는 http://13.209.144.115:3333을 baseURL이라는 상수에 저장해준다.
그리고 로그인을 할 때 접속되는 URL인 signinURL은 기존 베이스 URL인 " http://13.209.144.115:3333" (APIConstants.baseURL)에 "/user/signin"을 더한 값이다.
회원가입을 할 때 접속되는 URL인 signupURL도 마찬가지의 방법.
-
// 서버에서 JSON형식으로 넘어온 로그인 데이터를 swift형식으로 바꿔주는 decoding 단계.
👉🏻 Object는 키 값이 존재하거나 존재하지 않을 수 있기 때문에 특정 키가 없이 데이터가 내려올 수 있다.
그렇다면 이전에 잘 내려오던 데이터의 특정 키가 갑자기 안내려온다면 Codable을 썼을 때 무슨 문제가 생길까?
⭐️ KeyNotFound에러가 발생한다.⭐️
struct SigninData: Codable { var status: Int var success: Bool var message: String var data: TokenData? 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(TokenData.self, forKey: .data)) ?? nil } } struct TokenData: Codable { var jwt:String }
(JSONData가 바뀔 경우를 대비) 이런 문제가 발생하는 것을 방지하고자 JSON을 직접 decoding해서 문제를 해결한다.
1️⃣ JSON을 decoding할 때 init(from decoder:Decoder)을 호출하여 해당 부분에 직접 코드를 작성해준 다. 👉🏻 KeyNotFound에러는 init함수에 기본 값을 넣어주는 방식으로 해결 가능
2️⃣ JSON에 특정 키가 없는 경우는 항상 빈 문자열(Int의 경우에는 -1, Bool의 경우에는 false, data의 경우 에 는 nil)을 넣어주도록 코드 수정 ==> 에러 발생 x
❗️CodingKey는 왜 사용하는 건가요??
📚 JSON의 키 이름이나 타입을 다르게 하고 싶을 때 CodingKey를 정의해줘야 한다!!
(의문점)
=> 우리 코드에서 이름은 변경되지 않았는데, 왜 case status = "status" 를 해줘야 하나요??
=> 그냥 case status 로 놔둬도 되는 것 아닌가용......????😭😭😭
//decodable의 init(from:)은 NSCoding의 init(coder:)와 거의 비슷하다. 다만 Decoder프로토콜은 다음의 차이를 갖는다.
Decoder에서 키는 미리 정의된 코딩키만 사용가능하다. 즉, 디코더로부터 데이터를 바로 추출하지 못하고 코딩키에 의존하는 컨테이너를 생성해야 한다. 컨테이너는 연결된 디코더의 내부 메모리 뷰의 각 영역을 키를 통해서 액세스할 수 있게 해주는 장치이다. 컨테이너로부터 특정한 키의 값을 얻는 메소드는 decode(_:forKey:)인데, 얻어야 할 값의 타입을 먼저 전달해야 하고, 키에는 CodingKey 프로토콜의 enum타입값을 전달한다. 이와 같은 구조를 염두에 두면 init(from:)의 구현이 쉽게 이해될 것이다.
-
NetworkResult.swift
//서버 통신에 따른 결과를 case별로 나눠 저장함
enum NetworkResult<T> { case success(T) //성공 -> value를 가진 success case requestErr(T) //요청 에러 -> value를 가진 requestErr case pathErr //경로 에러 case serverErr //서버 내부 에러 case networkFail //네트워크 연결 실패 }
-
LoginService.swift
👉🏻👉🏻 Alamofire 라이브러리를 이용하여 실제로 로그인 과정의 데이터 통신을 시작하는 단계이다.
*** Closure는 익명함수로 알려진 기능으로, 함수를 func키워드로 선언하는 것이 아니라, 함수를 변수에 선언하는 형태를 취하고 있다. => Closure는 코드를 간결하고, 직관적으로 작성하는 데 많은 도움을 준다.
- Closure Expression Syntax
{(parameters) -> return type in statements }
클로저가 함수로부터 Escape한다는 것은 해당 함수의 인자로 클로저가 전달되지만, 함수가 반환된 후 실행되는 것을 의미한다. 함수의 인자가 함수의 영역을 탈출하여 함수 밖에서 사용할 수 있는 개념은 기존에 우리가 알고 있던 변수의 scope개념을 무시한다. 왜냐면 함수에서 선언된 로컬 변수가 로컬 변수의 영역을 뛰어넘어 함수 밖에서도 유효하기 때문이다.
일반 로컬 변수(주로 값들:
Int,String등등)가 함수 밖에서 살아있는 것은 전역 변수를 함수에 가져와서 값을 새로 주는 것과 크게 다르지 않아 보인다. 그래서 이와 같은Escape개념이 크게 의미가 없어 보인다. 하지만, 클로저의Escaping은A 함수가 마무리된 상태에서만 B 함수가 실행되도록함수를 작성할 수 있다는 점에서 유용하다.함수의 실행 순서를 보장 받을 수 있는 것은 상당히 중요한 기능이다. 왜냐하면, 이 순서 보장은 비동기 함수의 경우도 포함하기 때문이다. 서버에서 Json 형식의 데이터를 가져와 화면에 이를 보여주는 앱을 생각해보자. 이 때 HTTP 통신을 위해
Alamofire라이브러리를 사용한다.Alamofire라이브러리는 이 같은 경우 흔히 아래와 같은 형태로 사용된다.let dataRequest = Alamofire.request(APIConstants.signinURL, method: .post, parameters: makeParameter(id, pwd), encoding: JSONEncoding.default, headers: header)
Alamofire.request(urlRequest)메소드는 서버로Request를 전송한다. 여기서는 post 방식으로 Json 형식의 데이터를 받아온다. 그리고 그 결과는Response객체를 통해 받을 수 있다. 일반적으로 서버에Request를 전송하고 그Response받아오는 함수들은 비동기로 작동하여Request를 보낸 직후 반환 되어버리는데, 어떻게 이 같은Response가Request결과를 기다리게 하는 형태로 함수를 작성할 수 있는 것일까? 답은Escaping Closure에 있다.login함수의 파라미터를 간단히 들여다보면 다음과 같이 되어 있다.func login(id:String, pwd: String, completion: @escaping (NetworkResult<Any>) -> Void) { let header: HTTPHeaders = ["Content-Type": "application/json"] //Request Header 생성 let dataRequest = Alamofire.request(APIConstants.signinURL, method: .post, parameters: makeParameter(id, pwd), encoding: JSONEncoding.default, headers: header) //데이터 통신 시작 -> 서버로 Request 전송 // escaping closure로 인해 dataResponse in 부분은 서버로부터 데이터가 들어온 다음 실행된다!! dataRequest.responseData { dataResponse in switch dataResponse.result { //기본적으로 ResultType은 success와 failure 두가지의 인자를 갖고 있다. //*표준라이브러리의 Result Type!!!* case .success: guard let statusCode = dataResponse.response?.statusCode else {return} guard let value = dataResponse.result.value else {return} let networkResult = self.judge(by: statusCode,value) completion(networkResult) case .failure:completion(.networkFail) } } }
눈여겨 볼 부분은
completion이다. 이completion는Escaping Closure형태로 작성되어 있다.즉,
completion는login함수가 반환되고(완전히 서버로부터 값을 가져 온 상태에서) 실행된다.그 부분이 바로
trailing closure형태로 작성되어 있는{ dataResponse in }부분이다. -
로그인부 구현
@IBAction func login(_ sender: UIButton) { guard let inputID = emailTextField.text else {return} guard let inputPWD = passwordTextField.text else {return} LoginService.shared.login(id: inputID, pwd: inputPWD) { networkResult in switch networkResult{ case .success(let token): guard let token = token as? String else {return} UserDefaults.standard.set(token, forKey: "token") guard let tabbarController = self.storyboard?.instantiateViewController(identifier: "customTabbarController") as? UITabBarController else {return} tabbarController.modalPresentationStyle = .fullScreen self.present(tabbarController, animated: true, completion: nil) case .requestErr(let message): guard let message = message as? String else {return} let alertViewController = UIAlertController(title: "로그인 실패", message: message, preferredStyle: .alert) let action = UIAlertAction(title: "확인", style: .cancel, handler: nil) alertViewController.addAction(action) self.present(alertViewController, animated: true, completion: nil) case .pathErr: print("path") case .serverErr: print("serverErr") case .networkFail: print("networkfail") } } }