Swift 동시성과 Alamofire를 활용한 REST API 통신 구현

테스트용 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
}
{}

태그: Swift Alamofire REST API 동시성 비동기

6월 10일 17:28에 게시됨