[iOS] TableView의 Pagination을 구현해보자

Youth·2023년 9월 27일
1

TIL

목록 보기
11/21

오랜만에 UI관련 주제를 가지고온 킴스캐슬입니다

저도 처음엔 UI구현에 관한 taks인줄알았는데 막상 해보니 동기/비동기관련 내용도 포함되어있는거같더라고요
그래서 블로그 포스팅의 주제로 가져와봤습니다

그런데 정말 오랜만에 UI를 짜려니까 헷갈리는 부분이 많더라고요...

제가 구현하려는 UI는 pagination이라고도 하는데 무한스크롤이라고 알고계신분들도 많은거같아요
되게 자주 사용하는건데 tableview를 아래로 스크롤 할때 스크롤 마지막에가서 데이터가 계속 추가되어서 무한으로 스크롤할수있는 그런 뷰입니다

지금부터 이런 뷰를 만들어보겠습니다

UI구현하기

사실 UI구현은 어려울게 없습니다 그냥 기본 tableView를 하나 만들어주고 cell에다가 프로그램이름이랑 날짜를 넣어줬습니다

그런데 여기서 중요한 구현요소는 scoll이 table의 가장 아래부분에 도착했을때 어떤 네트워킹 task를 해야하는데 이 시점을 어떻게 알아낼것인가입니다
즉, tableview의 가장 아랫부분에 도착하는시점을 어떻게 알아낼 수 있을것인가입니다

키워드는 scroll입니다
tableview는 scrollview를 상속하고있기때문에 tableview의 delgate를 self로 해주는순간 scrollview관련 delegate메서드를 사용할 수 있습니다

그중에서도 func scrollViewDidScroll(_ scrollView: UIScrollView)를 사용하면 contentOffset.y를 통해서 내가지금 화면의 어느 y축 좌표에 스크롤해서 도착했는지를 알 수 있습니다

그러면 tableview의 맨아래부분을 어떻게 알 수 있을까요...?
이 부분을 알기위해서는 scroll의 조건에대해 알아야합니다
scroll은 bounds size의 높이보다 content size의 높이가 클때 발생합니다
(발생한다고 하니 말이 좀 이상한데 여튼 이런조건이어야 scroll이 되게 된다는것만 이해해주시면 됩니다)

이게 무슨 소리냐면 쉽게말해서 화면에 보이는 tableview의 높이보다 실제 tableview의 내용이 클때 scroll이 되야합니다. 우리가 tableview안에 cell이 하나만 있는데 스크롤이 되지는 않죠 보통 tableview의 크기는 작은데 cell이 200개~300개 되는 경우엔 당연히 scroll이 된다고 생각하잖아요 똑같은 말입니다

그래서 우리가 화면에 보이는 tableview자체의 높이는 bounds.size.height로 구할 수 있고 tableview안에 들어간 내용에 관한 높이는 content의 높이라고 해서 contentSize.height라고 할 수 있습니다 그러면 화면에서 가장 아랫부분은 contentSize와 tableview자체의 크기의 차이만큼이지 않을까요?

제가 tableview의 레이아웃을 아래와같이 잡았습니다

그렇다는 말은 아이폰의 screen의 크기가 tableview의 크기와 동일하니까 tableview의 높이가 screen전체의 높이와 같겠네요

즉 현재 tableview가 파란색영역인 아이폰 screen의 영역과 동일하게됩니다
만약에 tableview의 contentsize가 커서 scroll이 되야하는 경우라고 가정하면 어떻게될까요?

그러면 위와 같은 그림이 될텐데요 결국 scroll을 했을때 맨 아래부분에 도달한다는 뜻은 contentSize에서 tableview의 size를 뺀만큼을 scroll해야한다는 뜻이됩니다

그리고 scroll을 y축으로 얼만큼 했는지를 수치화할수있는 친구가 contentOffset.y이기때문에

contentOffset.ycontentSize.height - bounds.size.height보다 커지는 그 순간이 scroll을해서 tableview의 가장 아래부분에 도달했을때인겁니다

우선 이러한 아이디어가 pagination의 가장 기초적인 아이디어입니다
물론, 이렇게하면 문제가 발생은 합니다 이 문제는 네트워킹 관련 얘기를 할때 함께하도록 할게요

네트워킹

사실 이걸 구현할려했을때 대체 어떤 API를 써야하지가 큰 고민이었습니다 결국 해당 UI를 구현하려면 데이터를 page별로 받아올수가있어야하거든요

그래야 1page를 받아와서 뿌려주고 table아래에 도달했을때 2page데이터를 추가해줘서 늘려주고 하는 방식을 반복할수있을테니까요

그런데 영화API중에 TMDB라는 API가 있어서 API key를 받아서 사용해봤습니다

저는 async/await을 좀더 선호해서 async/await으로 구현해봤습니다
이렇게 네트워킹을해서 page마다 해당 data를 받아와서 viewcontroller에 데이터를 담는 변수에 값을 넣어주고 reloadData를 해주는 코드는 아래처럼 구현했습니다

1초라는 sleep을 줬는데 이건 image를 변환하는 코드도 없고 너무 빨리 데이터가 받아와져서 데이터를 받아오고 reload하는데 1초정도 걸린다고 가정하기 위해서 이렇게 구현했습니다

이렇게 해서 page라는 전역변수를 초기값 1로해두고 매번 1씩 증가시켜서 api를 호출하면되겠죠?

맨아래에 도달했다는 print문도 출력해보고 몇페이지를 가져오는지를 print해보겠습니다. 로직은 맞는거 같으니 실제로 실행을 해보겠습니다

