[Android] MVVM + Jsoup

cotton·2022년 11월 28일
1

Jsoup

Jsoup은 자바에서 HTML 파싱을 할 수 있게 도와주는 라이브러리로, Java 기반 라이브러리이기 때문에 Android, Kotlin에서도 동일하게 사용이 가능하다. 또한 Jsoup은 DOM 구조를 추적하거나, CSS selector를 이용해 사이트에 있는 데이터를 추출하기에 용이하다.

with MVVM

해당 이미지를 보면, Remote 부분에서는 Retrofit를 이용해 API들을 호출하는 형태를 띄고 있다.

하지만 Jsoup처럼 HTML 파싱을 진행하는 경우에는 API를 이용하는 형식이 아니므로, 위 사진처럼 Retrofit을 이용하지 않고 Jsoup으로 대체하여 패턴에 적용시켜야 한다.

그렇다면 위와 같은 MVVM 패턴에서는 어떻게 Jsoup을 패턴에 맞게 사용할 수 있을까?

Jsoup + MVVM

Jsoup을 이용해, 네이버 스포츠 메인에 있는 오늘의 스포츠 뉴스 리스트를 가져와 앱 내에 있는 RecyclerView에 보여줄 수 있도록 개발할 것이다.

해당 이미지에서 파싱해올 수 있는 타이틀, 디스크립션, 썸네일 등을 파싱해 RecyclerView를 이용해 띄워보자.

Activity

해당 프로젝트는 데이터 레이어 및 Lifecycle 부분에서의 종속성 코드를 추가적으로 개발하지 않기 위해서 의존성 주입 라이브러리 중 Hilt를 사용하여 개발했다.

Activity 단에서는 패턴과 관련된 코드보단 UI와 관련된 코드로, RecyclerView Adapter 정도를 설정해 주는 로직이 존재한다.

@AndroidEntryPoint
class SportsNewsActivity : BaseActivity<ActivitySportsNewsBinding>(
    R.layout.activity_sports_news
) {

    private val viewModel: SportsNewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding.viewModel = viewModel
        binding.rvSportsNews.adapter = SportsNewsItemAdapter()
    }
}

ViewModel

ViewModel 단에서는 Retrofit을 이용하는 기존과 동일하게, 가공된 데이터를 이용할 수 있는 형태로 구현하면 된다.

@HiltViewModel
class SportsNewsViewModel @Inject constructor(
    private val sportsNewsRepository: SportsNewsRepository
) : BaseViewModel() {

    private val _sportsList = MutableLiveData<List<SportsNews>>()
    val sportsList: LiveData<List<SportsNews>> = _sportsList

    init {
        getSportsNews()
    }

    private fun getSportsNews() = viewModelScope.launch {
        _isLoading.value = true

        sportsNewsRepository.getMainSportsNews().onCompletion {
            _isLoading.value = false
        }.collect {
            _sportsList.value = it
        }
    }
}

Repository

해당 프로젝트 코드에서는 단순히 DataSource에서 가공한 데이터를 받아 ViewModel로 넘겨주는 역할을 가진다.

interface SportsNewsRepository {

    fun getMainSportsNews(): Flow<List<SportsNews>>
}

class DefaultSportsNewsRepository(
    private val remoteSportsNewsDataSource: SportsNewsDataSource
) : SportsNewsRepository {

    override fun getMainSportsNews(): Flow<List<SportsNews>> {
        return remoteSportsNewsDataSource.getMainSportsNews()
    }
}

DataSource

DataSource 단에서는 Jsoup의 메인 로직이 존재한다.

Retrofit에서는 interface API 코드를 불러 서버 통신을 진행하지만, Jsoup에서는 링크를 연결해 해당 웹 사이트의 HTML 코드를 Document 객체로 불러와, attribute 등을 이용해 데이터를 가공해 객체를 생성하고 Flow 등을 이용해 데이터를 Repository 단으로 전달할 수 있게 한다.

finterface SportsNewsDataSource {

    fun getMainSportsNews(): Flow<List<SportsNews>>
}

object RemoteSportsNewsDataSource : SportsNewsDataSource {

    override fun getMainSportsNews(): Flow<List<SportsNews>> = flow {
        try {
            val sportsNewsUrl = "https://sports.news.naver.com"
            val doc = Jsoup.connect(sportsNewsUrl).get()
            val sportsNewsList = getSportsNewsWithDoc(doc)

            emit(sportsNewsList)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }.flowOn(Dispatchers.IO)

    private fun getSportsNewsWithDoc(document: Document): List<SportsNews> {
        val newsList = mutableListOf<SportsNews>()
        val newsListDoc = document.select("li.today_item")

        repeat(newsListDoc.size) {
            newsListDoc[it].also { element ->
                val href = element.select("a").attr("href")
                val textArea = element.select("div.text_area")
                val imageArea = element.select("div.image_area")

                val title = textArea.select("strong.title").text()
                val imgSrc = imageArea.select("img").attr("src")
                val description = textArea.select("p.news").text()
                val sportsType = textArea.select("div.information span").first()?.text()
                val channel = textArea.select("div.information span").last()?.text()

                newsList.add(
                    SportsNews(
                        href = href,
                        title = title,
                        imgUrl = imgSrc,
                        description = description,
                        sportsType = sportsType,
                        channel = channel
                    )
                )
            }
        }

        return newsList.toList()
    }
}

마치며

해당 내용은 Retrofit을 기반으로 한 데이터 레이어 패턴을 잘 알고 있는 상태라면 Jsoup을 적용한 패턴으로 리팩토링하는 과정이 그렇게 어렵지 않을 것이다. Retrofit을 Jsoup으로 교체하는 것이지 기존 패턴이 변경된다기 보다는 통신 방식의 차이에서 발생한 코드 수정 사항들만 적응한다면 쉽게 개발할 수 있을 것이다.

프로젝트는 하단 링크에서 자세한 코드를 확인할 수 있다.
https://github.com/KRMKGOLD/JsoupWithMVVM

profile
안드로이드 개발자

1개의 댓글

comment-user-thumbnail
2023년 5월 31일

좋은 글 감사합니다!

답글 달기