-
1. Enum과 Stored Property
-
2. Concurrency Programming
-
3. API(Application Programming Interface)
-
4. Codable(Swift4+)
-
JSONSerialization vs Codable
-
5. Decodable
-
6. 로직의 분리
-
7. 메타 타입
-
8. Extension을 활용한 재사용 로직 개선
-
9. DispatchGroup
-
10. Custom Font 적용하기
-
11. User Notification Framework(Local)
-
11+. UserNotification Foreground
1. Enum과 Stored Property
Enums must not contain storde properties
열거형에서 저장 프로퍼티를 선언할 경우 다음과 같은 에러가 발생한다.
열거형은 컴파일 타임에 로드되므로 인스턴스를 생성할 수 없기 때문에,
- 저장 프로퍼티는 열거형 내에서 선언할 수 없다.
- 연산 프로퍼티는 값을 저장하지 않으므로 열거형 내에서 선언이 가능하다.
- 인스턴스 연산 프로퍼티는 인스턴스 프로퍼티만, 타입 연산 프로퍼티는 타입 프로퍼티만 사용이 가능하다.
enum AAA: String, CaseIterable {
static let baseURL = "https://www.naver.com"
case aa
case bb
case cc
// test: 인스턴스 연산 프로퍼티 -> 값을 저장하고 있지는 않고, 값을 사용할 수 있는 통로로서의 역할만 담당
// 통로로서의 역할이기 때문에 타입 연산 프로퍼티, 인스턴스 연산 프로퍼티 둘 다 가능
var test: URL {
return URL(string: "https://www.naver.com")!
}
// 인스턴스 연산 프로퍼티는 인스턴스 프로퍼티만 사용 가능
// 타입 연산 프로퍼티는 타입 프로퍼티만 사용 가능
static var photo: URL {
return URL(string: baseURL + self.allCases.randomElement()!.rawValue)!
}
}
2. Concurrency Programming
1. main sync(Serial Sync)
- Serial Queue인 Main Thread에서 Sync(동기)로 작업을 진행.
- main은 DispatchQueue로 보낸 작업이 완료될때 까지 기다리고, DispatchQueue는 main에게 작업을 넘기려고 하는 상황.
- 무한 대기 상태에 진입, 교착 상태(DeadLock) 발생
func serialSync() {
print("Start")
for i in 1...100 {
sleep(1)
print(i, terminator: " ")
}
// 무한 대기 상태, 교착 상태(deadlock)
DispatchQueue.main.sync {
for i in 101...200 {
sleep(1)
print(i, terminator: " ")
}
}
print("End")
}

2. main async(Serial Async)
- Serial Queue인 Main Thread에서 ASync(비동기)로 작업을 진행.
- main에서 DispatchQueue에 작업을 보내고, main은 나머지 작업을 함.
- DispatchQueue에서 main에 async로 작업을 보내면, main의 작업이 끝날때 까지 해당 작업을 기다림.
func serialAsync() {
print("Start")
DispatchQueue.main.async {
for i in 1...50 {
sleep(1)
print(i, terminator: " ")
}
}
for i in 50...100 {
sleep(1)
print(i, terminator: " ")
}
print("End")
}

3. global sync(Concurrent Sync) ... 잘 안쓰임
- Concurrent Queue인 Global Thread에서 Sync(동기)로 작업을 진행.
- Main Thread에서 DispatchQueue에게 작업을 보내고, sync이므로 Main은 작업을 기다림.
- DispatchQueue는 받은 작업을 global thread에 작업을 보냄 = Main Thread에서 작업을 직접 수행하는 것과 똑같다고 보면 됨.
func globalSync() {
print("Start")
DispatchQueue.global().sync {
for i in 1...50 {
sleep(1)
print(i, terminator: " ")
}
}
for i in 50...100 {
sleep(1)
print(i, terminator: " ")
}
print("End")
}

4. global asnyc(Concurrent Async)
- Concurrent Queue인 Global Thread에서 Sync(동기)로 작업을 진행.
- main에서 DispatchQueue로 작업을 보내고, 다른 작업을 진행.
- DispatchQueue는 global 스레드에 해당 작업을 맡김.
func globalAsyncTwo() {
print("Start")
for i in 1...100 {
DispatchQueue.global().async {
sleep(1)
print(i, terminator: " ")
}
}
for i in 101...200 {
sleep(1)
print(i, terminator: " ")
}
print("End")
}

