본문 바로가기
iOS

[Swift] MusicKit으로 음악 검색기능 구현하기 - 2

by 워뇨옹2 2025. 6. 29.
728x90
반응형

MusicKit으로 음악 검색하기

지난 포스팅에서는 MusicKit이 어떻게 구성되어있는지 애플 개발자 공식 문서를 살펴보았다.

 

이번에는 MusicKit을 통해 실제로 음악, 가수, 플레이리스트 등을

검색하고 데이터를 받아오는 부분을 어떻게 구현하는지 알아보자.

목차(필요한 부분 검색해서 보세요)

  • Setting up - MusicKit을 사용하기 이한 기본 설정
  • MusicCatalogSearch - 음악 목록 검색
  • MusicCatalogSearchSuggestion - 검색어 자동완성
  • MusicCatalogResourceRequest - 특정 음악 조회

Setting up - MusicKit기본 설정

지난 포스팅에 작성해두었지만, 다시 한 번 알아보자

MusicKit 프레임워크를 사용하기 위해서는

사용자에게 권한을 요청해야한다.

info.plist에서 NSAppleMusicUsageDescription

권한을 등록한 뒤에

간단하게 앱 진입점(ContentView or AppRoot)혹은

MusicKit을 액세스 하는 부분에서

다음과 같은 코드로 진행할 수 있다.

import Swiftui
import MusicKit

struct ContentView: View {
	var body: some View {
    	VStack {
        	/// Your View
        }
        .task {
	        await MusicAuthorization.request()
        }
    }
}

먼저 설정한 뒤에 다음으로 넘어가자

MusicCatalogSearch - 음악 목록 검색

가장 먼저 기본적인 검색 기능부터 구현해보도록 하자.

검색은 MusicCatalogSearchRequest/Response를 통해

진행할 수 있다.

 

어렵지 않으니 먼저 코드를 보도록 하자.

Code

func search(term: String) async throws -> MusicItemCollection<Song> {
    var request = MusicCatalogSearchRequest(term: term, types: [Song.self])
    request.limit = 20
    request.offset = 0
    let response = try await request.response()

    return response.songs
}

아주 간단히 이 코드면 검색어에 따른 음악을 검색할 수 있다.

limit은 설정하지 않을 경우 기본값은 5이며 최대 20까지 설정할 수 있다.

offset은 1부터 시작하는게 아니라 0부터 시작하니 알아두자.

설정하지 않을 경우 기본값은 0이다.

 

또, MusicCatalogSearch는 음악만 검색할 수 있는게 아니라

가수, 앨범, 플레이리스트 등 여러가지 형태의 결과를 검색할 수 있다.

 

위 코드는 현재 request는 types에 Song 타입만 포함하고

있는 것을 볼 수 있다.

 

만약 다른 타입의 MusicItem도 검색하고 싶다면 아래처럼 작성할 수도 있다.

func search(term: String) async throws -> (MusicItemCollection<Song>, MusicItemCollection<Artist>, MusicItemCollection<Playlist>) {
    var request = MusicCatalogSearchRequest(term: term, types: [Song.self, Artist.self, Playlist.self])
    request.limit = 20
    request.offset = 0
    let response = try await request.response()

    return (response.songs, response.artists, response.playlists)
}

혹은, Response객체 자체를 넘겨 함수를 호출한 부분에서

데이터를 처리할 수도 있다.

func search(term: String) async throws -> MusicCatalogSearchResponse {
    var request = MusicCatalogSearchRequest(term: term, types: [Song.self, Artist.self, Playlist.self])
    request.limit = 20
    request.offset = 0
    return try await request.response()
}

types에 원하는 MusicItem 타입만 넣어주면

아주 손쉽게 검색을 구현할 수 있다.

MusicCatalogSearchSuggestion - 검색어 자동완성

검색어 자동완성은 말 그대로 사용자가 입력한

검색어에 따라 추천 검색어와 현재 입력된

쿼리를 기반으로 추천 검색 결과를 반환해준다.

현재 프로젝트에 도입한 자동완성인데, 이것만 추가해도 

앱이 한 층더 세련되어 보이는 느낌이 든다.

 

응답도 굉장히 빠르고 구현이 어렵지도 않으니

