SSAC iOS 앱 개발자 데뷔과정 - 20 & 21

Sangwon Shin·2021년 10월 28일
0

SSAC

목록 보기
17/19

📑 Pagenation

대량의 데이터와 리소스를 분할해서 가져오는 방법.
주로 서버의 데이터와 리소스를 다룰 때 사용

일반적인 게시판을 예시로 들어보겠습니다.

사용자가 어떤 키워드를 검색할 경우, 해당 키워드가 포함된 게시글을 서버로 부터 받아옵니다.

일반적으로 우리가 웹에서 보는 형태는 위와 같이 한번에 모든 정보를 보여주는 것이 아니고 페이지에 정보를 나눠서 가져오게 됩니다.
❗️ 게시글을 서버에서 전체에서 다 가져올 필요가 없습니다. 사용자가 모든 데이터를 항상 다 본다는 보장이 없기 때문에 필요한 시점에 맞춰서 보여주는게 더 효율적입니다.

Pagenation 을 처리하는 방법으로 크게 2가지 방법이 있습니다.


1 ) Offset Pagenation

앞서 살펴본 page 에 따라 보여줄 게시글 수가 제한되어 있는 형태가 Offset Pagenation 입니다.

API call 을 확인해보면, 위와 같이 몇 번째 페이지에서 몇 개의 데이터를 보여줄 지 쿼리스트링으로 전달하게 됩니다.

Offset Pagenation 방식에는 문제점이 있습니다.

사용자들이 굉장히 많은 게시글을 쓰는 사이트가 있다고 가정해보겠습니다.

현재 우리가 1 page 에 20개의 게시글을 확인할 수 있도록 pagenation 을 설정 했습니다.

만약 우리가 2page 로 넘기기전에 사람들이 20개의 게시글을 새로 등록했다면 우리는 2 page 에서 앞서 1 page 에서 확인한 게시글들을 보게 됩니다.
경험해보셨을겁니다 ㅎ

그렇기 때문에 Offset Pagenation 방식은 서버의 데이터 변화가 적은 구조인 경우에 사용하는 것이 안전합니다.


2 ) Cursor Pagenation

클라이언트가 가지고 있는 마지막 데이터를 기준으로, 다음 데이터를 조회하는 방식입니다.

위의 그림과 같이 사용자가 가지고 있는 마지막 데이터의 고유값을 기준으로 다음 데이터 n 개를 주세요 라고 서버에 요청하는 형태입니다.

앞선 Offset Pagenation 에서와 같은 page 를 넘겼을 때, 중복된 데이터를 보게되는 문제점을 피할 수 있습니다.

하지만, 사용자가 조회한 시점 이후에 데이터가 서버에 추가된 경우에는 조회가 힘들 수 있습니다.

만약 조회한 시점 이후에 데이터가 계속해서 위로 쌓이게 된다면 계속해서 위쪽으로 스크롤이 생기게 되니까 Pagenation 을 사용하는 의미가 없어지지 않을까? 라고 생각합니다.

그렇기 때문에 인스타에서 스크롤을 통해서 피드를 초기화를 해야만 새로운 피드를 확인할 수 있습니다.

정확한 원리를 알게되면 다시 정리하도록 하겠습니다.

또한, 현재 페이지의 마지막 데이터를 기준으로 데이터를 호출하기 때문에 정보를 건너띄고 중간 페이지에 대한 값을 얻기 힘듭니다.
❗️Offset Pagenation 과 같이 Pagenation Navigation 을 구현할 수 없습니다.


iOS 에서는 Pagenation 을 어떻게 구현할 수 있을까요?

  • tableView willDisplayCell
  • ScrollView 의 Offset 활용
  • TableViewDataSourcePrefetching

위와 같이 3가지 방법으로 구현할 수 있습니다.

마지막 방법을 이용해서 Pagenation 을 간단하게 구현해 보겠습니다.

Pagenation을 이용한다는(?) 표현이 더 어울리는 것 같습니다.
이미 서버로부터 Offset Pagenation 방식으로 데이터를 받아 오기 때문에 어떤 시점에 데이터를 더 요청하는가 ? 로 이해했습니다.

서버와의 통신을 통해 데이터를 받아 테이블뷰를 통해 씬을 구성하는 상황을 생각해보겠습니다.

func fetchMovieData() {
        TdmbManager.shared.fetchWeatherData(page: startPage) { code, json in
            self.maxCount = json["total_results"].intValue
            for item in json["results"].arrayValue {
                
                // 서버로 부터 필요한 정보를 저장하는 과정 (생략)
                
                let data = MovieModel(mediaType: mediaType, titleData: title, subtitle: subTitle, imageData: image, userRatingData: userRating, releaseDate: relaseDate, movieID: movieID, movieOverView: movieOverView)
                
                self.movieData.append(data)
            }
            self.tableView.reloadData()
        }
    }

