-Today's Learning Content-

  • 트러블슈팅

1. URL 패치 오류?

내용 정리

개인과제를 진행하던 도중 포켓몬의 상세 정보를 보는 페이지에서 잘만 뜨던 포켓몬에 대한 정보가 표시되지 않는 버그가 발생했다. 어떻게 된 일인지 하나하나 찾아보려고 한다.

1) URL 매니저 만들기

개인과제를 진행하며 포켓몬의 상세 정보를 확인할 수 있는 디테일뷰의 구현까지 완료하였다.
그런데 코드가 많아지니 슬슬 더러운 코드가 보이기 시작했고, 클린 코드로 바꿔주기 위해 리팩토링을 크게 진행하였다.

이 중 가장 신경쓴 부분은 URL에 대한 부분이다.
원래 내가 URL을 호출하던 방식은 아래와 같다.

guard let url = URL(string: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(data.id).png") else { ... }

코드가 무척 길어지기도 하고, url이 필요한 지점에서 url을 직접 호출하는 형식이 마음에 들지 않았다.
게다가 같은 url을 반복해서 사용하게 된다면, 그 때마다 이렇게 url을 직접 호출하게 될텐데 무척이나 불편할 것이다.

그래서 모든 url을 한 번에 관리할 수 있도록 URL 매니저를 만들어 주었다.

// API 통신에 사용하는 URL을 관리하는 enum
enum APIEndpoint {
    case pokemonList(limit: Int, offset: Int)
    case pokemonDetails(id: Int)
    case pokemonImageURL(id: Int)
    case customURL(url: String)
    
    // 각 케이스별로 다른 URL을 get 하는 프로퍼티
    var urlString: String {
        switch self {
        case .pokemonList(limit: let limit, offset: let offset):
            return "https://pokeapi.co/api/v2/pokemon?limit=\(limit)&offset=\(offset)"
        case .pokemonDetails(id: let id):
            return "https://pokeapi.co/api/v2/pokemon/\(id)/"
        case .pokemonImageURL(id: let id):
            return "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(id).png"
        case .customURL(url: let url):
            return url
        }
    }
}

이 열거형은 각 케이스별로 연관값을 가지고 있어 url별로 필요한 쿼리값을 지정하도록 하였다. 실제로 사용할 때는 아래처럼 사용할 수 있다.

URL(string: APIEndpoint.pokemonImageURL(id: id).urlString)

여전히 길기는 하지만, 전보다 간소화 되고 필요에 따라 자유롭게 구현할 수 있는 점과 재활용성이 높아진 점이 중요하다.
이제 빌드를 통해 확인해 보면...

왜 상세 정보가 안 보이게 된걸까...
분명 URL 매니저를 만들기 전까지는 잘 작동했는데...

원인을 찾아보자..!!

2) 에러포인트 체크하기

코드를 대대적으로 리팩토링 했기 때문에 어떤 점에서 문제가 발생했을지 함부로 예상하기가 어려웠다.
때문에 브레이크 포인트를 하나씩 걸어가며 어떤 부분에서 문제가 발생하는지 보고자 한다.

제일 먼저 네비게이션을 할 때 데이터 전달이 제대로 안 이루어지는지 확인하기 위해 셀을 선택했을 때 액션을 구현하는 부분을 확인해 보았다.

// 셀이 선택되었을 때 액션 구현
// 선택된 셀의 포켓몬 정보를 확인할 수 있도록 네비게이션 구현
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
	let detailView = PokemonDetailView(
		image: self.pokemonImageList[indexPath.item].image,
		model: DetailViewModel(pokemonManager: PokemonManager(), id: indexPath.item + 1)
	)
        
	guard let view = self.window?.rootViewController as? UINavigationController else { return }
	view.pushViewController(DetaileViewController(detailView: detailView), animated: true)
}

여기서 detailView를 초기화할 때 파라미터로 주는 model의 파라미터인 id를 사용해서 포켓몬의 정보를 불러온다. 이 id 값이 잘못되진 않았는지 브레이크 포인트를 사용해서 살펴보자


아무 문제 없다...

그럼 다음 포인트로 이동하자.
다음 포인트는 API 통신으로 정보를 가져오는 디테일 뷰 모델의 메소드이다.

/// 네트워크 매니저를 통해 포켓몬의 자세한 정보를 불러오는 메소드
/// - Parameter id: 불러올 포켓몬의 도감 번호
private func fetchPokemonData(id: Int) {
	self.pokemonManager.fetchPokemonData(urlType: .pokemonDetails(id: id), modelType: PokemonDetailDataModel.self)
		.subscribe(on: ConcurrentDispatchQueueScheduler(qos: .default))
		.subscribe(onSuccess: { [weak self] (details: PokemonDetailDataModel) in
			guard let self else { return }
                
			self.pokemonDetailData.onNext([details])
                
		}, onFailure: { error in
			self.pokemonDetailData.onError(error)
                
		}).disposed(by: self.disposeBag)
}