3. API(Application Programming Interface)
1. REST API
- 네트워크를 통해 핵심 컨텐츠와 기능을 활용할 수 있도록 제공되는 인터페이스, 아키텍처 스타일.
- 자원(Resource)을 중심으로 엔드포인트(URI)를 생성하고, HTTP method(GET, POST, PUT, DELETE)를 통해 동작을 수행.
- 웹의 장점을 최대한 활용한 아키텍처.
- HTTP(S)에서 손쉽게 구현 가능.
- 특정 언어나 기술에 종속되지 않음.
- API 의도를 직관적으로 파악할 수 있음.
2. REST API 6원칙
- Uniform Interface(유니폼 인터페이스)
- 자원에 대한 식별이 가능해야함.
- HTTP method를 통해 자원을 조작해야 함.
- Stateless(무상태)
- HTTP의 특징
- REST는 HTTP 위에서 구현되므로, REST 또한 무상태성!
- 클라이언트의 상태가 서버에 저장되지 않고, 각 요청에 대한 응답을 전송 받는 것으로 요청 종료.
- Cacheable(캐시 기능)
- REST는 HTTP 위에서 구현되므로, HTTP의 캐싱 기능을 활용할 수 있음..
- 다양한 캐싱 전략을 통해 서버의 부하를 감소시킬 수 있음.
- 네트워크 리소스 및 인프라 리소스를 경감시킬 수 있음.
- Self-Descriptiveness(자체 표현 구조)
- REST API 메시지(Response/ 요청 URL/ Endpoint 등) 만 보고도 어떤 의도로 구성되어 있는지 파악할 수 있어야 함.
- 쉽게 이해 할 수 있는 자체 표현 구조를 거쳐야함.
- Client-Server 구조
- 계층형 구조
3. REST API의 장단점
장점
- 웹의 장점을 최대한 활용한 아키텍처
- 기존의 웹 환경인 TCP/IP 연결을 통해 HTTP(S)에서 손쉽게 구현할 수 있음.
- 별도의 프로토콜 구현이 필요없음.
- 특정 언어나 기술에 종속되지 않음.
- API 엔드포인트나 메시지만 가지고 해당 API의 의도를 직관적으로 파악할 수 있음 -> REST 아키텍처
단점
- Overfetching
- 필요한 정보값보다 더 많은 정보값이 로딩될 수 있음.
- ex) 영화 API에서 사용하고자 하는 데이터는 1개이나 Response에 대한 조작이 불가능.
- Underfetching
- 필요한 정보보다 부족한 정보 로딩으로 인해 추가 API 요청이 필요.
- ex) 영화 API에서 영화 전반적인 데이터를 필요로 하더라도, 세부적인 데이터들이 각각 개별적인 API로 분리되어 있으므로 추가적인 Request가 필요함.
- Endpoint
- 서비스 규모가 커질수록 엔드포인트가 늘어나 관리하기 어려워짐.
- 서비스 pivot 및 업데이트로 기존 API 엔드포인트가 삭제되거나 변경될 경우, 클라이언트 업데이트를 하지 않은 사용자에게 문제가 발생할 수 있음.
- 이를 해결하기 위해, API 설계시 /v1/user, /v2/user 형태로 API 버전 관리
4. Codable(Swift4+)
- JSON과 같은 외부 표현과의 호환성을 위해 데이터 유형을 인코딩 및 디코딩할 수 있는 프로토콜.
- JSON 뿐 아니라, 디스크에 데이터 저장, 네트워크 연결 등을 통한 API 통신 등의 작업에서는 데이터가 전송되는 동안 중간 형식(intermediate format)으로 데이터를 인코딩 및 디코딩해야 하는 경우가 많은데, Swift 표준 라이브러리에서는 Codable을 통해 데이터 인코딩 및 디코딩에 대한 표준화된 접근 방식을 제공하고 있음.
Client ↔ Server
- Encoding = serialization , 데이터 직렬화
- Decoding = deserialization, 데이터 역직렬화
JSON parsing performance
1. JSONDecoder ( 적절히 빠름 )
2. JSONSerialization ( 제일 빠르지만 불편함 )
3. SwiftJSON ( 극단적으로 많아질수록 느림 )
JSONSerialization vs Codable
- JSONSerialization 은 Swift4에서 Codable이 등장하기 전까지 사용하던 클래스.
- JSON 값 중 단일한 값만 가져오는 거라면 JSONSerialization 이 빠르지만, 중첩된 JSON 구조 또는 여러번 반복해서 데이터를 가져와야 하는 경우 Codable이 빠름.
- SwiftyJSON 라이브러리는 JSONSerialization 을 기반으로 구현됨.
- 속도가 가장 오래 걸리지만 JSON 객체를 딕셔너리 형태로 접근할 수 있다는 장점이 있음.
5. Decodable
- Struct, Enum, Class에서 모두 채택할 수 있다.
- Key값이 동일하지 않거나 호환되지 않는 형식이 저장되어 있을 경우 디코딩은 실패하므로, 별도의 처리가 필요함
- 스펠링 오류 등
- 서버의 키 값과 다른 키를 사용할 경우 등
- 옵셔널 타입으로 선언하지 않는 키에 nil 값이 올 경우 등
1. String -> Data -> Type(Decode)
// String => Data => Quote (디코딩, 역직렬화)
struct Quote: Decodable {
let quote: String
let author: String
let category: String
}
// String => Data
guard let result = json.data(using: .utf8) else {
fatalError("ERROR")
}
print(result)
dump(result)
// Data => Quote
// Error handling, Do Try Catch, Meta Type
do {
let value = try JSONDecoder().decode(Quote.self, from: result)
print(value)
print(value.author)
} catch {
print(error)
}
2. DecodingStrategy, Optional Property
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let value = try decoder.decode(Quote.self, from: data)
dump(value)
} catch {
print(error)
}
- Decoding Strategy: SnakeCase
- 키 값이 같지 않을 경우 디코딩이 실패함.
- 만약 서버의 키 값과 모델의 키 값이 Snake Case로 해결이 되는 경우라면, 런타임 오류를 해결하고 원하는 Value를 얻을 수 있음.
- 옵셔널 프로퍼티
- 프로퍼티를 옵셔널로 구현하여 런타임 에러 방지
- CodingKeys
- 코딩키는 커스텀 키가 필요할때만 작성하면 됨!!
3. Custom Decoding
- 서버에서 받은 값을 그대로 사용하지 않고 일부 제약 조건을 추가하거나 값에 대한 변형을 하고 싶은 경우가 있음. 또는 nil 값일 경우 대체할 문자열을 추가하고 싶을때 사용
- 디코딩 한 이후 로직으로 구현해도 되지만, 디코딩을 하면서 원하는 결과를 바로 얻고 싶을때 용이
- init(from decoder: Decoder) throws 메서드에서 처리하면 된다
// String => Data => Quote (디코딩, 역직렬화)
struct Quote: Decodable {
let content: String
let name: String
let like: Int
let isInfluencer: Bool // 좋아요 3만개 이상
enum CodingKeys: String, CodingKey {
case content = "quote_content"
case name = "aurgor_name"
case like = "likelike"
}
// 수동으로 디코딩 하는 로직
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
content = try container.decode(String.self, forKey: .content)
like = try container.decode(Int.self, forKey: .like)
isInfluencer = (30000...).contains(like) ? true : false
// 옵셔널 타입 디코딩
name = (try? container.decodeIfPresent(String.self, forKey: .name)) ?? "unknown"
}
}
6. 로직의 분리
- ViewController는 UIKit 만 import → UI 로직만 가지고 있음
- Alamofire를 import 하고 있다 → 네트워크 로직이 분리가 안된 것임
- Completion Handler → 네트워크 API Call에 대한 응답을 completion handler로 담음
import Foundation
import Alamofire
final class LottoManager {
static let shared = LottoManager()
private init() { }
func callResponse(completion: @escaping (Lotto) -> ()) {
let url = "https://www.dhlottery.co.kr/common.do?method=getLottoNumber&drwNo=1080"
// 통신, 응답
AF.request(url, method: .get).validate()
.responseDecodable(of: Lotto.self) { response in
guard let value = response.value else { return }
completion(value)
}
}
}
- 네트워크 로직을 담당하는 네트워크 매니저 싱글턴 객체
7. 메타 타입
메타 타입은 타입의 타입을 의미함.
특정 타입의 인스턴스를 저장하거나 사용하는게 아니라, 타입 그 자체를 저장할 수 있다는 것을 의미.
import UIKit
struct User {
let name = "예스코치" // 인스턴스 프로퍼티
static let originalName = "YesCoach" // 타입(메타타입) 프로퍼티
}
User.originalName
User.self.originalName
// 메타 타입 그 자체는 User.Type, 메타 타입의 값은 User.self
let user = User()
user.name
// "예스코치" => String
// String => String.type
// User() => User
// User => User.type
// 메타 타입은 클래스 구조체 열거형 등의 유형 그 자체를 가리킴
type(of: user.name) // String.Type
type(of: user) // User.Type
// 타입에 대한 값을 가져오려면, 타입.self
// 타입에 대한 확장,, .self
let number: Int = 8.self
let result: Int.Type = Int.self
// Int: Instance Type, Int.Type: Type의 Type
8. Extension을 활용한 재사용 로직 개선
import UIKit
protocol ReusableViewProtocol {
static var identifier: String { get }
}
extension UITableViewCell: ReusableViewProtocol { }
extension UIViewController: ReusableViewProtocol { }
extension UICollectionViewCell: ReusableViewProtocol { }
extension UICollectionReusableView: ReusableViewProtocol { }
extension ReusableViewProtocol {
static var identifier: String {
return String(describing: self)
}
}
프로토콜을 통해 재사용 가능한 UIComponent에서 사용 가능한 identifier 프로퍼티를 선언한다.
이후, 재사용하는 UI 객체들이 해당 프로토콜을 채택하도록 하고, 프로토콜에 대한 extension으로 기본 구현을 진행했다.
위처럼 프로토콜 자체에 대한 확장으로 기본구현을 해줄 경우, 특정 객체만 별개의 구현을 진행하면 어떻게 될까?
타입에 대한 extension에서 따로 구현을 진행하는 경우, 프로토콜 자체에 대한 기본 구현은 무시된다!
import UIKit
protocol ReusableViewProtocol {
static var identifier: String { get }
}
extension UITableViewCell: ReusableViewProtocol {
static var identifier: Int {
return 1
}
}
extension UIViewController: ReusableViewProtocol { }
extension UICollectionViewCell: ReusableViewProtocol { }
extension UICollectionReusableView: ReusableViewProtocol { }
extension ReusableViewProtocol {
static var identifier: String {
return String(describing: self)
}
}

