티스토리가 가독성이 안좋아서 노션 링크로 보시는걸 추천 드립니다.
Swift에서 URLSession은 HTTP 통신을 위한 기본 API로, 서버와 데이터를 주고받을 때 사용됩니다. 앱에서 REST API와 통신하거나 파일을 다운로드할 때 자주 사용됩니다.
1. URLSession이란?
URLSession은 Apple에서 제공하는 비동기 네트워킹 API입니다. 주로 다음 용도로 사용됩니다:
- HTTP/HTTPS 요청 및 응답
- 파일 업로드/다운로드
- 백그라운드 전송
디테일하게 설명하면
- Apple에서 제공하는 네트워크 통신 API로, 앱이 서버와 데이터를 주고받을 수 있도록 지원합니다.
- Alamofire, Moya 등 서드파티 라이브러리의 기반이 되는 핵심 API입니다.
- 타임아웃, 캐시 정책, 백그라운드 전송 등 다양한 네트워크 설정을 구성할 수 있습니다.(gwonii.github.io, Velog)
2. 기본 구조
let url = URL(string: "<https://api.example.com/data>")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// 응답 처리
}
task.resume()
전체 플로우
import Foundation
// 1. URL 생성 (옵셔널 바인딩으로 안전하게 처리)
guard let url = URL(string: "<https://api.example.com/data>") else {
print("❌ 유효하지 않은 URL")
return
}
// 2. URLRequest 객체 생성 및 설정
var request = URLRequest(url: url)
request.httpMethod = "GET" // or "POST", "PUT", "DELETE"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// 필요 시 헤더 추가 가능
// request.setValue("Bearer ", forHTTPHeaderField: "Authorization")
// 3. URLSession으로 dataTask 생성
let task = URLSession.shared.dataTask(with: request) { data, response, error in
// 4. 에러 체크
if let error = error {
print("❌ 네트워크 에러: \\(error.localizedDescription)")
return
}
// 5. 응답 상태코드 확인
guard let httpResponse = response as? HTTPURLResponse else {
print("❌ 유효하지 않은 응답 객체")
return
}
print("📡 상태코드: \\(httpResponse.statusCode)")
guard (200...299).contains(httpResponse.statusCode) else {
print("❌ 서버 응답 오류: \\(httpResponse.statusCode)")
return
}
// 6. 데이터 존재 여부 확인 및 JSON 파싱
guard let data = data else {
print("❌ 데이터가 없습니다.")
return
}
do {
// 7. JSON 파싱 (Dictionary 또는 Array 형태)
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
print("✅ 파싱된 JSON: \\(json)")
} else {
print("❌ JSON 형식이 Dictionary가 아닙니다.")
}
} catch {
print("❌ JSON 디코딩 실패: \\(error)")
}
}
// 8. 네트워크 요청 시작
task.resume()
3. 주요 클래스 구성
클래스/타입 설명
| URLSession | 네트워크 작업을 관리하는 객체 |
| URLSessionTask | 네트워크 요청을 나타냄 (dataTask, uploadTask, downloadTask) |
| URLRequest | 요청 정보 설정 (URL, HTTP 메서드, 헤더, 바디 등) |
| URLResponse, HTTPURLResponse | 응답 정보 |
🧩 URLSessionConfiguration 종류 (네트워크 작업 관리 객테의 종류)
URLSession은 네 가지 세션 구성을 제공합니다:
- Shared Session
- 싱글턴 패턴으로 구현되어 간단한 요청에 적합합니다.
- 커스터마이징이 불가능하며, 백그라운드 전송을 지원하지 않습니다.
let url = URL(string: "<https://example.com/data>")! URLSession.shared.dataTask(with: url) { data, response, error in // 응답 처리 }.resume() - Default Session
- Shared Session과 유사하지만, 커스터마이징이 가능합니다.
- Delegate를 통해 데이터를 점진적으로 받아올 수 있습니다.
let url = URL(string: "<https://example.com/data>")! let defaultSession = URLSession(configuration: .default) defaultSession.dataTask(with: url) { data, response, error in // 응답 처리 }.resume() - Ephemeral Session
- 캐시, 쿠키, 인증 정보를 디스크에 저장하지 않아 시크릿 모드와 유사합니다.
let url = URL(string: "<https://example.com/data>")! let ephemeralSession = URLSession(configuration: .ephemeral) ephemeralSession.dataTask(with: url) { data, response, error in // 응답 처리 }.resume() - Background Session
- 앱이 백그라운드 상태이거나 종료되어도 데이터 전송이 가능합니다.
let url = URL(string: "<https://example.com/data>")! let config = URLSessionConfiguration.background(withIdentifier: "com.example.app.background") let backgroundSession = URLSession(configuration: config) backgroundSession.dataTask(with: url) { data, response, error in // 응답 처리 }.resume()
🧵 URLSessionTask 종류 (어떤 요청을 보낼지 Task에 대한 종류)
URLSession은 다양한 작업을 처리하기 위해 네 가지 주요 Task를 제공합니다:
- DataTask
- NSData 객체를 사용하여 데이터를 전송하고 받습니다.
- 짧고 빈번한 요청에 적합하며, 주로 HTTP GET 요청을 처리합니다.
- UploadTask
- 데이터를 서버로 업로드할 때 사용합니다.
- 앱이 실행 중이지 않을 때에도 백그라운드 업로드를 지원합니다.
- 주로 HTTP POST 요청을 처리합니다.
- DownloadTask
- 파일 형태로 데이터를 다운로드합니다.
- 앱이 실행 중이지 않을 때에도 백그라운드 다운로드를 지원합니다.
- 주로 HTTP GET 요청을 처리합니다.
- StreamTask
- TCP/IP 연결을 생성할 때 사용합니다.(202044021 이준혁)
4. 사용 방법
📌 A. GET 요청
let url = URL(string: "<https://jsonplaceholder.typicode.com/posts>")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
let responseString = String(data: data, encoding: .utf8)
print(responseString ?? "No Data")
}
}
task.resume()
📌 B. POST 요청
var request = URLRequest(url: URL(string: "<https://api.example.com/login>")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let parameters = ["email": "test@example.com", "password": "1234"]
request.httpBody = try? JSONSerialization.data(withJSONObject: parameters)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
let result = String(data: data, encoding: .utf8)
print(result ?? "")
}
}
task.resume()
6. 주의할 점
- .resume() 호출 안 하면 요청이 시작되지 않음
- 비동기 코드이므로 UI 업데이트는 DispatchQueue.main.async에서 해야 함
- 서버 응답은 statusCode를 통해 반드시 확인해야 함
- JSON 디코딩은 Codable을 활용하는 것이 일반적
7. JSON 디코딩 예시
struct Post: Codable {
let id: Int
let title: String
}
let url = URL(string: "<https://jsonplaceholder.typicode.com/posts/1>")!
URLSession.shared.dataTask(with: url) { data, _, _ in
if let data = data {
let post = try? JSONDecoder().decode(Post.self, from: data)
print(post?.title ?? "No title")
}
}.resume()
📤 URLRequest 구성
URLRequest를 사용하여 요청의 세부 사항을 설정할 수 있습니다:
let url = URL(string: "<https://example.com/data>")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
📥 URLResponse 처리
응답은 두 가지 방식으로 처리할 수 있습니다:
- Completion Handler
- 요청이 완료되면 한 번 호출됩니다.
let url = URL(string: "<https://example.com/data>")! let defaultSession = URLSession(configuration: .default) defaultSession.dataTask(with: url) { data, response, error in // 응답 처리 }.resume() - URLSessionDelegate
- 데이터 수신, 다운로드 진행 상황, 작업 완료 및 오류 발생 시의 처리를 세밀하게 제어할 수 있습니다.
let url = URL(string: "<https://example.com/data>")! let config = URLSessionConfiguration.default let defaultSession = URLSession(configuration: config, delegate: self, delegateQueue: nil) defaultSession.dataTask(with: url).resume() extension ViewController: URLSessionDataDelegate { func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { // 데이터 수신 처리 } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { // 작업 완료 처리 } }
좀더 상세하게 보자
- 이러한 Delegate 패턴을 활용하면, 대용량 데이터 전송이나 다운로드 진행 상황을 실시간으로 처리하는 데 유용합니다.
class ViewController: UIViewController, URLSessionDataDelegate {
func getData() {
let url = URL(string: "https://example.com/data")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let task = session.dataTask(with: request)
task.resume()
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
print("Received data: \(data)")
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Error: \(error.localizedDescription)")
} else {
print("Data received successfully.")
}
}
}
요게 어떻게 보면 URLSession 을 가장 정석적으로 사용하는 코드라고 볼수 있음
async/await 너 누군데?
async/await는 Swift 5.5부터 도입된 비동기 코드 작성을 쉽게 해주는 문법입니다. 기존의 completion handler 기반 URLSession 코드를 더 간결하고 읽기 쉬운 형태로 바꿔줍니다.
1. async/await란?
- async: 해당 함수가 비동기적으로 실행됨을 나타냄
- await: 비동기 함수를 기다림 (작업이 끝날 때까지 다음 코드 실행을 멈춤)
- 기존 completion handler보다 가독성이 좋고 오류 처리도 간편
2. 기존 방식 vs async/await
📌 기존 방식
URLSession.shared.dataTask(with: url) { data, response, error in
// callback 안에서 처리
}.resume()
📌 async/await 방식
let (data, response) = try await URLSession.shared.data(from: url)
// 이어서 처리
3. URLSession + async/await 기본 예제
func fetchPost() async throws -> Post {
let url = URL(string: "<https://jsonplaceholder.typicode.com/posts/1>")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
let post = try JSONDecoder().decode(Post.self, from: data)
return post
}
호출은 이렇게 합니다:
Task {
do {
let post = try await fetchPost()
print(post.title)
} catch {
print("에러 발생: \\(error)")
}
}
4. POST 요청 예제
func login() async throws -> LoginResponse {
let url = URL(string: "<https://api.example.com/login>")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let parameters = ["email": "test@example.com", "password": "1234"]
request.httpBody = try JSONEncoder().encode(parameters)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
let result = try JSONDecoder().decode(LoginResponse.self, from: data)
return result
}
5. 주의할 점
- async 함수는 await 없이 호출 불가 → Task { } 안에서 호출하거나 다른 async 함수 내에서 사용해야 함
- UI 업데이트는 반드시 MainActor 또는 DispatchQueue.main.async에서 실행해야 함
- try await는 에러 처리가 필요 → do-catch로 감싸야 함
6. SwiftUI에서 사용 예시
@MainActor
func loadData() async {
do {
let post = try await fetchPost()
self.title = post.title
} catch {
self.errorMessage = error.localizedDescription
}
}
.onAppear {
Task {
await loadData()
}
}
다 필요없다 이것만 알고 가자 URLSession, async/await 사용법 정리
URLSession 사용 순서 정리
- URL 생성 (통신할 주소)
- URLRequest 객체 생성 및 설정 (httpMethod, 필요 시 헤더 추가)
- URLSession으로 dataTask 생성 (요청 이후 처리할 일을 정의하는 코드 블럭을 정의 completion handler)
- 내부에서 할일 에러체크, 응답 상태코드 확인, 데이터 응답 객체 JSON 파싱 각 case 별 에러 대응
- task.resume()를 통해 실제 실행 !!!!! 위 1,2,3 과정을 정의 했다면 실행을 시켜야함
import Foundation
// 1. URL 생성 (옵셔널 바인딩으로 안전하게 처리)
guard let url = URL(string: "<https://api.example.com/data>") else {
print("❌ 유효하지 않은 URL")
return
}
// 2. URLRequest 객체 생성 및 설정
var request = URLRequest(url: url)
request.httpMethod = "GET" // or "POST", "PUT", "DELETE"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// 필요 시 헤더 추가 가능
// request.setValue("Bearer ", forHTTPHeaderField: "Authorization")
// 3. URLSession으로 dataTask 생성
let task = URLSession.shared.dataTask(with: request) { data, response, error in
// 4. 에러 체크
if let error = error {
print("❌ 네트워크 에러: \\(error.localizedDescription)")
return
}
// 5. 응답 상태코드 확인
guard let httpResponse = response as? HTTPURLResponse else {
print("❌ 유효하지 않은 응답 객체")
return
}
print("📡 상태코드: \\(httpResponse.statusCode)")
guard (200...299).contains(httpResponse.statusCode) else {
print("❌ 서버 응답 오류: \\(httpResponse.statusCode)")
return
}
// 6. 데이터 존재 여부 확인 및 JSON 파싱
guard let data = data else {
print("❌ 데이터가 없습니다.")
return
}
do {
// 7. JSON 파싱 (Dictionary 또는 Array 형태)
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
print("✅ 파싱된 JSON: \\(json)")
} else {
print("❌ JSON 형식이 Dictionary가 아닙니다.")
}
} catch {
print("❌ JSON 디코딩 실패: \\(error)")
}
}
// 8. 네트워크 요청 시작
task.resume()
async/await 사용 순서 정리
- 비동기 처리 작업이 들어가는 함수에 async 키워드 붙이기 async 함수로 만들어 주는 것 (비동기 작업을 하는 함수라고 정의)
- 반환 타입 = await (비동기 작업 실행) 구조를 꼭 기억하자
- await 왼쪽은 비동기 작업 실행 이후 받을 값들에 대해서 정의를 하고 await 뒤에 비동기 작업을 실행하는 코드를 위치
- async 함수는 await 없이 호출 불가 → Task { } 안에서 호출하거나 다른 async 함수 내에서 사용해야 함
- UI 업데이트는 반드시 MainActor 또는 DispatchQueue.main.async에서 실행해야 함
- try await는 에러 처리가 필요 → do-catch로 감싸야 함
func fetchPost() async throws -> Post {
let url = URL(string: "<https://jsonplaceholder.typicode.com/posts/1>")!
// 반환 타입 = await (비동기 작업 실행) 구조를 꼭 기억하자
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
let post = try JSONDecoder().decode(Post.self, from: data)
return post
}
호출은 이렇게 합니다:
Task {
do {
// 반환 타입 = await (비동기 작업 실행) 구조를 꼭 기억하자
let post = try await fetchPost()
print(post.title)
} catch {
print("에러 발생: \\(error)")
}
}
자자 다 떠먹여줄게
특히 헷갈리기 쉬운 async, await, try await, throws 키워드의 의미와 관계를 중심으로
1. async 키워드
의미:
- 해당 함수가 비동기 함수임을 나타냅니다.
- 즉, 함수 내에서 비동기 작업을 await로 호출할 수 있게 해줍니다.
특징:
- async 함수는 동기 함수에서 직접 호출 불가
- 반드시 아래 중 하나로 감싸야 함:
- Task { ... } 블록
- 다른 async 함수 내부
2. await 키워드
의미:
- 비동기 작업의 결과가 완료될 때까지 기다림을 나타냄
- await은 단독으로 사용할 수 없고, 반드시 async 함수 안에서만 사용 가능
사용 예:
let (data, response) = try await URLSession.shared.data(for: request)
→ data(for:)는 비동기 메서드이기 때문에 await으로 기다려야 함
3. throws 키워드
의미:
- 해당 함수가 에러를 던질 수 있음을 나타냄
- Swift의 오류 처리 방식인 do-try-catch 문법과 함께 사용
특징:
- try 키워드를 통해 오류 가능성이 있는 함수 호출
- 오류가 발생하면 catch로 넘어감
4. try await 키워드
의미:
- await 대상이 에러를 던질 수 있는 비동기 함수일 경우 두 키워드를 함께 사용
- await만 써선 부족하고, 반드시 try까지 붙여야 함
정리:
상황 키워드
| 동기 + 오류 발생 가능 | try |
| 비동기 + 오류 발생 불가 | await |
| 비동기 + 오류 발생 가능 | try await |
5. 전체 흐름 구조
🔸 함수 정의:
func fetchPost() async throws -> Post
요소 의미
| async | 비동기 작업 가능 |
| throws | 에러 던질 수 있음 |
| -> Post | 최종 결과로 Post 타입을 반환 |
→ 이 함수는 비동기 작업을 기다리고, 에러를 던질 수 있고, Post 타입 결과를 돌려줌
🔸 함수 내부:
let (data, response) = try await URLSession.shared.data(for: request)
- URLSession.shared.data(for:): 비동기 함수 + throws
- 따라서 → try await 필요
🔸 함수 호출:
Task {
do {
let post = try await fetchPost()
} catch {
print("에러 발생: \\(error)")
}
}
- fetchPost()는 async throws 함수
- 따라서 호출은 반드시 try await로, 그리고 do-catch로 감싸야 함
Task { }는 Swift에서 비동기 작업을 시작할 수 있는 스레드-safe한 방법
async 함수는 일반 함수에서 직접 호출할 수 없기 때문에, 비동기 함수 호출을 감싸는 컨테이너의 역할을 담당
핵심 포인트
상황 써야 할 키워드 예시
| 비동기 함수 정의 | async | func foo() async {} |
| 에러 던질 수 있음 | throws | func foo() throws {} |
| 에러 + 비동기 | async throws | func foo() async throws {} |
| 에러나는 동기 호출 | try | try JSONDecoder().decode(...) |
| 에러 안 나는 비동기 호출 | await | await delay(1) |
| 에러 나는 비동기 호출 | try await | try await URLSession.shared.data(for: request) |
요약
- async는 "이 함수는 기다려야 해!"
- await는 "이 작업이 끝날 때까지 기다릴게!"
- throws는 "이 함수는 실패할 수 있어!"
- try는 "실패할 수 있으니 조심해서 실행해!"
- try await는 "실패할 수 있는 비동기 작업을 기다릴게!"
SOPT 4주차 실습 코드로 한번 봐보자
우리 실습 코드는 URLSession, async/await 구조가 섞여 있는 구조임
//
// RegisterService.swift
// Week_04
//
// Created by 정정욱 on 5/3/25.
//
import Foundation
// MARK: - RegisterService (Singleton 패턴)
class RegisterService {
static let shared = RegisterService() // 싱글톤 인스턴스
private init() {} // 외부에서 인스턴스 생성 방지
//MARK: 회원가입 요청 바디 생성 함수 - JSON 데이터 생성
// URL Session에 넣어줄 URL 요청 객체만을 만드는 부분
func makeRequestBody(loginId: String, password: String, nickName: String) -> Data? {
do {
let data = RegisterRequest(
loginId: loginId,
password: password,
nickname: nickName
)
let jsonEncoder = JSONEncoder()
let requestBody = try jsonEncoder.encode(data)
return requestBody
} catch {
print("💥 Encoding error: \\(error)")
return nil
}
}
// MARK: URLRequest 생성 함수 - URL, 메서드, 헤더, 바디
// URLRequest 객체 생성 및 설정 (httpMethod, 필요 시 헤더 추가)
func makeRequest(body: Data?) -> URLRequest {
let url = URL(string: "http://......")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
let header = ["Content-Type": "application/json"]
header.forEach {
request.addValue($0.value, forHTTPHeaderField: $0.key)
}
if let body = body {
request.httpBody = body
}
// 이후에 아까 받아온 Body를 넣어줌
return request
}
// MARK: 회원가입 요청 함수 - 서버 통신
// - try await을 사용함
func PostRegisterData(loginId: String, password: String, nickName: String) async throws -> RegisterUserInfo {
// makeRequestBody 함수를 이용해서, 리퀘스트 바디를 만들어줍니다! 실패할 경우, 아까 NetworkError에서 선언한 오류들을 던지게 됩니다
guard let body = makeRequestBody(
loginId: loginId,
password: password,
nickName: nickName
) else {
throw NetworkError.requestEncodingError
}
let request = self.makeRequest(body: body)
let (data, response) = try await URLSession.shared.data(for: request)
dump(request)
// 응답 유효성 검사
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.responseError
}
dump(response)
guard (200...299).contains(httpResponse.statusCode) else {
throw configureHTTPError(errorCode: httpResponse.statusCode)
}
// 문제 없으면 디코딩 수행
do {
let decoded = try JSONDecoder().decode(RegisterResponse.self, from: data)
return decoded.data // 결국 정상 반환시 리턴 값
} catch {
print("디코딩 실패:", error)
throw NetworkError.responseError
}
}
private func configureHTTPError(errorCode: Int) -> Error {
return NetworkError(rawValue: errorCode)
?? NetworkError.unknownError
}
}
RegisterService는 URLSession을 async/await 방식으로 사용하는 구조라서 dataTask(with:) { ... }.resume() 방식의 task 객체가 코드에 드러나지 않음
전통적인 URLSession 코드:
let task = URLSession.shared.dataTask(with: request) { data, response, error in
...
}
task.resume()
하지만 지금 사용하신 구조는 Swift의 async/await 기반의 URLSession.shared.data(for:) 를 사용한 방식
let (data, response) = try await URLSession.shared.data(for: request)
여기서 중요한 건:
data(for:)는 내부적으로 URLSessionDataTask를 생성하고 resume()까지 자동으로 처리해줍니다.
즉, 평소에 보던 task는 암묵적으로 생성되고 실행되기 때문에 코드에서 task 객체가 직접적으로 보이지 않음
RegisterService 흐름 정리
아래는 RegisterService의 네트워크 흐름을 요약한 순서입니다:
- makeRequestBody(...)
- Encodable 모델을 JSON 형식으로 Data로 변환
- makeRequest(...)
- POST, Header 설정, body 삽입
- URLSession.shared.data(for: request)
- 네트워크 요청 비동기 실행
- 내부에서 DataTask가 자동 생성 및 실행
- response에서 status code 검사
- 성공 시 JSON → RegisterResponse로 디코딩
참고로 내부에서 무슨 일이 일어나는가?
let (data, response) = try await URLSession.shared.data(for: request)
이 한 줄은 다음을 포함합니다:
- URLSessionDataTask 생성
- resume() 호출
- 응답이 올 때까지 suspension
- 응답 오면 data, response 리턴 or throw
참고
https://1000one.tistory.com/58
나는 이번 API 통신 과제를 어떻게 했는가
관련 PR https://github.com/AT-SOPT-iOS/Jeonguk_practice/pull/2
Feat/#1 network learning by jeonguk29 · Pull Request #2 · AT-SOPT-iOS/Jeonguk_practice
🎫 What is the PR? 세미나때 배운 네트워크 통신 내용을 기반으로 API 네트워크 레이어 추상화 구조 구현 🎫 Changes API 네트워크 레이어 추상화 구조 구현 로그인, 회원가입 API 연결 로그인 id KeychainM
github.com


해당 부분을 좀 살펴보자
이번에 HTTP 통신을 진행하기 위해 네트워크 관련 작업을 기능별, 책임별로 나눴고 추상화를 통해 네트워크 통신을 하도록 만들었음 URLSession 만을 사용하라는 요구사항이 있었는데 사실 해당 구조는 얼핏 본다면 Moya 라이브러리의 구조와 비슷하다고 볼 수도 있음
"역할별로 나눴다"는 건 무슨 말?
네트워크 관련 작업을 기능별, 책임별로 나눠서 관리한다는 뜻
즉, 하나의 파일에 모든 걸 몰아넣는 게 아니라, 누가 뭘 할지 명확하게 나눴음
역할 파일 하는 일
| 어떤 API를 요청할지 정리 | Endpoint.swift | URL 경로와 메서드 정의 |
| HTTP 메서드 정의 | HTTPMethod.swift | GET, POST 등을 enum으로 표현 |
| 실제 요청 실행 | APIService.swift | URLSession 사용해서 요청 보내기 |
| 로그인/회원가입 등 호출 | AuthService.swift | 위 기능들을 모아서 비즈니스로직 제공 |
👉 이게 바로 "역할별 분리"
하나하나 파일을 살펴보자
일단 네트워크 에러 정의 코드
enum NetworkError: Int, Error, CustomStringConvertible {
var description: String { self.errorDescription }
case requestEncodingError
case responseDecodingError = 1
case responseError
case unknownError
case loginFailed = 400
case internalServerError = 500
case notFoundError = 404
case invalidURL
var errorDescription: String {
switch self {
case .loginFailed: return "로그인에 실패하였습니다."
case .requestEncodingError: return "REQUEST_ENCODING_ERROR"
case .responseError: return "RESPONSE_ERROR"
case .responseDecodingError: return "RESPONSE_DECODING_ERROR"
case .unknownError: return "UNKNOWN_ERROR"
case .internalServerError: return "500:INTERNAL_SERVER_ERROR"
case .notFoundError: return "404:NOT_FOUND_ERROR"
case .invalidURL: return "잘못된 URL 입니다."
}
}
}
- 설명
전체 구조이 한 줄에 들어있는 뜻키워드 의미enum 열거형: 정해진 케이스 집합을 나타냄 : Int 각 케이스에 숫자(Int) 원시값을 부여할 수 있게 함 Error Swift의 표준 에러 처리용 프로토콜 채택 (try/catch용) CustomStringConvertible description 문자열을 커스터마이징할 수 있게 함
열거형 케이스 정의- case requestEncodingError → rawValue가 0 (기본값)
- case responseDecodingError = 1 → 명시적으로 1
- 이후는 자동 증가됨 (단, 400, 404, 500처럼 직접 지정한 케이스도 있음)
Error 프로토콜이 뭐야?- Swift의 try, catch, throw 구문에서 사용할 수 있는 "에러 타입"으로 만들겠다는 뜻입니다.
- 즉, 아래처럼 사용할 수 있게 됨:
throw NetworkError.loginFailed
CustomStringConvertible은 뭐야?- print(error) 또는 print("\\(error)")처럼 출력할 때 자동으로 description을 사용하게 해주는 프로토콜이에요.
- description은 String 타입으로 반환되어야 함
- 여기서 description은 self.errorDescription을 그대로 가져다 씀
그럼 self.errorDescription은 뭐야?- 이건 계산 속성(computed property) 이라고 해요.
- description은 실제로는 errorDescription이라는 다른 커스텀 프로퍼티를 간접 참조하고 있는 셈이에요.
열거형 안에 변수/함수를 넣을 수 있나?→ enum은 거의 struct와 비슷하게 행동할 수 있는 강력한 타입이에요.
목적:- 서버 상태 코드나 앱 내부 네트워크 오류를 하나의 타입(NetworkError)로 표현
- throw와 catch에서 오류를 처리할 수 있도록 Error 채택
- 로그에 출력하거나 사용자에게 보여줄 메시지를 깔끔하게 만들기 위해 description 정의
- Int 원시값을 주어서 상태코드 대응도 쉽게 함
정리 요약
요소 설명
| enum NetworkError: Int, Error | 에러 처리용 열거형 (상태코드 대응) |
| CustomStringConvertible | 출력 시 설명 텍스트 제공 |
| description: String | 실제로는 errorDescription을 가져옴 |
| var errorDescription: String | 각 케이스에 대한 사용자 친화 메시지 정의 |
| rawValue | HTTP 상태코드와 연동 가능 (예: 400, 404, 500 등) |
HTTPMethod 정의 코드
enum HTTPMethod: String {
case GET, POST, PATCH, PUT, DELETE
}
열거형을 통해 앱에서 사용할 API의 경로(path)와 HTTP 메서드(method)를
정리하고 구조화한 코드, 서버와 통신할 때 필요한 “주소”와 “방식”을 정리해놓은 API 전화번호 부 역할
//
// Endpoint.swift
// Week_04
//
// Created by 정정욱 on 5/6/25.
//
import Foundation
enum Endpoint {
enum Auth {
case signup
case signin
var path: String {
switch self {
case .signup: return "/api/v1/auth/signup"
case .signin: return "/api/v1/auth/signin"
}
}
var method: HTTPMethod {
switch self {
case .signup, .signin: return .POST
}
}
}
enum User {
case me
case all(keyword: String?)
case update
var path: String {
switch self {
case .me: return "/api/v1/users/me"
case .all(let keyword):
if let keyword = keyword, !keyword.isEmpty {
// 인코딩시 한글 깨짐 방지
return "/api/v1/users?keyword=\\(keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")"
} else {
return "/api/v1/users"
}
case .update: return "/api/v1/users"
}
}
var method: HTTPMethod {
switch self {
case .me, .all: return .GET
case .update: return .PATCH
}
}
}
}
이 APIService 클래스는 앱의 모든 네트워크 요청을 담당하는 중앙 통신 관리자 역할
즉, 모든 API 요청을 하나의 함수로 통합하고, 재사용성을 높임
//
// APIService.swift
// Week_04
//
// Created by 정정욱 on 5/6/25.
//
import Foundation
final class APIService {
static let shared = APIService()
private init() {}
private let baseURL = "<http://api.atsopt-4.site>"
func request(
path: String,
method: HTTPMethod,
headers: [String : String]? = nil,
body: Data? = nil,
responseType: T.Type
) async throws -> T {
guard let url = URL(string: baseURL + path) else {
throw NetworkError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let headers {
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
}
request.httpBody = body
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.responseError
}
guard (200...299).contains(httpResponse.statusCode) else {
throw configureHTTPError(errorCode: httpResponse.statusCode)
}
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw NetworkError.responseDecodingError
}
}
private func configureHTTPError(errorCode: Int) -> Error {
return NetworkError(rawValue: errorCode)
?? NetworkError.unknownError
}
}
실제 사용할때 AuthService처럼 사용
전체 역할 요약
final class APIService
- final: 상속 금지 (다른 클래스가 이 클래스를 상속하지 못하도록)
- shared: 싱글톤(Singleton) 패턴 — 앱 전역에서 하나의 인스턴스를 공유
- 핵심 메서드: request(...)
결과적으로 이 클래스는 “모든 HTTP 요청을 수행하고 응답을 디코딩해서 반환해주는 역할”을 해요.
🔹 1. 싱글톤 정의
static let shared = APIService()
private init() {}
- 외부에서 new APIService()처럼 새로 만들지 못하게 private init()
- 오직 APIService.shared로만 접근 가능하게 함
🔹 2. 기본 URL 설정
private let baseURL = "<http://apsite>"
- 모든 API 경로에 앞에 붙는 공통 서버 주소
- 실제 요청 시 baseURL + path로 완성된 URL 생성
🔹 3. 핵심 메서드: request(...)
func request<T: Decodable>(
path: String,
method: HTTPMethod,
headers: [String : String]? = nil,
body: Data? = nil,
responseType: T.Type
) async throws -> T
매개변수 설명:
매개변수 의미
| path | Endpoint에서 넘겨준 경로 (예: "/api/v1/auth/signup") |
| method | GET, POST, PATCH 등 (HTTPMethod enum) |
| headers | 추가로 붙일 HTTP 헤더 |
| body | HTTP 요청 body (JSON 등) |
| responseType | 디코딩할 모델 타입 (예: RegisterResponse.self) |
🔹 4. URL 생성 및 유효성 검사
guard let url = URL(string: baseURL + path) else {
throw NetworkError.invalidURL
}
- 문자열을 URL 객체로 만들 수 없는 경우 → 오류 던짐 (invalidURL)
🔹 5. URLRequest 설정
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- HTTP 메서드 설정 ("GET", "POST" 등)
- 기본 Content-Type 헤더 설정
🔹 6. 추가 헤더 처리
if let headers {
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
}
- Authorization, Accept-Language 등 다른 헤더를 동적으로 추가할 수 있도록 확장성 제공
🔹 7. HTTP Body 설정
request.httpBody = body
- 보통 POST나 PATCH 같은 요청은 서버에 데이터를 보내야 하므로 body 필요
- 미리 JSONEncoder().encode(...) 해서 넘겨줘야 함
- 요청 객체 즉 Request 객체를 외부에서 받아 삽입
🔹 8. URLSession 비동기 요청 실행
let (data, response) = try await URLSession.shared.data(for: request)
- 실제 HTTP 통신 발생
- async/await를 사용하여 비동기 응답 대기
- 오류 발생 시 바로 throw
🔹 9. 응답 검증
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.responseError
}
guard (200...299).contains(httpResponse.statusCode) else {
throw configureHTTPError(errorCode: httpResponse.statusCode)
}
- response가 HTTP 형식이 아닐 경우 오류
- 상태 코드가 2xx 범위를 벗어나면 NetworkError로 변환 후 throw
🔹 10. 디코딩 처리
return try JSONDecoder().decode(T.self, from: data)
- 서버로부터 받은 JSON 데이터를 우리가 원하는 모델 타입 T로 디코딩
- 실패 시 .responseDecodingError 발생
🔹 11. 상태 코드에 따른 오류 매핑
private func configureHTTPError(errorCode: Int) -> Error {
return NetworkError(rawValue: errorCode)
?? NetworkError.unknownError
}
- 예: 404 → .notFoundError, 500 → .internalServerError
- 정의되지 않은 오류는 .unknownError로 처리
사용 예시
let result = try await APIService.shared.request(
path: Endpoint.Auth.signup.path,
method: Endpoint.Auth.signup.method,
body: encodedBody,
responseType: RegisterResponse.self
)
→ 서버에서 받은 응답이 RegisterResponse 타입으로 자동 변환되어 반환됨
실제 사용할때 AuthService처럼 사용
//
// AuthService.swift
// Week_04
//
// Created by 정정욱 on 5/6/25.
//
import Foundation
final class AuthService {
static let shared = AuthService()
func signup(
loginId: String,
password: String,
nickName: String
) async throws -> RegisterUserInfo {
// 1. Request body 모델 생성 및 인코딩
let requestModel = RegisterRequest(
loginId: loginId,
password: password,
nickname: nickName
)
let encodedBody = try JSONEncoder().encode(requestModel)
// 2. APIService 통해 요청
return try await APIService.shared.request(
path: Endpoint.Auth.signup.path,
method: Endpoint.Auth.signup.method,
body: encodedBody,
responseType: RegisterResponse.self
).data // 라서 반환 타입이 RegisterResponseBody임
}
func signin(
loginId: String,
password: String
) async throws -> LoginUserID {
let requestModel = LoginRequest(
loginId: loginId,
password: password
)
let encodedBody = try JSONEncoder().encode(requestModel)
return try await APIService.shared.request(
path: Endpoint.Auth.signin.path,
method: Endpoint.Auth.signin.method,
body: encodedBody,
responseType: LoginResponse.self
).data
}
}
는 실제 앱에서 사용자가 회원가입(signup) 또는 로그인(signin)
요청을 보낼 때 호출되는 비즈니스 로직 중심의 서비스 클래스
API를 직접 호출하는 게 아니라 그 과정을 깔끔하게 감싸주는 역할
View에서 이렇게 사용
@objc private func loginButtonTap() {
Task {
do {
let response = try await AuthService.shared.signin(loginId: self.loginId,
password: self.password)
// Keychain 자체는 Int를 직접 저장할 수 없고, Data 또는 String 형식만 저장 가능
let userId = response.userID
let userIdString = String(userId)
let saved = KeychainManager.shared.save(key: "userId", value: userIdString)
print("Keychain 저장 성공 여부: \\(saved)")
let alert = UIAlertController(
title: "로그인 성공",
message: "토큰 대용 ID로 로그인 성공 (ID: \\(userIdString))",
preferredStyle: .alert
)
let okAction = UIAlertAction(title: "확인", style: .default)
alert.addAction(okAction)
self.present(alert, animated: true)
} catch {
let alert = UIAlertController(
title: "로그인 실패",
message: error.localizedDescription,
preferredStyle: .alert
)
let okAction = UIAlertAction(title: "확인", style: .default)
alert.addAction(okAction)
self.present(alert, animated: true)
print("로그인 에러:", error)
}
}
}
3. "추상화했다"는 건 무슨 말?
복잡한 것을 단순하게 겉으로만 보이게 감싼다는 뜻이에요.
예를 들어, 사용자는 AuthService.shared.signup(...) 이렇게만 쓰면 되지만,
내부에서는:
- URL 경로를 가져오고 (Endpoint)
- HTTPMethod를 설정하고 (POST)
- JSON으로 인코딩하고
- URLRequest를 만들고
- URLSession으로 요청하고
- 응답을 디코딩하고
- 에러를 구분해서 처리
이 많은 과정이 들어가요.
하지만 개발자는 몰라도 돼요.
왜냐하면 APIService가 이걸 "추상화"해서 감춰줬기 때문이에요.
4. "클린 아키텍처 기반이다"란?
클린 아키텍처(Clean Architecture)의 핵심은 "관심사의 분리"
즉, 한 파일/모듈이 하나의 책임만 가지게 만드는 구조죠.
원칙 네트워크 구조 예시
| 하나의 컴포넌트는 하나의 책임만 | APIService: 요청 전송, Endpoint: URL 정의 |
| 비즈니스 로직은 네트워크와 분리 | AuthService: 비즈니스 로직 (URLSession 직접 몰라도 됨) |
| 테스트 가능성 향상 | 각 레이어가 분리돼 있어 모킹 가능 |
비유로 쉽게 설명해볼게요
💡 편의점에서 커피 주문하기
역할 사람/행동 코드에서 대응하는 것
| 손님 | "커피 하나 주세요" | AuthService.signup() 호출 |
| 점원 | 커피 주문 받아서 기계 작동 | APIService |
| 기계 | 물, 커피, 컵을 조합해서 커피 제조 | URLSession, JSON 인코딩, 디코딩 |
| 메뉴판 | 어떤 커피가 있는지 정리 | Endpoint, HTTPMethod |
👉 손님은 "커피 주세요"만 하면 되죠?
내부에서는 복잡한 과정이 일어나지만, 깔끔하게 감춰져 있어요.
이게 바로 추상화고, 역할 분리, 그리고 클린한 구조입니다.
요약
개념 의미 적용된 코드
| 네트워크 레이어 | 서버와 통신하는 부분 | APIService, AuthService 등 |
| 역할 분리 | 각 기능을 나눠서 설계 | Endpoint, HTTPMethod, APIService, AuthService |
| 추상화 | 복잡한 내부 로직을 감추고 단순하게 사용 | AuthService.signup(...) |
| 클린 아키텍처 | 관심사 분리 + 재사용성 + 테스트 용이성 | 전체 구조가 이 철학에 기반 |