[ swift ] 날씨앱 api

sonny·2024년 12월 6일
4

TIL

목록 보기
63/133

날씨앱을 만들어보며 api에 대한 이해를 높여가는 시간을 가졌다.

우선 ViewController에 기본이 되는 라벨들을 배치해주고 스냅킷을 이용해 오토레이아웃까지 잡아준다.

오토레이아웃까지 야무지게 잡은 모습.

그리고 강의를 들을 때마다 느끼지만 configureUI 만들 때마다 forEach문으로 addSubview 처리하는게 가독성도 좋고 너무 편하다.

그리고 스택뷰를 이용했다면 스택뷰 안에 들어간 레이블들은 따로 빼서 view.addSubview 가 아닌

StackView.addArrangedSubview 로 해주어야한다.

  • addSubview : 단순히 뷰 계층에 서브뷰를 추가하지만, 자동 배치와 관련된 작업은 하지 않는다. 따라서 제약 조건을 수동으로 설정해야 한다.
  • addArrangedSubview : 스택 뷰의 배열된 서브뷰 목록에 포함되며, 레이아웃이 자동으로 설정된다.

서버 데이터 가져오기

이 코드는 네트워크 요청과 JSON 데이터를 처리하는데 필수적인 함수다.

제너릭과 Decodable을 활용해 확장성과 재사용성을 높이면서 iOS 개발에서 API를 호출할 때 자주 사용된다.

"제너릭 JSON 네트워크 요청 함수"로 부르면 될 듯 하다.

주요 역할

  1. 네트워크 요청

    • URLSession을 이용해 특정 URL로 요청을 보낸다.
    • .dataTask를 사용해 비동기적으로 데이터를 가져온다.
  2. 응답 처리

    • HTTP 응답의 상태 코드를 확인한다(성공 범위인 200~299 내에 있는지).
  3. 데이터 디코딩

    • 서버에서 가져온 JSON 데이터를 Decodable 프로토콜을 준수하는 타입으로 변환한다.
  4. 비동기 콜백 제공

    • 결과를 completion 클로저를 통해 호출한 곳으로 전달한다. 성공 시 디코딩된 데이터를 반환하고, 실패 시 nil을 반환한다.

왜 제너릭<T: Decodable>을 사용하는가?

  • 제너릭을 사용해 어떤 데이터 타입이든 디코딩할 수 있도록 했다.
  • TDecodable을 준수하도록 제한하여, JSON 디코딩이 가능한 타입만 사용할 수 있게 했다.

현재 날씨 결과 가져오기

새로 만든 CurrentWeatherResult.swift 파일에 현재 날씨 결과를 가져오는 코드를 짜야하는데,

이 때 Codable 이라는 프로토콜을 사용해야한다.

Codable?

Codable은 위에서 본 것처럼 두 개의 프로토콜인 EncodableDecodable의 조합이다.

  • Decodable : JSON이나 다른 데이터 형식에서 Swift 객체로 변환할 때 사용된다(디코딩).
  • Encodable : Swift 객체를 JSON이나 다른 데이터 형식으로 변환할 때 사용된다(인코딩).

CodableJSON 데이터와 Swift 객체 간의 변환을 매우 간단하고 안전하게 만들어주고,

Swift 기반의 REST API 호출 작업에서 매우 중요한 도구인데, 데이터를 다룰 때 가장 많이 쓰이는 프로토콜이다.

구조체와 함께 사용할 때 가장 효과적이라고 하고, JSONDecoderJSONEncoder와 함께 작동한다.

자세한 내용은 디코딩과 인코딩 포스트에 있으니 꼬옥 읽어보길 추천한다.

그리고 오픈웨더 API 주소에 들어서 보면.

우리는 저 네모박스로 쳐준 부분만 사용할건데,

아래 main에서도 사용할 거는 temp로 써진 것들까지 사용할 것이다.

이렇게 하면 CurrentWeatherResult 코드를 스위프트 코더블로 받아올 수 있게 완료를 했다.

그리고 다시 ViewController로 돌아와서 ,

아까 작성한 코더블 코드 아래에 프라이빗한 함수를 하나 만들어준다.

URL 컴포넌츠에는 아까 날씨 api 사이트에서 나온 주소 중에

이렇게 표시한 부분까지 쿼리 전까지만 복사를 해줘서 넣어준 것이다.

queryItems를 따로 설정하는 이유는 URL에 쿼리 파라미터(query parameter)를 추가해서

서버로 요청을 보낼 때 유연하고 명확하게 작업할 수 있기 때문이고,