9. DispatchGroup
서로 다른 Task들을 그룹화 해서, Queue에 보낸 Task들이 작업을 완료할 때 까지 기다리고, 모든 Task가 완료되면 notify를 통해 알림을 받을 수 있음.
- group: 네트워크 통신과 같은 비동기 함수가 group으로 묶이는 경우, 비동기 함수는 다른 쓰레드에서 동작하는 요소이기 때문에 이 작업이 끝날때 까지 기다리지 않고 notify를 바로 띄우는 문제가 있음.
- enter / leave: 비동기 함수에 적용. enter를 하면 RC가 1 증가하고, leave를 하면 RC가 1 감소함. RC가 0이 될때 비로소 notify 작업을 수행함.
10. Custom Font 적용하기
- otf, ttf 파일을 앱에 등록 하므로
- 용량이 크진 않은지 고려해야함(R, B, L → light)
- 서비스 로고만 사용하는 경우 굳이 폰트로 넣을 필요는 없다. → 이미지로 해결
- 채팅 UI → QA에서 걸렸는데, 신조어, 줄임말 등이 폰트에 반영되지 않는 경우가 생각보다 많다

Fonts provided by application


11. User Notification Framework(Local)
- 사용자 디바이스에 앱의 알림을 표시하는 기능이 담긴 프레임워크.
- 앱 실행 여부와 상관 없이, 중요한 정보를 전달할 수 있음. 알림과 함께 소리를 재생하거나, 앱 아이콘에 뱃지를 지정할 수 있음.
- 사용자가 권한을 허용해야만 알림 센터에 알림이 표시될 수 있음.
- User Notification을 통해 앱의 재사용률(Retention)에 기여할 수 있음.
- 알림은 로컬 환경에서 구현하거나, 서버에서 원격으로 생성할 수 있음.
1. Local Notification(로컬 알림)
- 앱 내부에서 기기에 알림을 전달하는 방법.
- 대체적으로 로컬 알림은 같은 시각에 비슷한 컨텐츠로 구성되어 있음.
- 개인 앱이나 1인이 사용하는 앱에서 많이 활용되고 있음.
- ex) 일기, 디데이, 할일 어플 등
2. Remote Notification(원격 알림)
- 서버에서 사용자 기기에 알람을 전달하는 방법. = Push Notification(푸시 알림)
- 대체적으로 일정하지 않은 시간에 다양한 컨텐츠로 구성되어 있음.
- 서버가 전달한 컨텐츠가 애플의 알림 서버(APNS, Apple Push Notification Service)를 통해 사용자 기기에 전달됨.
- 유료 개발자 계정이 있어야 구현 및 테스트 가능, 시뮬에서 테스트 불가.
3. 구현
로컬 노티피케이션의 구현 흐름은 다음과 같다.
1) 유저로부터 Authorization(권한) 받기
- 권한이 필요한 내용은 대표적으로 Alert(알림), Badge(배지), Sound(소리)가 있음
- 권한에 대한 문구는 시스템에 미리 구현되어 있음
func requestNotificationAuthorization() {
let authOptions = UNAuthorizationOptions(arrayLiteral: .alert, .badge, .sound)
userNotificationCenter.requestAuthorization(options: authOptions) { success, error in
if success {
self.sendNotification(seconds: 60)
} else {
print("ERROR")
}
}
}
- UNAuthorizationStatus : 권한 허용, 거부 등 사용자의 다양한 상태값에 대한 대응도 필요하다.
UNAuthorizationStatus | Apple Developer Documentation
Constants indicating whether the app is allowed to schedule notifications.
developer.apple.com
2) Request(요청)
UNMutableNotificationContent
- 알림을 통해 전달할 정보를 관장하는 클래스.
- title, subtitle, body, sound 등의 정보를 작성할 수 있음. attachment를 통해 부가적인 정보 표현도 가능.
trigger
- 시간 간격(UNTimeIntervalNotificationTrigger): 최소 60초 이상의 시간 간격일 때만 반복 알림 설정이 가능.
- 캘린더(UNCalendarNotificationTrigger)
- 위치 기반(UNLocationNotificationTrigger)
3) UNNotificationRequest
- 로컬 알림을 예약하기 위해 요청하는 클래스.
- 알림을 통해 전달할 정보와 전달을 위한 트리거, 알림에 대한 Identifier를 포함하고 있음.
- Identifier: 알림에 대한 고유한 값, 여러 알림을 보내도 Identifier가 동일하면 알림 내용이 수정되는 형태로 동작. Identifier가 다르면 알림이 스택 형태로 쌓이게 됨.
func sendNotification(
title: String,
body: String,
seconds: Double,
isRepeat: Bool,
key: NotificationKey
) {
let notificationContent = UNMutableNotificationContent()
notificationContent.title = title
notificationContent.body = body
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: seconds, repeats: isRepeat)
let request = UNNotificationRequest(
identifier: key.identifier,
content: notificationContent,
trigger: trigger
)
userNotification.add(request) { error in
if let error {
debugPrint(error)
}
}
}
11+. UserNotification Foreground
UserNotification은 기본적으로 Foreground에서 뜨지 않음.
하지만 코드를 통해 Foreground에서 알람을 뜨게 할 수 있다!
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.sound, .badge, .banner, .list])
}
}
UNUserNotificationCenterDelegate의 willPresentNotification을 구현하고, delegate를 할당하면 끗.
+. 앱 Foreground에 진입하면 배지 숫자 초기화하기
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
// 앱 배지 숫자 초기화
UIApplication.shared.applicationIconBadgeNumber = 0
}
SceneDelegate의 sceneDidBecomeActive 에서 applicationIconBadgeNumber를 0으로 초기화하면 끗.
'SeSAC' 카테고리의 다른 글
8.23 TIL(SeSAC iOS 3기), CoreLocation (0) | 2023.08.27 |
---|---|
8.21 ~ 8.22 TIL(SeSAC iOS 3기) (0) | 2023.08.27 |
8.7 ~ 8.11 TIL (0) | 2023.08.15 |
7.31 ~ 8.4 TIL (0) | 2023.08.09 |
7.24 ~ 7.28 TIL (0) | 2023.08.02 |
1. Enum과 Stored Property
Enums must not contain storde properties
열거형에서 저장 프로퍼티를 선언할 경우 다음과 같은 에러가 발생한다.
열거형은 컴파일 타임에 로드되므로 인스턴스를 생성할 수 없기 때문에,
- 저장 프로퍼티는 열거형 내에서 선언할 수 없다.
- 연산 프로퍼티는 값을 저장하지 않으므로 열거형 내에서 선언이 가능하다.
- 인스턴스 연산 프로퍼티는 인스턴스 프로퍼티만, 타입 연산 프로퍼티는 타입 프로퍼티만 사용이 가능하다.
enum AAA: String, CaseIterable {
static let baseURL = "https://www.naver.com"
case aa
case bb
case cc
// test: 인스턴스 연산 프로퍼티 -> 값을 저장하고 있지는 않고, 값을 사용할 수 있는 통로로서의 역할만 담당
// 통로로서의 역할이기 때문에 타입 연산 프로퍼티, 인스턴스 연산 프로퍼티 둘 다 가능
var test: URL {
return URL(string: "https://www.naver.com")!
}
// 인스턴스 연산 프로퍼티는 인스턴스 프로퍼티만 사용 가능
// 타입 연산 프로퍼티는 타입 프로퍼티만 사용 가능
static var photo: URL {
return URL(string: baseURL + self.allCases.randomElement()!.rawValue)!
}
}
2. Concurrency Programming
1. main sync(Serial Sync)
- Serial Queue인 Main Thread에서 Sync(동기)로 작업을 진행.
- main은 DispatchQueue로 보낸 작업이 완료될때 까지 기다리고, DispatchQueue는 main에게 작업을 넘기려고 하는 상황.
- 무한 대기 상태에 진입, 교착 상태(DeadLock) 발생
func serialSync() {
print("Start")
for i in 1...100 {
sleep(1)
print(i, terminator: " ")
}
// 무한 대기 상태, 교착 상태(deadlock)
DispatchQueue.main.sync {
for i in 101...200 {
sleep(1)
print(i, terminator: " ")
}
}
print("End")
}

