테스트용 REST API 소개
JSONPlaceholder (https://jsonplaceholder.typicode.com)는 클라이언트 개발 시 사용 가능한 무료 온라인 REST API 서비스입니다. 이 문서에서는 Swift의 동시성(concurrency) 기능과 Alamofire 라이브러리를 조합하여 다음과 같은 HTTP 요청을 수행하는 방법을 설명합니다:
- GET /posts/1 – 단일 게시물 조회
- GET /posts – 전체 게시물 목록 조회
- POST /posts – 새 게시물 생성
- PUT /posts/1 – 기존 게시물 수정
- DELETE /posts/1 – 게시물 삭제
프로젝트 생성
Xcode에서 새 프로젝트를 생성합니다. "File → New → Project…"를 선택하고, iOS 앱 템플릿에서 SwiftUI 인터페이스를 사용하는 앱을 만듭니다. 프로덕트 이름으로는 NetworkDemo를 입력하고, 원하는 위치에 프로젝트를 저장합니다.
Alamofire 패키지 추가
프로젝트 탐색기에서 프로젝트를 선택한 후 마우스 오른쪽 버튼으로 클릭하여 "Add Packages…"를 선택합니다. 팝업된 창에서 다음 URL을 입력합니다:
https://github.com/Alamofire/Alamofire
검색 후 나타나는 결과에서 Alamofire를 선택하고, 기본 브랜치를 추가합니다. 완료되면 "Package Dependencies" 섹션에 Alamofire 5.6.1 이상이 포함됩니다.
네트워크 통신 계층 설계
프로젝트 루트에 NetworkManager.swift 파일을 생성하고 아래 코드를 추가합니다. 이 클래스는 Alamofire 기반의 비동기 네트워크 요청을 추상화합니다.
import Foundation
import Alamofire
class NetworkManager {
static func fetchObject<T: Decodable>(from url: String) async -> T {
try! await AF.request(url).serializingDecodable(T.self).value
}
static func fetchList<T: Decodable>(from url: String) async -> [T] {
try! await AF.request(url).serializingDecodable([T].self).value
}
static func sendObject<T: Encodable>(to url: String, method: HTTPMethod, body: T) async -> String {
try! await AF.request(url, method: method, parameters: body, encoder: JSONParameterEncoder.default)
.serializingString().value
}
static func deleteResource(at url: String) async -> String {
try! await AF.request(url, method: .delete).serializingString().value
}
static func fetchRawString(from url: String) async -> String {
try! await AF.request(url).serializingString().value
}
}
데이터 모델 정의
PostModel.swift 파일을 만들어 다음과 같은 구조체를 정의합니다. 이 모델은 게시물 데이터를 표현하며, Codable 프로토콜을 채택해 JSON 직렬화를 지원합니다.
import Foundation
struct PostModel: Codable, CustomStringConvertible {
let userId: Int
let id: Int
let title: String
let body: String
var description: String {
"PostModel {userId=\(userId), id=\(id), title=\"\(title)\", body=\"\(body.replacingOccurrences(of: \"\\n\", with: \"\\\\n\"))\"}"
}
private static let baseURL = "https://jsonplaceholder.typicode.com/posts"
static func fetchSinglePostText() async -> String {
await fetchRawString(from: "\(baseURL)/1")
}
static func fetchSinglePost() async -> PostModel {
await fetchObject(from: "\(baseURL)/1")
}
static func fetchFirstNPosts(count: Int) async -> [PostModel] {
let all = await fetchList(from: baseURL)
return Array(all.prefix(count))
}
static func createNewPost() async -> String {
let newPost = PostModel(userId: 101, id: 0, title: "새 제목", body: "새 내용")
return await sendObject(to: baseURL, method: .post, body: newPost)
}
static func modifyExistingPost() async -> String {
let updated = PostModel(userId: 101, id: 1, title: "수정된 제목", body: "수정된 본문")
return await sendObject(to: "\(baseURL)/1", method: .put, body: updated)
}
static func removePost() async -> String {
await deleteResource(at: "\(baseURL)/1")
}
}
앱 진입점에서 실행
NetworkDemoApp.swift 파일 내의 앱 구조체 초기화 블록에서 비동기 작업을 시작합니다.
import SwiftUI
@main
struct NetworkDemoApp: App {
init() {
Task {
print(await PostModel.fetchSinglePostText())
print(await PostModel.fetchSinglePost())
print(await PostModel.fetchFirstNPosts(count: 2))
print(await PostModel.createNewPost())
print(await PostModel.modifyExistingPost())
print(await PostModel.removePost())
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
예상 출력
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
PostModel {userId=1, id=1, title="sunt aut facere repellat provident occaecati excepturi optio reprehenderit", body="quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"}
[PostModel {userId=1, id=1, title="sunt aut facere repellat provident occaecati excepturi optio reprehenderit", body="quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"}, PostModel {userId=1, id=2, title="qui est esse", body="est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"}]
{
"title": "test title",
"body": "test body",
"userId": 101,
"id": 101
}
{
"title": "test title",
"body": "test body",
"userId": 101,
"id": 1
}
{}