우선 서버와의 통신을 통해 우리가 요청한 데이터를 배열에 저장합니다.

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let cell = tableView.dequeueReusableCell(withIdentifier: TrendTableViewCell.identifier) as? TrendTableViewCell else {
            return UITableViewCell()
        }
        
        let row = movieData[indexPath.row]
        
        //Poster Set
        if let url = URL(string: "https://image.tmdb.org/t/p/original/" + row.imageData) {
            cell.movieImageView.kf.setImage(with: url)
        }
        else {
            cell.movieImageView.image = UIImage(systemName: "star")
        }
        
        
        cell.titleLabel.text = row.titleData
        cell.originTitleLabel.text = row.subtitle
UIFont.systemFont(ofSize: 18)
        cell.userRateLabel.text = row.userRatingData
        cell.mediaTypeLabel.text = row.mediaType
        cell.overViewButton.tag = indexPath.row
        cell.overViewButton.addTarget(self, action: #selector(overViewButtonClicked(selectButton:)), for: .touchUpInside)
        
        
        return cell
    }

그리고 저장된 배열을 기준으로 cellForRowAt 을 통해서 테이블뷰의 셀을 구성하게 됩니다.

사용자의 휴대폰에서 최대로 보이는 셀의 개수를 6개로 가정하겠습니다. 그리고 현재 서버의 데이터가 1page 당 20개의 게시물을 보여주도록 설정되어 있다고 가정하겠습니다.

그럼 사용자가 스크롤을 내릴 때 마다 cellForRowAt 함수가 호출되서 셀을 구성하게 될겁니다.

그럼 사용자가 스크롤을 계속해서 내리다 보면 20개의 게시물을 보게 되는 시점이 오게 되고 우리는 서버와의 통신을 통해 데이터를 받아와야 합니다.

우리는 언제 서버와 통신을 통해 데이터를 추가해야 할까요?

이때 prefetchRowAt 을 사용합니다.

func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
        for indexPath in indexPaths {
            if movieData.count - 1 == indexPath.row && movieData.count < maxCount {
                startPage += 1
                fetchMovieData()
                print("prefetch: \(indexPath)")
            }
        }
    }
    
    func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
        print("취소: \(indexPaths)")
    }

prefetchRowAtcellForRowAt 이 호출되기 전에 미리 필요한 데이터를 로딩에서 준비하고 있습니다.

그렇기 때문에 사용자가 20개의 데이터를 보기 이전에 미리 서버와의 통신을 통해서 데이터를 미리 준비할 수 있어, 우리는 어떤 로딩없이 스크롤을 계속 내려 데이터를 확인할 수 있습니다.

그리고 cellForRowAt 이 호출되면 미리 로딩했던 데이터들을 표현해줍니다. 만약 사용자가 스크롤을 빠르게 해서 셀을 보여줄 필요가 없는 경우에는 cancelPrefetching 을 통해서 관련 작업을 취소합니다.

하지만 우리는 서버로 부터 데이터를 배열에 저장했기 때문에 결국 사용자가 스크롤을 끝까지 내리면 모든 데이터가 배열에 저장되는 형태입니다.

추후에 이런 부분까지 개선할 방법을 찾게 되면 추가적으로 정리하겠습니다.

📸 Image PickerController

앱 내에서 카메라를 사용하기 위해서는 어떻게 해야 할까요?

우선 사용자의 위치에 접근하기 위해 권한을 요청했던 것과 마찬가지로, 카메라나 갤러리를 앱 내에서 사용하기 위해서는 권한이 필요합니다.

  • 카메라
  • 사진(갤러리)
  • 사진저장
  • 마이크

일반적으로 카메라와 관련된 4가지 권한이 있습니다.
마이크는 동영상 촬영을 위해서 필요합니다

그리고 사진촬영, 사진 선택을 위해 사용하는 ViewContrller 는 크게 2가지가 있습니다

  • UIImage PickerViewController
  • PHPickerViewController (iOS 14 +)

PHPickerViewController 를 사용한 경우에는 갤러리 내에서도 각각의 사진에 대한 접근 권한을 설정 할 수 있습니다.

그리고 갤러리 내에서의 Zoom in/out 이 가능합니다.

ImagePickerView 는 deprecate 될 예정이라고 합니다.(ㅠ)

하지만 아직까지 대부분의 앱에서 해당 Image Picker ViewController 를 사용하고 있습니다.

Image Picker ViewController 를 이용해 갤러리에 존재하는 사진을 이용하는 간단한 예제를 함께 살펴 보겠습니다.