2. main async(Serial Async)
- Serial Queue인 Main Thread에서 ASync(비동기)로 작업을 진행.
- main에서 DispatchQueue에 작업을 보내고, main은 나머지 작업을 함.
- DispatchQueue에서 main에 async로 작업을 보내면, main의 작업이 끝날때 까지 해당 작업을 기다림.
func serialAsync() {
print("Start")
DispatchQueue.main.async {
for i in 1...50 {
sleep(1)
print(i, terminator: " ")
}
}
for i in 50...100 {
sleep(1)
print(i, terminator: " ")
}
print("End")
}

3. global sync(Concurrent Sync) ... 잘 안쓰임
- Concurrent Queue인 Global Thread에서 Sync(동기)로 작업을 진행.
- Main Thread에서 DispatchQueue에게 작업을 보내고, sync이므로 Main은 작업을 기다림.
- DispatchQueue는 받은 작업을 global thread에 작업을 보냄 = Main Thread에서 작업을 직접 수행하는 것과 똑같다고 보면 됨.
func globalSync() {
print("Start")
DispatchQueue.global().sync {
for i in 1...50 {
sleep(1)
print(i, terminator: " ")
}
}
for i in 50...100 {
sleep(1)
print(i, terminator: " ")
}
print("End")
}

4. global asnyc(Concurrent Async)
- Concurrent Queue인 Global Thread에서 Sync(동기)로 작업을 진행.
- main에서 DispatchQueue로 작업을 보내고, 다른 작업을 진행.
- DispatchQueue는 global 스레드에 해당 작업을 맡김.
func globalAsyncTwo() {
print("Start")
for i in 1...100 {
DispatchQueue.global().async {
sleep(1)
print(i, terminator: " ")
}
}
for i in 101...200 {
sleep(1)
print(i, terminator: " ")
}
print("End")
}