URLComponentsURL을 구성하는 각 부분(스킴, 호스트, 경로, 쿼리 등)을 손쉽게 다룰 수 있도록 도와주는 구조체다.

오픈 웨더에서 queryItems에 들어가야할 네가지다.

이렇게 URL 쿼리에 넣을 아이템들에 서울역 위경도 등 넣어주었다. ( 모두 String 값으로 넣어야한다 )

그리고 마지막 units는 온도 데이터를 사용자가 원하는 단위로 설정하기 위한 파라미터인데,

metric을 사용했기 때문에 섭씨로 반환되고, 한국과 같이 섭씨 단위를 사용하는 경우에 적합해서 사용했다.

설정하지 않으면 기본값으로 켈빈 단위를 반환하기 때문에 따로 계산이 필요하다.

만약 units를 설정하지 않으면?

API는 기본적으로 켈빈 단위로 데이터를 반환하는데, 켈빈 값은 섭씨로 변환하기 위해 계산이 필요하다.

변환 공식)

𝐶 = 𝐾 − 273.15

(섭씨 C는 켈빈 K 값에서 273.15를 뺀 값)


URLComponents

다시 fetchCurrentWeatherData 함수로 돌아와 코드 이어서 작성한다.

위 코드는 OpenWeatherMap API 를 사용해서 현재 날씨 데이터를 가져오기 위해 URL을 생성하고 유효성을 확인하는 단계를 포함하고 있는 것이고,

각 부분이 하는 역할과 코드 작성 이유를 설명해보겠다.


코드 분석

1. URLComponents

  • URLComponents는 URL을 구성하기 위한 객체다. 이 코드를 통해 URL의 기본 구조를 설정한다.
  • URLComponents를 사용하면 URL의 쿼리 파라미터를 쉽게 추가하고 관리할 수 있고,
    문자열로 직접 URL을 생성하는 것보다 안전하고 가독성이 좋다.

2. urlComponents?.queryItems

  • self.urlQueryItems는 미리 정의한 쿼리 아이템 배열로, 예를 들어 lat, lon, appid, units와 같은 정보를 담고 있다.
  • queryItems를 통해 쿼리 파라미터를 직관적으로 관리할 수 있고,
    문자열을 직접 조작하지 않아도 되기에 오타나 잘못된 형식의 URL을 방지할 수 있다.

3. guard let url = urlComponents?.url

  • URL이 제대로 생성되었는지 확인한다. urlComponents?.url은 URLComponents를 기반으로 URL 객체를 반환한다.
  • 유효하지 않은 URL이라면 nil을 반환한다. 이걸 방어적으로 처리하기 위해서 guard문을 사용한 것이다.
  • API 요청에 사용할 URL이 유효하지 않다면 네트워크 요청을 진행해도 에러가 발생하기에
    사전에 이를 방지하기 위해 URL 유효성 검사를 수행하는 것이 좋다.

이제 url을 넣어줘야하는데,

아래에 코드를 작성했다.

result: CurrentWeatherResult? 로 타입을 명시하면 위에 썼던 T? 로 인식이 되어서

모든 T 들이 CurrentWeatherResult로 인식한다.

fetchData는 네트워크 요청을 처리하고, 응답 데이터를 디코딩하여 completion 클로저로 전달하는 역할을 한다.

weak self를 사용해서 클로저의 순환 참조를 방지했는데,

네트워크 응답이 클로저 안에서 처리되기에, self를 약한 참조로 유지하여 메모리 누수를 방지할 수 있게 된다.

요청 결과가 없거나 디코딩이 실패한 경우 다음 단계를 진행하지 않기 위해 guard let을 사용했다.

DispatchQueue부터는 API로부터 받은 날씨 데이터를 UI에 반영했다.

result.main.temp, result.main.tempMin, result.main.tempMax는 각각 현재 기온, 최저 기온, 최고 기온을 나타낸다.

네트워크 작업은 백그라운드 스레드에서 이루어져서 UI 업데이트는 메인 스레드에서 처리해야 애플리케이션이 정상적으로 동작한다.

그리고 기온 데이터를 Int로 변환해 보기 쉬운 형식으로 표시한다.

그리고 fetchData 역시 재사용이 가능한 로직이다. (댕꿀)

잘 반영이 된다. 근데 왜 최소 온도랑 최고 온도가 같은지 모르겠네.. ㅜ


Image 로드

아이콘 관련 주소에 들어가서 보면,

이렇게 나오는데 원하는 아이콘이 10d 이면 아래 주소처럼 기입해라 라는 뜻이다.

1. URL 생성