만약 앱에 음악 검색 기능을 도입한다면

자동완성을 추가하지 않을 이유는 없어보인다.

Code

func getSuggestions(term: String) async throws -> ([MusicCatalogSearchSuggestionsResponse.Suggestion], MusicItemCollection<MusicCatalogSearchSuggestionsResponse.TopResult>) {
    let request = MusicCatalogSearchSuggestionsRequest(term: term, includingTopResultsOfTypes: [Song.self, Artist.self])
    let response = try await request.response()

    let suggestions = response.suggestions
    let topResults = response.topResults

    return (suggestions, topResults)
}

위와 같이 구현할 수 있다.

혹은 검색과 마찬가지로 response 자체를 리턴해서

response를 함수 호출부에서 다루는 방법도 있다.

 

suggestions는 다음과 같은 프로퍼티들을 가진다.

displayTerm과 searchTerm을 String 형태로 가지는데,

내가 직접 사용했을 때는 둘이 같은 값을 가졌다.

 

한국어라 그런가? 해서 영어로도 해보고 다른 언어로도

해봤는데, 두 값이 다른 경우는 아직 발견하지 못했다.

 

어쨋든 의도 자체는 displayTerm은 UI상에 표현하고

해당 term을 선택해 검색할 때는 searchTerm으로

검색하라는 의도같다.

 

본인은 그냥 하라는대로 나눠서 구현하고 있지만,

둘중 어떤걸 사용해도 문제는 없어보인다.

TopResults

카탈로그를 다루다 보면 top result가 굉장히 많이

언급되는데, 자동완성도 마찬가지로 top result를 

포함할 수 있다.

 

현재까지 입력된 쿼리에서 정확도가 높은 MusicItem들을

확인할 수 있다. 

 

MusicCatalogSearch에서와 마찬가지로 원하는 타입들을

includingTopResultsOfTypes에 배열 형태로 추가하면

response.topResults에서 받아볼 수 있다.

 

다만, topResults는 내부적으로 MusicItem들을 프로퍼티로

가지고 있는게 아니라, TopResult 자체가 MusicItem을 래핑한

개념이라고 보면 된다.

 

따라서 topResults.songs, topResults.artists 같은 형태로

받아오는게 아니라, 다음과 같은 방식으로 MusicItem을 가져와야한다.

let (_, topResults) = try await getSuggestions(term: term)
for topResult in topResults {
    switch topResult {
    case .album(let album):
    	// Processing
    case .song(let song):
    	// ...
    case ...other cases:
    default:
    }
}

이 정도만 구현할 수 있다면 자동완성 기능도 

충분히 도입할 수 있다.

MusicCatalogResourceRequest - 특정 음악 조회

이제 제너럴하게 검색을 하는 것이 아니라,

특정 음악의 데이터를 가져오는 방법에 대해 알아보자.

 

좀 더 풀어서 말하자면,

데이터베이스에 MusicItem 데이터 전체를 저장하지

않고, id만 저장하여 추후에 해당 아이디로

데이터를 받아와 음악의 메타데이터 및 재생 데이터를

가져올 때 사용한다고 볼 수 있다.

 

먼저 코드를 보면 다음과 같은 형태로 사용한다.

Code

func getSong(id: String) async throws -> Song? {
    var request = MusicCatalogResourceRequest<Song>(matching: \.id, equalTo: MusicItemId(id))
    
    let response = try await request.response()

    guard let musicItem = response.items.first else {
        print("No music item found for id: \(id)")
        return nil
    }
    return musicItem
}

단일 아이템 조회

func getSong(ids: [String]) async throws -> MusicItemCollection<Song> {
    let musicItemIds = ids.map { MusicItemId($0) }
    var request = MusicCatalogResourceRequest<Song>(matching: \.id, memberOf: musicItemIds)
    
    let response = try await request.response()

    return response.items
}

여러 아이템 조회

 

생성자에 제너릭 타입을 추가하는데,

이 타입에 따라 id 혹은 isrc 두가지 키를 

사용할 수 있다.

Song, MusicVideo의 경우 isrc 키도 사용할 수 있으며

나머지 타입들은 id만 사용 가능하다.

 

728x90
반응형