3. API(Application Programming Interface)
1. REST API
- 네트워크를 통해 핵심 컨텐츠와 기능을 활용할 수 있도록 제공되는 인터페이스, 아키텍처 스타일.
- 자원(Resource)을 중심으로 엔드포인트(URI)를 생성하고, HTTP method(GET, POST, PUT, DELETE)를 통해 동작을 수행.
- 웹의 장점을 최대한 활용한 아키텍처.
- HTTP(S)에서 손쉽게 구현 가능.
- 특정 언어나 기술에 종속되지 않음.
- API 의도를 직관적으로 파악할 수 있음.
2. REST API 6원칙
- Uniform Interface(유니폼 인터페이스)
- 자원에 대한 식별이 가능해야함.
- HTTP method를 통해 자원을 조작해야 함.
- Stateless(무상태)
- HTTP의 특징
- REST는 HTTP 위에서 구현되므로, REST 또한 무상태성!
- 클라이언트의 상태가 서버에 저장되지 않고, 각 요청에 대한 응답을 전송 받는 것으로 요청 종료.
- Cacheable(캐시 기능)
- REST는 HTTP 위에서 구현되므로, HTTP의 캐싱 기능을 활용할 수 있음..
- 다양한 캐싱 전략을 통해 서버의 부하를 감소시킬 수 있음.
- 네트워크 리소스 및 인프라 리소스를 경감시킬 수 있음.
- Self-Descriptiveness(자체 표현 구조)
- REST API 메시지(Response/ 요청 URL/ Endpoint 등) 만 보고도 어떤 의도로 구성되어 있는지 파악할 수 있어야 함.
- 쉽게 이해 할 수 있는 자체 표현 구조를 거쳐야함.
- Client-Server 구조
- 계층형 구조
3. REST API의 장단점
장점
- 웹의 장점을 최대한 활용한 아키텍처
- 기존의 웹 환경인 TCP/IP 연결을 통해 HTTP(S)에서 손쉽게 구현할 수 있음.
- 별도의 프로토콜 구현이 필요없음.
- 특정 언어나 기술에 종속되지 않음.
- API 엔드포인트나 메시지만 가지고 해당 API의 의도를 직관적으로 파악할 수 있음 -> REST 아키텍처
단점
- Overfetching
- 필요한 정보값보다 더 많은 정보값이 로딩될 수 있음.
- ex) 영화 API에서 사용하고자 하는 데이터는 1개이나 Response에 대한 조작이 불가능.
- Underfetching
- 필요한 정보보다 부족한 정보 로딩으로 인해 추가 API 요청이 필요.
- ex) 영화 API에서 영화 전반적인 데이터를 필요로 하더라도, 세부적인 데이터들이 각각 개별적인 API로 분리되어 있으므로 추가적인 Request가 필요함.
- Endpoint
- 서비스 규모가 커질수록 엔드포인트가 늘어나 관리하기 어려워짐.
- 서비스 pivot 및 업데이트로 기존 API 엔드포인트가 삭제되거나 변경될 경우, 클라이언트 업데이트를 하지 않은 사용자에게 문제가 발생할 수 있음.
- 이를 해결하기 위해, API 설계시 /v1/user, /v2/user 형태로 API 버전 관리
4. Codable(Swift4+)
- JSON과 같은 외부 표현과의 호환성을 위해 데이터 유형을 인코딩 및 디코딩할 수 있는 프로토콜.
- JSON 뿐 아니라, 디스크에 데이터 저장, 네트워크 연결 등을 통한 API 통신 등의 작업에서는 데이터가 전송되는 동안 중간 형식(intermediate format)으로 데이터를 인코딩 및 디코딩해야 하는 경우가 많은데, Swift 표준 라이브러리에서는 Codable을 통해 데이터 인코딩 및 디코딩에 대한 표준화된 접근 방식을 제공하고 있음.
Client ↔ Server
- Encoding = serialization , 데이터 직렬화
- Decoding = deserialization, 데이터 역직렬화
JSON parsing performance
1. JSONDecoder ( 적절히 빠름 )
2. JSONSerialization ( 제일 빠르지만 불편함 )
3. SwiftJSON ( 극단적으로 많아질수록 느림 )
JSONSerialization vs Codable
- JSONSerialization 은 Swift4에서 Codable이 등장하기 전까지 사용하던 클래스.
- JSON 값 중 단일한 값만 가져오는 거라면 JSONSerialization 이 빠르지만, 중첩된 JSON 구조 또는 여러번 반복해서 데이터를 가져와야 하는 경우 Codable이 빠름.
- SwiftyJSON 라이브러리는 JSONSerialization 을 기반으로 구현됨.
- 속도가 가장 오래 걸리지만 JSON 객체를 딕셔너리 형태로 접근할 수 있다는 장점이 있음.
5. Decodable
- Struct, Enum, Class에서 모두 채택할 수 있다.
- Key값이 동일하지 않거나 호환되지 않는 형식이 저장되어 있을 경우 디코딩은 실패하므로, 별도의 처리가 필요함
- 스펠링 오류 등
- 서버의 키 값과 다른 키를 사용할 경우 등
- 옵셔널 타입으로 선언하지 않는 키에 nil 값이 올 경우 등
1. String -> Data -> Type(Decode)
// String => Data => Quote (디코딩, 역직렬화)
struct Quote: Decodable {
let quote: String
let author: String
let category: String
}
// String => Data
guard let result = json.data(using: .utf8) else {
fatalError("ERROR")
}
print(result)
dump(result)
// Data => Quote
// Error handling, Do Try Catch, Meta Type
do {
let value = try JSONDecoder().decode(Quote.self, from: result)
print(value)
print(value.author)
} catch {
print(error)
}
2. DecodingStrategy, Optional Property
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let value = try decoder.decode(Quote.self, from: data)
dump(value)
} catch {
print(error)
}
- Decoding Strategy: SnakeCase
- 키 값이 같지 않을 경우 디코딩이 실패함.
- 만약 서버의 키 값과 모델의 키 값이 Snake Case로 해결이 되는 경우라면, 런타임 오류를 해결하고 원하는 Value를 얻을 수 있음.
- 옵셔널 프로퍼티
- 프로퍼티를 옵셔널로 구현하여 런타임 에러 방지
- CodingKeys
- 코딩키는 커스텀 키가 필요할때만 작성하면 됨!!
3. Custom Decoding
- 서버에서 받은 값을 그대로 사용하지 않고 일부 제약 조건을 추가하거나 값에 대한 변형을 하고 싶은 경우가 있음. 또는 nil 값일 경우 대체할 문자열을 추가하고 싶을때 사용
- 디코딩 한 이후 로직으로 구현해도 되지만, 디코딩을 하면서 원하는 결과를 바로 얻고 싶을때 용이
- init(from decoder: Decoder) throws 메서드에서 처리하면 된다
// String => Data => Quote (디코딩, 역직렬화)
struct Quote: Decodable {
let content: String
let name: String
let like: Int
let isInfluencer: Bool // 좋아요 3만개 이상
enum CodingKeys: String, CodingKey {
case content = "quote_content"
case name = "aurgor_name"
case like = "likelike"
}
// 수동으로 디코딩 하는 로직
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
content = try container.decode(String.self, forKey: .content)
like = try container.decode(Int.self, forKey: .like)
isInfluencer = (30000...).contains(like) ? true : false
// 옵셔널 타입 디코딩
name = (try? container.decodeIfPresent(String.self, forKey: .name)) ?? "unknown"
}
}
6. 로직의 분리
- ViewController는 UIKit 만 import → UI 로직만 가지고 있음
- Alamofire를 import 하고 있다 → 네트워크 로직이 분리가 안된 것임
- Completion Handler → 네트워크 API Call에 대한 응답을 completion handler로 담음
import Foundation
import Alamofire
final class LottoManager {
static let shared = LottoManager()
private init() { }
func callResponse(completion: @escaping (Lotto) -> ()) {
let url = "https://www.dhlottery.co.kr/common.do?method=getLottoNumber&drwNo=1080"
// 통신, 응답
AF.request(url, method: .get).validate()
.responseDecodable(of: Lotto.self) { response in
guard let value = response.value else { return }
completion(value)
}
}
}
- 네트워크 로직을 담당하는 네트워크 매니저 싱글턴 객체
7. 메타 타입
메타 타입은 타입의 타입을 의미함.
특정 타입의 인스턴스를 저장하거나 사용하는게 아니라, 타입 그 자체를 저장할 수 있다는 것을 의미.
import UIKit
struct User {
let name = "예스코치" // 인스턴스 프로퍼티
static let originalName = "YesCoach" // 타입(메타타입) 프로퍼티
}
User.originalName
User.self.originalName
// 메타 타입 그 자체는 User.Type, 메타 타입의 값은 User.self
let user = User()
user.name
// "예스코치" => String
// String => String.type
// User() => User
// User => User.type
// 메타 타입은 클래스 구조체 열거형 등의 유형 그 자체를 가리킴
type(of: user.name) // String.Type
type(of: user) // User.Type
// 타입에 대한 값을 가져오려면, 타입.self
// 타입에 대한 확장,, .self
let number: Int = 8.self
let result: Int.Type = Int.self
// Int: Instance Type, Int.Type: Type의 Type
8. Extension을 활용한 재사용 로직 개선
import UIKit
protocol ReusableViewProtocol {
static var identifier: String { get }
}
extension UITableViewCell: ReusableViewProtocol { }
extension UIViewController: ReusableViewProtocol { }
extension UICollectionViewCell: ReusableViewProtocol { }
extension UICollectionReusableView: ReusableViewProtocol { }
extension ReusableViewProtocol {
static var identifier: String {
return String(describing: self)
}
}
프로토콜을 통해 재사용 가능한 UIComponent에서 사용 가능한 identifier 프로퍼티를 선언한다.
이후, 재사용하는 UI 객체들이 해당 프로토콜을 채택하도록 하고, 프로토콜에 대한 extension으로 기본 구현을 진행했다.
위처럼 프로토콜 자체에 대한 확장으로 기본구현을 해줄 경우, 특정 객체만 별개의 구현을 진행하면 어떻게 될까?
타입에 대한 extension에서 따로 구현을 진행하는 경우, 프로토콜 자체에 대한 기본 구현은 무시된다!
import UIKit
protocol ReusableViewProtocol {
static var identifier: String { get }
}
extension UITableViewCell: ReusableViewProtocol {
static var identifier: Int {
return 1
}
}
extension UIViewController: ReusableViewProtocol { }
extension UICollectionViewCell: ReusableViewProtocol { }
extension UICollectionReusableView: ReusableViewProtocol { }
extension ReusableViewProtocol {
static var identifier: String {
return String(describing: self)
}
}