Image Picker ViewController 는 네비게이션 컨트롤러를 상속받습니다! (이미지 선택화면으로 넘어가기 때문에)

let imagePicker = UIImagePickerController()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        imagePicker.delegate = self
        imagePicker.sourceType = .photoLibrary 
        imagePicker.allowsEditing = true //default = false

    }
    
@IBAction func captureButtonClicked(_ sender: UIButton) {
        self.present(imagePicker, animated: true, completion: nil)
        
    }
    

코드를 통해서 Image Picker ViewController 를 사용합니다.

extension VisionViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    
    //사진을 촬영하거나, 갤러리에서 사진을 선택한 직후에 실행 (필수)
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        print(#function)
        
        //1) 선택한 사진 가져오기 (편집 기능을 사용할거라면, originalImage 가 아닌 editImage 사용!)
        // 편집이 혀용되지 않은 경우에 editImage 에는 접근할 수 없음
        if let value = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
            // 2) 이미지뷰에 선택한 사진을 보여주기
            visionImageView.image = value
        }
        
        //3) picker dismiss
        picker.dismiss(animated: true, completion: nil)
        
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        print(#function)
    }
}

didFinishPicker 를 통해서 사용자가 사진을 선택했을 때 어떤 동작을 할 지 구현합니다.
❗️ editImage 를 허용하고 editImage 를 사용해야 합니다.


위와 같이 사용자가 입력한 정보를 기준으로 서버와 통신해 데이터를 가져오는 상황을 생각해보겠습니다.

우리는 어떤 시점에서 서버와 통신을 시작해야 할까요?

  • 사용자가 return 키를 눌렀을 때
  • 텍스트가 달라질 때 마다 (실시간)

후자의 방법이 좀 더 이상적이지만 문제가 있습니다.

텍스트가 수정 될 때마다 서버와 통신을 하게 되면 너무 잦게 서버를 호출하게 되고, 서버는 해킹, 과부화에 대한 문제로 호출을 제한 할 수 있습니다.

물론, 후자의 방법을 이용해서 구현한 앱들도 존재합니다!
저런 문제점들을 어떻게 극복할 수 있는지 추후에 방법을 알 게 되면 정리하도록 하겠습니다.

extension SearchViewController: UISearchBarDelegate {
    
    // 검색 버튼(키보드 리턴키) 눌렀을 때 실행
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        print(#function)
        if let text = searchBar.text {
            test = text
            movieData.removeAll()
            startPage = 1
            fetchMovieData(query: test)
        }
    }
    
    //취소버튼 눌렀을 때 실행
    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        print(#function)
        //self.searchBar.showsCancelButton = false // 너무 안 예쁨
        movieData.removeAll()
        tableView.reloadData()
        searchBar.setShowsCancelButton(false, animated: true)
    }
    
    //서치바에서 커서 깜빡이기 시작할 때
    func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
        print(#function)
        searchBar.setShowsCancelButton(true, animated: true)
        //self.searchBar.showsCancelButton = true
    } 
}

서치바에서 위의 그림과 같이 Cancel 버튼이 나왔다가 사라지도록 어떻게 구현할 수 있을까요?

DidBeginEditing 함수에서 showCancelButton = true
CancleButtonClicked 함수에서 showCancelButton= false

로 설정하면 됩니다.
❗️setShowCancelButton 을 이용하면 좀 더 애니매이션 효과가 적용된 것을 확인할 수 있습니다.

스크롤을 내리면 키보드가 내려가는 가는건 TableViewScroll View 를 상속받기 때문에
keyboard - Dismiss on Drag 를 사용하면 됩니다!


🏷 P.S.

드디어 깃허브에 이번주 과제를 하나, 둘 올리기 시작했습니다...(ㅠㅠ)
gitignore!!

과제를 미루지는 않았지만 정말 구현에 초점을 두고 너무 지저분하게 만든것 같아 하나씩 복습하면서 다시 수정해서 올리고 있습니다!!

바로 지난 블로그 P.S. 에서 서버와의 통신보다 웹뷰가 먼저 호출되서 딜레이를 주는 문제를 해결하기 위해서

1 ) 이중 for 문으로 딜레이
2 ) didSet
을 이용했었는데, 후자의 방법으로 성공하고 이중 for문으로는 실패했었습니다.

당시에는 성공했다는 기쁨에 빠져 왜 이중 for 문으로 딜레이를 줬을때는 실패했는가? 에 대해 생각하지 않았습니다.

이는 금요일 수업을 들으면서 깨달았습니다!! Alamofire 를 이용해 서버와 통신하는 과정이 비동기 과정이기 때문입니다!!!!!

빨리 금요일 수업내용도 정리해서 블로그에 올리도록 하겠습니다!

profile
개발자가 되고싶어요

0개의 댓글