guard let imageUrl = URL(string: "https://openweathermap.org/img/wn/\(result.weather[0].icon)@2x.png") else { return }
  • result.weather[0].icon은 API에서 받은 날씨 정보 중 첫 번째 아이콘 값을 가져오는 부분이다.
    icon 값이 01d라면 "https://openweathermap.org/img/wn/01d@2x.png" 같은 URL로 하면 된다.
  • URL(string:) 메서드를 사용하여 위 URL 문자열을 URL 객체로 변환하려고 시도한다.
  • 만약 URL이 올바르게 생성되지 않으면 guard 구문에 의해 함수가 종료된다. (return)

2. 이미지 데이터를 가져오기

if let data = try? Data(contentsOf: imageUrl) {
  • Data(contentsOf:)는 주어진 URL에서 데이터를 다운로드하는 메서드인데, 이 부분에서는 날씨 아이콘 이미지를 URL에서 직접 다운로드하고 있다.
  • try?는 오류가 발생할 수 있는 코드에 사용되며, 오류가 발생하면 nil을 반환하게 된다.
    오류가 없으면 다운로드한 이미지 데이터가 data 변수에 할당이 된다.

3. UIImage로 변환

if let image = UIImage(data: data) {
  • UIImage(data:)Data 객체로부터 UIImage 객체를 생성하는 메서드인데, 다운로드한 이미지 데이터를 UIImage로 변환하여 image 변수에 할당한다.
  • 만약 이미지로 변환할 수 없으면 nil이 되어 해당 코드는 실행되지 않는다.

4. UI 업데이트

DispatchQueue.main.async {
    self.imageView.image = image
}
  • UI 업데이트는 반드시 메인 스레드에서 수행해야 하므로, DispatchQueue.main.async를 사용해 메인 스레드에서 실행되도록 한다.
  • self.imageView.image = imageimageView라는 UIImageView에 다운로드한 이미지를 설정하는 부분이다.
    결론적으로 날씨 아이콘 이미지를 imageView에 표시하는 것이다.

테이블 뷰 추가하기

지금 각 테이블셀들에 대한 데이터를 가지고 있어야하는데 없어서 오류가 난건데,

그 데이터 소스를 self로 해서 현재 ViewController에서 정의한다 라는 거라서.. 하단에 extension으로 추가하면 된다.

하단에 코드를 쓰다보면 자기가 Fix 해주겠다고 난리가 난다.

왜 데이터 소스와 델리게이트는 extension에 나눠둘까?

역할에 따라 코드를 분리를 위함인데, 데이터 소스와 델리게이트는 보통 많은 메서드를 필요로 한다.

이것을 extension으로 분리하면 뷰 컨트롤러 본체에 UI 설정 및 중요한 로직만 남길 수 있다.

뿐만 아니라 가독성 향상에도 좋은 것이, 뷰 컨트롤러 본체가 복잡해지는 것을 방지하고 특정 역할의 메서드를 쉽게 찾을 수 있게 해준다.

TableViewCell 파일 추가

따로 tableViewCell 이라는 swift 파일을 추가해서 안에 cell에 대한 내용을 추가해야한다.

cell은 각자의 고유한 id를 가지고 있어야하기 때문에 static한 프로퍼티를 사용했다.

그리고 날짜와 시간을 알려주는 라벨과 온도를 알려주는 라벨이 필요하기에 두 라벨만 만들었고,

오버라이드 된 이니셜라이저는 TableViewstyleid로 초기화할때 사용하는 코드다.

그렇게 스타일과 아이덴티티파이어를 써주면 자꾸 Fix가 뜬다.

required init? 은 커스텀한 UI를 생성할 때 인터페이스 빌더를 통해 셀을 초기화 할 때 사용하는 코드다.

여기서는 fatalError 를 통해 명시적으로 인터페이스 빌더로 초기화 하지 않음을 나타낸다.

UITableViewCell에서 contentView를 사용하는 이유는 셀의 구조와 성능 최적화 때문입니다. 아래에서 이를 자세히 설명한다.


잠시 정리해보는 contentView

1. contentView란?

  • contentViewUITableViewCell의 주요 서브뷰로, 셀 내부에 개발자가 추가하는 모든 서브뷰를 담는 컨테이너 역할을 한다.
  • 셀의 레이아웃내용을 관리한다.
  • 셀의 다른 요소들과 충돌하지 않도록 보호한다.

2. 왜 직접 셀(UITableViewCell)에 추가하지 않고 contentView를 사용하는가?

셀의 기본 구조

UITableViewCell은 다양한 시스템 서브뷰를 포함하는 계층 구조를 가진다.

  • contentView: 셀의 실제 콘텐츠를 배치하는 공간.
  • backgroundView: 셀의 배경을 나타내는 뷰.
  • selectedBackgroundView: 셀이 선택되었을 때 표시되는 배경 뷰.
  • 기타 시스템 레벨 뷰들: 스크롤과 선택을 처리하기 위한 요소들.

contentView는 사용자 정의 서브뷰를 추가하기 위한 지정된 영역이다.


성능 최적화

뷰 재사용

  • UITableView는 셀을 재사용(dequeueReusableCell)하며 성능을 최적화한다.
  • contentView 내부의 서브뷰는 이 재사용 메커니즘에 포함되어 효율적으로 관리된다.
  • 셀 자체에 직접 서브뷰를 추가하면, 시스템의 예상 동작을 방해하거나 성능에 영향을 줄 수 있다.

구조적 일관성

  • 셀의 다른 요소(backgroundView, selectedBackgroundView 등)와 독립적으로 UI 요소를 안전하게 배치할 수 있다.
  • contentView를 사용하면, 시스템이 셀의 레이아웃을 관리하는 방식과 일치하며, 레이아웃 깨짐을 방지할 수 있다.

기타 기능 연계

  • contentView는 iOS가 제공하는 자동 레이아웃UI 업데이트 기능과 잘 통합된다.
  • 예를 들어contentView의 크기와 위치는 항상 UITableViewCell의 크기에 맞게 조정된다. 이를 통해 레이아웃 코드가 간단해지고 유지보수가 쉬워진다.

정리하자면,,,

  • contentViewUITableViewCell에서 개발자가 커스터마이즈할 수 있는 안전한 영역이다.
  • 셀의 기본 구조와 시스템 동작을 방해하지 않으면서 서브뷰를 추가할 수 있다.
  • 항상 contentView를 사용해 서브뷰를 추가하면, 레이아웃 충돌과 성능 문제를 방지할 수 있다.
  • 그냥 UITableViewCell 만들 때 contentView로 해.

5일간의 날씨 정보 넣기

위에 테이블뷰셀에 대해 으쌰으쌰 넣고 하단에는 퍼블릭한 컨피규어 셀 함수를 만든다.

그리고 새로운 swift 파일을 만들어 거기에 5일간의 날씨 정보를 받아와야하니 디코딩 작업을 해줘야한다.

api 홈페이지로 가서 5일간의 날씨 데이터를 가져와서 코더블 객체를 만들어준다.

아까 적다 말았던 컨피규어셀도 적어주고,

main에는 이미 전에 사용했던 WeatherMain 에도 똑같은 내용이 있엇기에 재활용했다.

그렇게 5일간의 날씨정보를 입력해준다. 이제 슬슬 힘들어온다. 벌써 이러면 안되는데.

그리고 위에서 다 적지 못했던 익스텐션 부분을 적어준다.

트러블..인가

오류가 발생해서 코드를 구석구석 뒤져보았다. 코드내용을 보면 fatalError("Expected superview but found nil when attempting make constraint equalToSuperview.") 라고 한다.

이 에러는 SnapKit에서 특정 뷰가 superview에 추가되지 않은 상태에서 제약 조건을 설정하려고 할 때 발생한다고 하는데,

뷰가 계층 구조에 올바르게 추가되지 않았음을 의미한다라는 걸 알았다.

여기서 이상한 점 역시나 발견...

addSubview 안에 테이블뷰가 없었다^^

그렇게 테이블 뷰까지 추가한 뒤 드디어 나온 뷰!!!!


음...

날씨앱은 코드를 하나하나 보면서 이해하려고 했지만 시간이 너무 오래 걸렸다.

디코딩도 너무 힘들었고, 디코딩 하면서 사용한 제네릭이라던지, 하나하나 파헤쳐가며 복습을 했지만..

과연 제대로 기억했을까 걱정이 된다.

아 그리고 API 호출 시 발생할 수 있는 여러 오류들에 대한 처리가 부족했던 것 같다..

그리고 이번 강의에서는 에러를 단순히 출력하는 방식으로 처리했지만,

실제 앱에서는 사용자에게 적절한 오류 메시지를 표시하거나 오류 처리 로직을 추가하는 것이 중요하다고 느꼈다.

UI가 매우 간단하게 구현되어 있어서 사용자에게 더 직관적이고 좀더 예쁜 UI를 제공하기 위해 추가적인 디자인의 조정도 했으면 더 좋았을 것 같다.

profile
iOS 좋아. swift 좋아.

0개의 댓글

관련 채용 정보