9. DispatchGroup
서로 다른 Task들을 그룹화 해서, Queue에 보낸 Task들이 작업을 완료할 때 까지 기다리고, 모든 Task가 완료되면 notify를 통해 알림을 받을 수 있음.
- group: 네트워크 통신과 같은 비동기 함수가 group으로 묶이는 경우, 비동기 함수는 다른 쓰레드에서 동작하는 요소이기 때문에 이 작업이 끝날때 까지 기다리지 않고 notify를 바로 띄우는 문제가 있음.
- enter / leave: 비동기 함수에 적용. enter를 하면 RC가 1 증가하고, leave를 하면 RC가 1 감소함. RC가 0이 될때 비로소 notify 작업을 수행함.
10. Custom Font 적용하기
- otf, ttf 파일을 앱에 등록 하므로
- 용량이 크진 않은지 고려해야함(R, B, L → light)
- 서비스 로고만 사용하는 경우 굳이 폰트로 넣을 필요는 없다. → 이미지로 해결
- 채팅 UI → QA에서 걸렸는데, 신조어, 줄임말 등이 폰트에 반영되지 않는 경우가 생각보다 많다

Fonts provided by application


11. User Notification Framework(Local)
- 사용자 디바이스에 앱의 알림을 표시하는 기능이 담긴 프레임워크.
- 앱 실행 여부와 상관 없이, 중요한 정보를 전달할 수 있음. 알림과 함께 소리를 재생하거나, 앱 아이콘에 뱃지를 지정할 수 있음.
- 사용자가 권한을 허용해야만 알림 센터에 알림이 표시될 수 있음.
- User Notification을 통해 앱의 재사용률(Retention)에 기여할 수 있음.
- 알림은 로컬 환경에서 구현하거나, 서버에서 원격으로 생성할 수 있음.
1. Local Notification(로컬 알림)
- 앱 내부에서 기기에 알림을 전달하는 방법.
- 대체적으로 로컬 알림은 같은 시각에 비슷한 컨텐츠로 구성되어 있음.
- 개인 앱이나 1인이 사용하는 앱에서 많이 활용되고 있음.
- ex) 일기, 디데이, 할일 어플 등
2. Remote Notification(원격 알림)
- 서버에서 사용자 기기에 알람을 전달하는 방법. = Push Notification(푸시 알림)
- 대체적으로 일정하지 않은 시간에 다양한 컨텐츠로 구성되어 있음.
- 서버가 전달한 컨텐츠가 애플의 알림 서버(APNS, Apple Push Notification Service)를 통해 사용자 기기에 전달됨.
- 유료 개발자 계정이 있어야 구현 및 테스트 가능, 시뮬에서 테스트 불가.
3. 구현
로컬 노티피케이션의 구현 흐름은 다음과 같다.
1) 유저로부터 Authorization(권한) 받기
- 권한이 필요한 내용은 대표적으로 Alert(알림), Badge(배지), Sound(소리)가 있음
- 권한에 대한 문구는 시스템에 미리 구현되어 있음
func requestNotificationAuthorization() {
let authOptions = UNAuthorizationOptions(arrayLiteral: .alert, .badge, .sound)
userNotificationCenter.requestAuthorization(options: authOptions) { success, error in
if success {
self.sendNotification(seconds: 60)
} else {
print("ERROR")
}
}
}
- UNAuthorizationStatus : 권한 허용, 거부 등 사용자의 다양한 상태값에 대한 대응도 필요하다.
UNAuthorizationStatus | Apple Developer Documentation
Constants indicating whether the app is allowed to schedule notifications.
developer.apple.com
2) Request(요청)
UNMutableNotificationContent
- 알림을 통해 전달할 정보를 관장하는 클래스.
- title, subtitle, body, sound 등의 정보를 작성할 수 있음. attachment를 통해 부가적인 정보 표현도 가능.
trigger
- 시간 간격(UNTimeIntervalNotificationTrigger): 최소 60초 이상의 시간 간격일 때만 반복 알림 설정이 가능.
- 캘린더(UNCalendarNotificationTrigger)
- 위치 기반(UNLocationNotificationTrigger)
3) UNNotificationRequest
- 로컬 알림을 예약하기 위해 요청하는 클래스.
- 알림을 통해 전달할 정보와 전달을 위한 트리거, 알림에 대한 Identifier를 포함하고 있음.
- Identifier: 알림에 대한 고유한 값, 여러 알림을 보내도 Identifier가 동일하면 알림 내용이 수정되는 형태로 동작. Identifier가 다르면 알림이 스택 형태로 쌓이게 됨.
func sendNotification(
title: String,
body: String,
seconds: Double,
isRepeat: Bool,
key: NotificationKey
) {
let notificationContent = UNMutableNotificationContent()
notificationContent.title = title
notificationContent.body = body
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: seconds, repeats: isRepeat)
let request = UNNotificationRequest(
identifier: key.identifier,
content: notificationContent,
trigger: trigger
)
userNotification.add(request) { error in
if let error {
debugPrint(error)
}
}
}
11+. UserNotification Foreground
UserNotification은 기본적으로 Foreground에서 뜨지 않음.
하지만 코드를 통해 Foreground에서 알람을 뜨게 할 수 있다!
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.sound, .badge, .banner, .list])
}
}
UNUserNotificationCenterDelegate의 willPresentNotification을 구현하고, delegate를 할당하면 끗.
+. 앱 Foreground에 진입하면 배지 숫자 초기화하기
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
// 앱 배지 숫자 초기화
UIApplication.shared.applicationIconBadgeNumber = 0
}
SceneDelegate의 sceneDidBecomeActive 에서 applicationIconBadgeNumber를 0으로 초기화하면 끗.
'SeSAC' 카테고리의 다른 글
8.23 TIL(SeSAC iOS 3기), CoreLocation (0) | 2023.08.27 |
---|---|
8.21 ~ 8.22 TIL(SeSAC iOS 3기) (0) | 2023.08.27 |
8.7 ~ 8.11 TIL (0) | 2023.08.15 |
7.31 ~ 8.4 TIL (0) | 2023.08.09 |
7.24 ~ 7.28 TIL (0) | 2023.08.02 |