오늘은 iOS에서 네트워크 호출 (Network Calls) 하기에 대해서 간단한 예제 코드와 함께 공부했다.
네트워크 호출을 해야하는 이유는
iOS 앱에서 사용자에게 데이터를 보여주기 위해서는 서버에 있는 데이터를 가져와서
보기 좋게 유저에게 보여주어야 하기 때문이다.
☁️ (서버) --- 💾 (데이터) --> 📱(아이폰)
바로 여기서 서버에서 데이터를 가져오기에서 필요한 것이
네트워크 호출이다.
우선, 그럼 그 데이터가 어떤 것인지 잠시 설명하자면,
주로 요즘 우리가 서버에서 받아오는 데이터의 형태는
JSON (Java Script Object Notation?) 이다.
JSON은 이렇게 key와 value로 이루어진 데이터의 형식이다.
대부분 문자열 형태이기 때문에 ""로 감싸져있다.
데이터가 예를 들어 사람 하나의 정보뿐이라면 {}로 감싸진 하나의 형태만 오겠지만,
여러 사람들의 정보를 한꺼번에 받을 수도 있다.
이럴 때는 위 그림의 초록색으로 표시된 Array 처럼 [] 로 감싸진 형태로 전달된다.
그럼 이제 이 데이터를 어떻게 서버에서 받아와서 아이폰에 보여줄건지
4단계로 코드랑 함께 살펴보자.
1. 더미데이터를 보여주는 UI 만들기
우선 서버에서 데이터를 받아오기 전에
내가 임의로 만든 데이터를 보여주는 화면 하나를 만들어주자.
VStack (spacing: 10) {
// profile
Image("unknown")
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.frame(width: 120, height: 120)
Text("Login placeholder")
.bold()
Text("bio placeholder")
.foregroundStyle(.gray)
}
.padding(20)
이번엔 간단히 GitHub Open API를 써서
프로필 사진, 이름, bio 를 가져와서 보여주는 걸 만들거라
위와 같이 화면을 만들어줬다.
보이는 것 처럼 받아온 데이터가 아닌 임시이미지, 임시이름, 임시 bio를 넣어준 모습
2. Model 만들기
여기서 Model은 서버에서 받아온 데이터를
iOS에서 다루기 쉽도록 만든 객체이다.
즉 우리는 이제 서버에서 받아온 데이터를 우리가 만든 Model로 만들거다.
(둘은 일대일 대응)
struct GitHubUser: Codable {
// property name == json key
let login: String
let avatarUrl: String // .convertFromSnackCase
let bio: String
}
앞서 설명했듯이 우리는 login(사용자 이름), avatarUrl(프로필 사진 url), bio(프로필 텍스트)만 받아와서 쓸 것이기 때문에
서버에서 받아온 json 데이터를 이 GitHubUser 객체로 만들어 줄 것임.
그러기 위해서는 우선 GitHubUser 구조체가 Codable 프로토콜을 준수해야한다.
그래야 swift가 json데이터를 GitHubUser로 매핑시킬 수 있구나 하고 매핑시켜줌
그리고 그 반대의 경우도 가능하게 됨, 이 구조체를 json으로 만들어주는 것도 해줄 수 있게 된다.
우리가 일일이 json을 읽어서 githubuser 객체로 만들지 않아도 되기 때문에 간편하지만,
어떤 key-value를 githubuser의 어떤 프로퍼티에 대응시켜야 하는지 알려줘야한다.
프로퍼티 이름과 json 데이터의 key 이름이 일치하게끔 해주면 되는데
간혹 이런 경우가 있다.
json에서는 avatar_url (snake case) 인데 GitHubUser에서는 avatarUrl (camel case) 인 경우다.
그럼 json에 맞춰서 GitHubUser도 avatar_url로 맞추면 안되냐? 싶긴한데 그래도 되지만
swift convention에 맞지 않다..
Sean Allen 왈, 그러면 swift 개발자는 "뭐야 이 snake case는?" 라고 할것이라고..ㅋㅋ
그래서 swift가 만들어둔 api가 있다. 바로 .convertFromSnakeCase
그래서 json 데이터를 해독(decode)할 때 이 api를 써주면 둘 간의 호환이 된다.
3. 네트워크 코드 작성하기
데이터를 받을 준비가 끝났다면 이제 데이터를 받아와야 한다.
// async : 우리가 이 함수를 비동기로 실행할거라
// throws : 굳이 명시하진 않아도 되지만 network 코드는 인터넷이 안된다던가 하는 수많은 에러가 발생할 가능성이 높음
func getUser() async throws -> GitHubUser {
let endpoint = "https://api.github.com/users/"
guard let url = URL(string: endpoint) else { throw GitHubError.invalidURL
} // url을 optional로 만들거기 때문에 guard let 사용
// 비동기 처리가 예상되는 곳에 await 키워드 붙이기
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
throw GitHubError.invalidResponse
}
do {
let decoder = JSONDecoder() // JSON to Object
decoder.keyDecodingStrategy = .convertFromSnakeCase // swift 는 camelcase 쓰니까 json의 snack case를 camelcase로 변환해주는 장치
return try decoder.decode(GitHubUser.self, from: data)
} catch {
throw GitHubError.invalidData
}
}
복잡하지만 깃허브 사용자 정보를 받아오는 API를 호출해서 GitHubUser에 담아주는 일을 수행하는 함수이다.
네트워크 호출은 시간이 오래걸릴 수 있기 때문에, 이 과정이 사용자 모르게 스무스 하게 흘러가는 것처럼 보이게 하기 위해서
함수명 옆에 async 키워드를 붙여서 이 함수가 비동기 처리를 포함하는 함수임을 명시해준다.
그리고 throws 표시는 네트워크 호출은 수많은 오류를 발생시킬 수 있다. 따라서 에러를 던질 수 있는 함수임을 명시해주는 것.
3.1. URL 만들기
우선 어떤 URL을 통해서 데이터를 받아올 것인지 알려줘야 한다.
그 부분이 endpoint에 url 문자열을 담고, 그걸 URL 객체로 만드는 부분이다.
만약 url이 잘못돼서 URL객체로 만들지 못했다면 그걸 의미하는 Error 타입을 던져줘야한다.
GitHubError라는 enum 타입을 만들어서 사용했는데, 그래야 어떤 에러인지 알 수 있어서
나중에 에러가 발생했을 때 해결하기 쉽다.
enum GitHubError: Error {
case invalidURL
case invalidResponse
case invalidData
}
3.2. URL 호출하기
만든 URL 객체를 이제 호출해야하는데 이 역할을 apple에서 만들어준 URLSession 객체를 통해서 한다.
URLSession의 data 함수에 url 객체를 전달해주면 api를 호출해서 data와 response를 준다.
이때 data와 response가 잘 오면 좋지만 만약에 성공했다는 200 상태코드가 아니라 404라던가 500이라던가의 에러코드를 반환하고,
데이터를 받아오지 못했다면 그 다음 단계를 진행할 수 없다.
따라서 그런 경우, 그에 맞는 에러코드를 또 만들어서 던져주고 함수를 끝낸다.
3.3. JSON데이터 GitHubUser로 만들기 (디코딩)
위에서 오류없이 API호출 후 데이터를 잘 받아왔다면
이제 그 JSON 데이터를 우리가 아는 객체인 GitHubUser로 만들어서
써주는 일만 남았다.
우선 JSON -> GitHubUser를 담당해줄 JSONDecoder()를 만들고
앞서 말했듯 json의 snake 케이스를 camel 케이스로 호환해서 swift 객체로 만들어주도록
key decoding strategy를 convert from snake case로 설정해준다.
그 다음 (json) data를 GitHubUser로 decode해서 반환해주면 끝
여기까지가 getUser() 함수의 내용이다.
4. 연결하기 (뷰에 데이터 노출시키기)
이제 GitHubUser 객체를 뷰에 적절히 나타내는 일만 남았다.
아까 1에서 dummy data를 보여줬던 것을 GitHubUser를 넣어주면 된다.
VStack (spacing: 10) {
// profile
AsyncImage(url: URL(string: user?.avatarUrl ?? "")) { image in
image.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
} placeholder: {
Image("unknown")
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
}.frame(width: 120, height: 120)
Text(user?.login ?? "Login placeholder")
.bold()
Text(user?.bio ?? "bio placeholder")
.foregroundStyle(.gray)
}
.padding(20)
.task {
do {
user = try await APIServices().getUser() // async 함수 호출할 때 await 사용
} catch GitHubError.invalidURL {
print("invalid URL")
} catch GitHubError.invalidResponse {
print("invalid response")
} catch GitHubError.invalidData {
print("invalid data")
} catch {
print("unexpected error")
}
}
뷰에서 사용자 정보를 저장하고 있는 프로퍼티 user를 만들고 거기에 getUser()를 호출해서 받은 값을 넣어준다.
실제로 getUser()를 호출하는 부분이다.
나는 APIServices라는 파일을 따로 만들어서 getUser() 함수를 그 안에 선언해줬다.
호출할 때는 이 함수는 async 즉 비동기 함수이기 때문에 앞에 await를 표시해주고,
그 앞의 try는 이 함수가 에러를 throw할 수 있기 때문에 이 함수로부터 던져져나온 에러들을 처리할 수 있도록
do catch 문으로 작성해주었다.
그러면 do 안에 있는 코드를 실행하고, 만약 에러가 발생하면
각 에러 값들에 대해서 하나하나 처리해줄 수 있다.
그리고 SwiftUI에서는 비동기적인 작업을 호출할 때
.task { } 안에 넣어서 호출한다.
.task : Adds an asynchronous task to perform before this view appears.
아주 간단한 예제로 알아본 네트워크 호출
부분을 정리해봤다.
URLSession을 사용해서 API를 호출하고,
JSON decoding하고, 비동기 처리는 async, await를 사용해서 했는데
간단하게 사용할 수 있는 API가 많아져서 더 편리해진 것 같다.
잘 활용해서 깔끔한 코드, 유지보수하기 좋은 코드로
프로젝트에 적용해봐야겠다
아 이 블로그 내용은 모두
https://www.youtube.com/watch?v=XTAziR-tY-A&t=6656s
Sean Allen의 유튜브를 보고 따라하고 정리한 것
'iOS' 카테고리의 다른 글
[iOS] View Life Cycle (UIKit) (0) | 2025.03.28 |
---|---|
[iOS] Delegate & Protocol (0) | 2025.03.26 |
[iOS+@] Concurrency (동시성) (0) | 2025.03.25 |
[iOS+@] Dependency Injection (의존성 주입) (0) | 2025.03.24 |
[Swift] 순환참조와 strong, weak, unowned (0) | 2025.03.14 |