오른쪽의 출력문을 보면 맨아래에 도달하는순간 api호출이 계속 반복되어서 1초안에 api를 50번출력한걸 알 수 있습니다...
그래서 api호출이 끝나서 reload가 되는 순간 50page의 데이터들이 한번에 쫙 들어오게됩니다

그래서 api호출이 진행되고 있는 동안에는 다음 page로의 api호출이 될 수 없도록 제약조건을 걸어두는 로직이 필요합니다

그래서 isFethcing이라는 변수를 bool로 하나 만들어서 isFethcing이 true라면 api호출이 진행되고 있다는 뜻이니 api호출을 하지 않도록 해보겠습니다

만약에 맨아래에 도달한 순간 isFetching이 true라서 api호출을 하는데 isFetching을 false로 만들어주고 getMovieData라는 api호출이 끝날때까지 isFetching이 계속 false니까 방금처럼 api호출이 여러번호출될일이 없다고 생각하고 짠 코드입니다

실제로 한번 동작을 시켜보겠습니다

이상하죠...? 분명히 제약조건을 걸어줬는데도 이전이랑 똑같이 작동을합니다....

async/await의 Task

이런게 동작하는 이유를알기 위해서는 async/await의 task에 대한 이해가 조금 필요합니다
getMovieData라는 메서드는 Taks블럭을 실행시키는 메서드입니다

그리고 Task블럭은 비동기로 작동합니다
그렇다는 말은 우리가 아래 코드를 해석하는 순서를 다시 생각해볼필요가 있다는 뜻이됩니다

처음에는 우리가 1->2->3의 순서로 작동할거라고 생각했지만 getMovieData가 비동기로 실행된다면 애초에
2번이 실행되는 순간 1초의 sleep을 기다리지 않고 바로 3번이 실행된다는 뜻이됩니다

즉 1번이 실행되고나서 2번과 동시에 3번실행된다면 2번을 실행하는 1초의 시간을 기다리지 않고 3번이 실행되니까 isFetchgin이 다시 true가 되고 if문을 통과할 수 있게됩니다

그러면 다시 1번이 실행되고 2번과 3번이 동시에 실행됩니다. 그러면또 2번을 실행하는 1초의 시간을 기다리지 않고 3번이 실행되니까 isFetching이 다시 true가 되고 if문을 통과할수있게되는 반복이 계속되는겁니다 그러다가 비동기task가 끝나면 reloadData를해주니까 데이터가 업데이트되는데 이런 task들도 전부 각각 비동기로 처리가 되기때문에 데이터의 완료또한 순서를 보장할수가 없게됩니다

각 task의 호출이 끝나고나서 몇페이지의 데이터를 가져오는걸 완료시켰는지를 출력해볼까요?

각 page의 순서에 맞게 완료되는게 아니라 완전 뒤죽박죽으로 실행이 완료되는걸 알 수 있습니다

그러면 어떻게 해야 순서대로 한번씩만 호출이될까요?
순서대로 실행이 된다는건 동기적으로 작동을 해야한다는 뜻이되고 비동기처리에서 동기적으로 수행을 보장해주는 곳은 task블럭안입니다

getMovieData라는 전체 task블럭은 비동기로 작동하지만 do안에있는 코드들은 순서대로 실행이 됩니다
await을 만나면 해당 task가 suspend되어서 os로 제어권이 넘어가고 await이 끝나면 다시 제어권을 돌려받아 순서대로 수행을 하게됩니다

이게 무슨말이냐면

Task블럭을 수행할때 우선 1초를기다리는 await을 만났으면 시스템한테 너 할거해라고 이야기를합니다 그리고 이 tak블럭의 수행은 잠시 멈춥니다

그리고 1초가 지나고 await메서드가 끝나면 다음줄인 manager.getMovieData를 호출하는데 이거또한 await이기때문에 끝날때까지 해당 task내부에서는 다음 명령인 append를 실행하지 않고 기다립니다

결국 task안에서 isFetching을 만져줘야 task가 끝날때까지 isFetching을 원하는상태로 유지할수있습니다

isFetching을 task안에서 동기적으로 수행하도록 바꾸고 scollview delgate를 통해 맨아래에 도달을 감지하는 부분에서는 isFetching이 false여야 다음 api호출을 할테니 약간 바꿔서 isFetching이 false이면 api를 호출하고 taks내에서는 api호출을 시작하면 isFetching을 true로 바꾸고 끝나면 다시 false로 바꿨습니다

이렇게 하고 한번 다시 실행해볼까요

task가 한번씩만 실행되고, 끝나면 비로소 다음 task를 실행해서 순서가 보장되고 api호출이 원하는대로 불리는걸 볼 수 있습니다


처음에는 크게 어렵지 않을 주제일거같았는데 생각해야할 요소가 몇가지는 있었던 구현이었습니다
async/await을 사용할줄만 알았었다면 아마 이런 실제작동원리에대한 이해는 없었을거고 그렇다면 원하는 방식으로 구현을 하지 못했거나 했더라도 주먹구구식으로 했었을수있겠다는 생각이들더라고요

wwdc를 보면서 asyn/await에 대한 작동원리와 suspend에대한 공부를 했던게 이런 부분에서 와닿는것같습니다. 역시 사용방법도 중요하지만 작동원리와 중요한 개념들을 알고있어야 이런 문제를 마주쳤을때 근거를 가지고 해결방법을 찾아낼 수 있는거같습니다

다음에도 한번쯤은 고민해볼만하고 재미있는 주제로 돌아오겠습니다
그럼 20000!

profile
AppleDeveloperAcademy@POSTECH 1기 수료, SOPT 32기 iOS파트 수료

0개의 댓글