포켓몬 도감 앱 만들기

maxminseok·2024년 12월 27일
1

시작

이번엔 개인 과제로 포켓몬 도감 앱 만들기에 도전하였다.

메인 화면에서 포켓몬 이미지가 나오게 되고, 포켓몬 이미지를 클릭하면 해당 포켓몬의 상세 정보가 나오는 화면으로 넘어가는 식이다.

이전에 했던 포켓몬 연락처앱과 비슷할 수도 있는데, 이번에는 MVVM 구조와 RxSwift를 이용하여 옵저버 패턴과 비동기 프로그래밍 등을 실습하고자 했다.

그리고 저번 팀프로젝트를 하면서 배운 깃허브의 issue 브랜치와 Project 기능을 활용하여,
혼자 진행하는 개인 과제이지만 좀 더 진척도를 체계적으로 관리해보려고 하였다.


NetworkManager 추가

import Foundation
import RxSwift

enum NetworkError: Error {
    case invalidUrl
    case dataFetchfail
    case decodingFail
}

class NetworkManager {
    
    static let shared = NetworkManager()
    private init() {}
    
    func fetch<T: Decodable>(url: URL) -> Single<T> {
        return Single.create { observer in
            let session = URLSession(configuration: .default)
            session.dataTask(with: URLRequest(url: url)) { data, response, error in
                // 네트워크 요청 중 에러 발생 여부 확인
                if let error = error {
                    observer(.failure(error))
                    return
                }
                // 응답 데이터 상태 코드 200~299 범위인지 확인(성공인지 확인)
                guard let data = data,
                      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 {
                    observer(.failure(NetworkError.decodingFail))
                }
            }.resume()
            return Disposables.create()
        }
    }
}

기존에 쓰던 네트워크 통신 코드와 같은 코드이다.

한가지 바꾸고 싶은 부분이 있다면 전에 공부한 적 있는 asnyc/awiat를 적용시켜보고 싶은데, 일단 전체 기능 구현 먼저 하기로 하였다.


Model 구현하기

https://pokeapi.co/api/v2/pokemon?limit=\(limit)&offset=\(offset) 이런 형태의 url을 사용하여 서버로부터 데이터를 받아올 것이다.

따라서 이 데이터를 저장할 구조체를 선언할 것이다.

위 url을 사용하여 실제로 넘어오는 데이터를 확인해보면 위와 같다.

import Foundation

struct PokemonList: Codable {
    let results: [PokemonData]
}

struct PokemonData: Codable {
    let name: String
    let url: String
}

nameurlresults라는 배열 안에 들어있는 형태로 받아지기 때문에 이를 저장하기 위한 구조체 두개를 선언하였다.

그리고 https://pokeapi.co/api/v2/pokemon/\(pokemon_id)/ 이런 형태의 url도 사용할 건데, 이 부분이 고민이 되었다.

포켓몬의 id를 받아야 하는데 이를 받기위해 따로 제공되는 api가 없어서 찾다가, 일단 없이 구현하려고 했다.

일단 먼저 구현해야 하는 건

이런 형태의 화면이고, 이 화면을 띄우기 위해선 https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(id).png 이런 url을 사용해야 하기 때문에 이곳에도 id가 필요했다.

id를 받지 않고는 구현이 안되는 것이다.

func pokemonID(_ url: String) -> String {
    if let components = url.split(separator: "/").last {
        return String(components)
    } else {
        return ""
    }
}

이런 메서드를 써서 위 PokemonData에 있는 url의 마지막 숫자를 쓸 수 있도록 해야하나 싶었다.

어떻게 해야하나 한참 고민하면서 저번에 넷플릭스 클론 코딩 때 했던 코드를 다시 복습하다가, 메인 뷰모델에서 이런 코드를 보았다.

    // 동영상 키 값을 반환하는 메서드
    func fetchTrailerKey(movie: Movie) -> Single<String> {
        guard let movieId = movie.id else { return Single.error(NetworkError.dataFetchfail) }
        let urlString = "https://api.themoviedb.org/3/movie/\(movieId)/videos?api_key=\(apiKey)"
        guard let url = URL(string: urlString) else {
            return Single.error(NetworkError.invalidUrl)
        }
        
        return NetworkManager.shared.fetch(url: url)
            .flatMap{ (videoResponse: VideoReponse) -> Single<String> in
                if let trailer = videoResponse.results.first(where: { $0.type == "Trailer" && $0.site == "YouTube" }) {
                    guard let key = trailer.key else { return Single.error(NetworkError.dataFetchfail) }
                    return Single.just(key)
                } else {
                    return Single.error(NetworkError.dataFetchfail)
                }
            }
    }

메인 뷰 컨트롤러에서 컬렉션 뷰 셀을 클릭하면, 위 메서드를 이용해 해당 영화의 key 값을 받고, 이 key 값을 url에 넣어 동영상을 재생하는 코드였다.

이 코드와 비슷하게, 메인 뷰모델에서 Single을 이용해 포켓몬 이미지를 받아올 URL을 반환하면, UICollectionViewCell에서 바인딩 하여 셀의 이미지를 그리면 되겠다 싶었다.

struct PokemonData: Codable {
    let name: String
    let url: String
    
    // url에서 id 추출
    var id: Int {
        Int(url.split(separator: "/").last ?? "0") ?? 0
    }
}

PokemonData에서 포켓몬의 id를 추출하기 위해, URL의 마지막 부분이 항상 포켓몬의 ID를 나타내는 PokeAPI의 규칙을 활용하여 id라는 계산 프로퍼티를 추가하였다.

위에서 작성했던 메서드를 활용한 것이다.

MainViewModel에도 다음과 같은 메서드를 추가하였다.

    // 포켓몬 이미지 불러오기
    let baseUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/"
    func fetchPokemonImage(_ pokemonData: PokemonData) -> Single<URL> {
        guard let imageUrl = URL(string: baseUrl + "\(pokemonData.id).png") else {
            return Single.error(NetworkError.invalidUrl)
        }
        return Single.just(imageUrl)
    }

이미지를 불러올 url이 정해져 있으니 NetworkManager를 거치지 않고 바로 반환할 수 있도록 작성하였다.


남은 구현 기능

남은 구현 기능은 다음과 같다.

  • MainViewController 구현
  • UICollectionViewCell 구현
  • DetailViewController 구현
  • DetailViewModel 구현
  • 영어로 제공되는 포켓몬 데이터 한글로 번역
  • 무한 스크롤 기능 구현

이전처럼 한번에 구현하는 게 아니라 파트를 나눠 체계적으로 하나씩 구현하려니 이전보다 구조에 대해 더 생각하면서 작성하게 되어 더 오래걸리는 것 같다.
2~3일이면 충분히 구현하지 않을까 했는데 생각보다 더 길게 잡아야 할지도 모르겠다.

2개의 댓글

comment-user-thumbnail
2024년 12월 28일

PokeAPI의 규칙을 활용하여 id라는 계산 프로퍼티를 추가를 잘 시도해보셨네요 !👍
새로운 기술을 학습하면서 구현하는 경우 평소보다 1.5배로 산정했던게 많이 도움되었어요
먼저 기간 산정을 해보면서, 실제 기간과 비교해보면서 어떤 사유로 딜레이 되는지 살펴보면 일정 산정하는 능력이 향상될거같아요 !

답글 달기
comment-user-thumbnail
2025년 1월 7일

마음에 드는 코드라고 유명해서 보러 왔습니다.

답글 달기