ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • iOS) BoostCourse) PTJ1 MusicPlayer
    iOS 2019. 7. 14. 16:13

    MusicPlayer

    부스트코스 PTJ1을 진행하면서 배운 내용과 리뷰받은 내용을 정리해보고자 합니다. 스스로 공부하며 이해한 내용을 적은 것으로 내용에 오류가 있을 수 있습니다. 오류 또는 수정이 필요한 부분은 댓글로 남겨주시면 정말 감사하겠습니다!


    Contents

    • AVFoundation
    • AVAudioPlayer
    • Timer
      • @escaping
      • [unowned self]
      • truncatingRemainder
    • Optional과 예외처리

    PTJ1을 공부하면서 새롭게 배운 내용들을 정리해보고자 합니다! 사실 구현 자체는 부스트코스의 PTJ1을 성실하게 들었다면 모두 쉽게 할 수 있는 내용이므로 구현 자체가 아닌 그 안의 숨은 내용들을 공부하고자 합니다. 기능 구현보다는 그 외의 내용들이 더 주가 될 수도 있겠네요 :D

    AVFoundation

    이번 Project1 에서 만들어 볼 과제는 간단한 음악 재생기였습니다. 첫번째 프로젝트부터 음악을 뚝딱 재생시켜주다니 꽤나 놀라운 것 같습니다. 이는 애플에서 이미 이를 위한 프레임워크를 제공해주기 때문에 가능한 일인데요, 이러한 시청각 미디어를 제공해주는 프레임워크가 바로 AVFoundation 입니다.

    AVAudioPlayer

    AVAudioPlayer 는 그중에서 파일이나 메모리에 있는 오디오 데이터를 재생하는 audio player 제공하는 클래스 입니다.

    audio플레이어는 다음과 같은 일들을 할 수 있는데요,

    특정 기간동안 음악을 재생하고

    파일이나 메모리 버퍼에서 재생하고

    반복하고

    동시에 재생하고

    … 등등 와 엄청난 일들을 하는 강력한 녀석이에요. 좋은 녀석인줄 알았으니 이제 어떻게 사용하는지 알아봐야겠죠?

    일단 한번 만들어보죠!

    import AVFoundation
    
    var player: AVAudioPlayer?

    AVFoundation 을 import 해주고 안전하게 옵셔널로 선언까지 잘 해주었어요.

    하지만… 그다음은…? 먼저 재생할 뭐 부터 있어야 재생을 하든 말든 하겠죠?

    음원 Data 가져오기

    AVAudioPlayer 의 초기화를 보면 이렇게 재생할 data 또는 url 을 줘서 초기화를 할 수가 있어요! iOS 7.0 부터는 fileTypeHint 까지 줘서 초기화 할 수가 있겠네요!

    저는 Asset 에 미리 다운받은 음원 파일을 재생할 것이기 때문에 다음과 같은 형태로 하면 될 것 같아요.

    player = AVAudioPlayer(data: data)

    그렇다면 데이터는..?

    Asset 에 있는 음원 파이은 NSDataAsset 으로 가져와서 그 안의 데이터를 뽑아내야 한답니다. 안전하게 guard 문을 통해서 가져와보겠습니다.

    guard let soundAsset: NSDataAsset = NSDataAsset(name: "sound") else { 
        print("음원 파일 에셋을 가져올 수 없습니다.")
      return
    }
    
    // 이런 식으로 위에서 가져온 data 를 추가시켜 초기화 한다.
    // 하지만 그냥 이렇게 한다면 될까요..?
    player = AVAudioPlayer(data: soundAsset.data)

    여기서 주의!!

    위의 AVAudioPlayer 클래스의 정의를 잘 살펴보시면 init 뒤에 throws 가 붙어있는게 보이네요.

    따라서 에러가 발생할 수 있으니 이를 처리해서 안전하게 사용해줘야 합니다!

    저는 try catch 문을 사용하여 처리하였고 에러가 발생하면 에러 메세지 로그를 띄워주도록 작성하였었습니다.

    제가 이부분에서 리뷰어님께 조언을 받았는데요, 예외처리시 단순 로그를 띄워주고 마는 것은 직접적인 해법이 되지 않아 명확한 예외처리가 아닙니다. 단순히 thorws 니까 예외처리를 해야겠다는 생각만 할 줄 알았지, 진정한 예외처리에 대해서는 생각을 해본 적이 없었더라구요.. 그래서 저는 soundAsset 의 데이터를 가져오는 부분에서 발생한 에러이기 때문에, 음원 파일에 문제가 있었을 것이라 생각하고 음원파일을 제대로 확인하라는 alert 창을 띄워서 처리하는 것으로 수정하였습니다.

    사실 data를 가져오는 부분에서 어떤식의 에러가 발생할 수 있을지 제 수준에서는 잘 모르겠더라구요.. 이런식으로 처리하는 것을 리뷰어님이 원하셨던 것이 아닌 것 같은데.. 계속 고민해봐야 할 것 같습니다 ㅠ

    ## Timer

    음악을 재생하면 재생시간을 확인해야겠죠? Swift 는 Timer 객체를 지원합니다! 간단한 사용법을 알아볼까요?

    타이머는 sheduledTimer 에서 설정해줘서 타미어를 사용할 수가 있어요. 잠시 scheduledTimer 의 정의를 보면 다음과 같습니다!

     open class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer

    보시면 아시겠지만 시간의 단위(interval), 반복여부, block 등을 사용할 수 있습니다. block은 interval 이 끝날 때마다 실행됩니다.

    @escaping

    timer 클래스의 block 부분을 보면 @escaping 라는 녀석이 보이네요.

    @escaping 이 붙으면 말 그대로 탈출 클로저라는 뜻으로 함수가 종료된 뒤에도 클로저가 사용될 수 있도록 해주는 녀석이에요.

    @escaping 을 굳이 붙여줘야 한다는 건, 일반적인 클로저는 탈출 불가 클로저라는 뜻이겠죠? 찾아보니 Swift 3 부터 매개변수로 사용되는 클로저는 @noescape 가 기본 값이 되었다고 해요. 따라서 일반적으로 함수가 종료되면 메모리에서 사라져 개발자가 따로 메모리 관리를 해줄 필요가 없게 되었다고 하네요!

    그렇다면 @escaping 은 필요한 경우는 언제일까요?

    • 클로저 저장: 글로벌 변수에 클로저를 저장하여 함수가 종료된 후에도 사용되는 경우
    • 비동기 작업: 비동기 작업을 수행하는 경우, 함수가 종료된 뒤에도 클로저를 메모리에 잡아두어 비동기 작업을 이어가도록 해야하는 경우.

    타이머 클래스가 @escaping 으로 정의되어 있는 것은 비동기 작업을 위함인 것 같습니다. interval 마다 데이터가 새롭게 업데이트 되고, 이 상태가 화면에 업데이트 되는 것과 타이머를 종료 시키는 것이 비동기로 처리가 되어야 하기 때문이죠.

    [unowned self]

    앞서 클로저가 @noescape 가 기본 값이 되어 개발자가 따로 메모리 관리를 해줄 필요가 없게 되었다는 걸 알아봤는데요, 그렇다면 저희는 @escaping 으로 정의를 했으니 메모리에 신경을 써줘야 한다는 의미가 되겠죠.

    self.timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true, block: { [unowned self](timer: Timer) in
    
                ...
            })

    따라서 다음과 같이 block 매개변수 앞에 [unowned self] 를 붙여주었어요. 이는 캡쳐 리스트를 약하게 참조하겠다는 뜻인데요, 쉽게 말하면 저희는 var timer 변수는 scheduledTimer 객체를 강하고 있고 scheduledTimer 도 block 의 클로저를 참조하고 있습니다. 여기서 block 의 클로저를 강하게 참조하게 된다면

    timer 가 해제되어 scheduledTimer 와의 참조를 해제하여도 scheduledTimer 가 block 의 클로저를 강하게 참조하고 있어서 자동으로 해제되지 않고 남아있어 interval 마다 메모리에 클로저가 계속 쌓여서 문제가 발생하겠죠!

    따라서 [unowned self] 를 사용해 미소유로 참조하여서 scheduleTimer 가 해제되면 자신을 참조하는 대상이 사라졌으므로 클로저 또한 자동으로 사라지게 됩니다.

    참조에 대한 부분은 따로 포스팅을 하겠습니다!

    Optional과 예외처리

    제가 구현이 아닌 제대로된 좋은 코드를 짜고자 마음먹고 가장 처음 했던 것은 "!" 을 지우기 였습니다. 값을 무조건 확신할 수 있는 경우에만 "!" 을 사용해야 합니다. 하지만 수 많은 예외가 발생할 가능성이 있는데, 값을 확신한다는 것이 오히려 더 어려운 상황이겠죠. 아직은 이를 판단할 수 있는 실력도 안 되구요. 따라서 저는 아예 "!" 은 사용하지 말자! 고 다짐하였습니다.

    따라서 이번 프로젝트에서도 AVAudioPlayer 와 Timer 객체를 당연히 Optional 로 처리하여 사용하였고, 리뷰어님께서도 이를 Optional로 처리하지 않고 강제캐스팅하여 문제가 발생한 개발자들도 여럿 있으셨다고 리뷰를 달아주시기도 하였습니다.

    하지만 문제는 거기서 끝이였다는 것이죠.. 옵셔널로 처리해 if let 이든, guard 문이든, 예외처리 까지 해준 것은 좋았으나 예외가 발생시 어떻게 처리하냐 에 대해서는 생각을 하지 않았었습니다. 사실 혼자서 개발하고 토이프로젝트를 해보고 하는 과정들에서 예외가 발생할 일들이 그리 많치는 않았기에 자연스럽게 습관적으로 예외처리는 하지만 정말로 예외처리 는 하지 않고 있었던 것이죠.. 이번에도 예외처리를 하고 에러를 print 로 찍어내고, alert 등 처리를 했지만 제가 이 앱을 쓰는 사용자의 입장이라면 알 수 없는 에러 코드자체가 뜬다면 그래서 어쩌라는 건지 당황스럽겠죠..

    따라서 리뷰를 받고 예외처리를 어떻게 해야할까 에러 상황도 찾아보고 했지만 딱 이렇다 할 대답은 찾지 못하긴 하였습니다. print는 찍되 좀 더 구체적으로 어떻게 해야하는지를 명시적으로 남겨 보여주는 정도로 처리하였습니다. 이는 앞으로 개발해나가면서 계속 고민하고 경험해봐야 할 문제인 것 같아요. 중요한 건 "생각" 이라는 걸 하게 되었다는 것 같습니다.


    제대로 코드를 이해하려고 공부를 하고 나니 생각보다 꽤나 공부해야 할 내용들이 많이 담겨있는 것 같습니다. 이렇게 간단간단하게 정리하고 넘어갈 주제들이 아닌 녀석들도 좀 있는 것 같아요. 이러한 주제들은 따로 심도있게 포스팅하며 정리해봐야겠네요. 이렇게 오늘도 북마크만 가득가득 쌓여가는 시간이었네요!!

    긴 글 읽어주셔서 감사합니다!! XD

    'iOS' 카테고리의 다른 글

    MVC 패턴  (2) 2019.08.07
    iOS)BoostCourse) PTJ2 SignUp  (0) 2019.08.01
    iOS) Core Data document 뿌시기 - 1  (0) 2019.07.04
    iOS) Status Bar, Navigation Bar 바꾸기  (0) 2019.06.07
    iOS) Core Data Tutorial  (0) 2019.05.13

    댓글

Designed by Tistory.