여기서 self.pokemonDetailData.onNext([details])까지 온다면 API 통신은 문제가 없는 것이기 때문에 여기에 브레이크 포인트를 걸고 계속해서 진행해보자.

그런데...

다음 스텝을 누르니 브레이크가 걸리지 않고 포켓몬 디테일뷰 화면이 네비게이션을 통해 표시되었다.
그럼 여기서 문제가 발생한다는 의미인데, fetchPokemonData 메소드에서 다시 브레이크 포인트를 걸어보자.

/// 네트워크 매니저를 사용해서 데이터를 디코딩하고 옵저버블로 반환해주는 메소드
/// - Parameters:
///   - urlType: 디코딩할 URL
///   - modelType: 디코딩할 데이터 모델 타입
/// - Returns: 디코딩된 데이터를 가진 옵저버블 타입
func fetchPokemonData<T: Decodable>(urlType: APIEndpoint, modelType: T.Type) -> Single<T> {
	guard let url = URL(string: urlType.urlString) else {
		print(NetworkError.invalidURL.errorDescription)
		return Single.error(NetworkError.invalidURL)
	}
        
	return NetworkManager.shared.fetch(url: url)
}

여기서는 네트워크 매니저를 통해 파라미터로 입력받은 url을 특정 타입으로 디코딩하는 작업을 진행한다.
guard문 안에 있는 프린트문에 브레이크 포인트를 찍고 url이 잘못된건 아닌지 체크해보자.

음... 이번에도 브레이크가 걸리지 않고 빌드가 진행되는 것을 보면 url은 괜찮은 듯 하다.
그렇다면 return으로 네트워크 매니저가 호출하는 fetch메소드가 문제가 있는 듯 하다.

/// URL을 통해 API 통신을 통해 데이터를 디코딩하는 메소드
/// - Parameter url: 디코딩할 URL
/// - Returns: Single 타입 -> 디코딩한 값을 데이터 바인딩
func fetch<T: Decodable>(url: URL) -> Single<T> {
	return Single.create { observer in
            
		let session = URLSession(configuration: .default)
		session.configuration.httpMaximumConnectionsPerHost = 5
		session.dataTask(with: url) { (data, response, error) in
                
			guard let data, error == nil else {
				observer(.failure(error!))
				return
			}
                
			guard let response = response as? HTTPURLResponse,
				  (200..<300).contains(response.statusCode)
			else {
				observer(.failure(NetworkError.dataFetchFail))
				return
			}
                
			do {
				let decodedData = try JSONDecoder().decode(T.self, from: data)
				observer(.success(decodedData))
                    
			} catch {
				print(NetworkError.decodingFail.errorDescription, error)
				observer(.failure(NetworkError.decodingFail))
			}
                
		}.resume()
            
		return Disposables.create()
	}
}

fetch 메소드는 입력받은 url을 URLSession을 통해 검증하고 특정 타입으로 디코딩해주는 메소드이다.
여기서 guard문의 else 내부 코드에 모두 브레이크 포인트를 걸고 실행을 시켜보자.

그랬더니 아래 코드에 브레이크가 걸렸다.

이 코드는 url의 response를 HTTPURLResponse 타입으로 타입 캐스팅하여 http의 데이터 송수신이 바르게 일어났는지 확인하는 부분이다. response의 statusCode가 200~299의 범위라면 데이터 송수신이 올바르게 일어난 것이고, 그렇지 않다면 문제가 있다는 것이다.

혹시 범위를 벗어난건가 싶어서 statusCode를 검출해보니...

404...
인터넷을 많이 하면 자주 보이는 코드가 출력되었다.

이걸 보자마자 혹시 url이 잘못된건가?! 싶어서 URL 매니저를 다시 확인해 보았는데...

포켓몬의 상세 정보를 불러오는 URL의 끝에 공백" "이 포함되어 있었다.
솔직히 설마 이것 때문에 에러가 발생했겠어... 싶은 마음도 있었지만, 혹시 몰라서 공백을 지우고 실행해 보았는데...

상세 정보가 너무 잘 뜬다...
이걸 찾고나서 좀 허무했다... 블로그로는 금방금방 찾은 것처럼 보이지만 실제로는 30분 정도 열심히 브레이크를 걸어가며 찾았기 때문이다.

3) 결론

URL을 잘 확인하자!

-Today's Lesson Review-

너무 어이없어서 이 날은 이후 더 진행하지 않았다...
profile
이유있는 코드를 쓰자!!

0개의 댓글