HP Trivia (2)
Audio
이제 이전 글에서 구현한 transition animation 효과에 맞춰 사운드를 재생시켜, 유저에게 시각뿐 아니라 청각적으로도 몰입감을 줄 수 있도록 해보려 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import AVKit
@State private var audioPlayer: AVAudioPlayer!
.onAppear {
animateViewsIn = true
playAudio()
}
private func playAudio() {
let sound = Bundle.main.path(forResource: "magic-in-the-air", ofType: "mp3")
audioPlayer = try! AVAudioPlayer(contentsOf: URL(filePath: sound!))
audioPlayer.numberOfLoops = -1
audioPlayer.play()
}
이 코드가 새롭게 추가되었다.
AVKit 프레임워크를 import해야 오디오 기능을 사용할 수 있다.
핵심은 playAudio()
함수이다.
여기서 오디오 파일 경로를 찾고, AVAudioPlayer 인스턴스를 생성하여 반복 재생 설정 후 재생을 시작한다.
numberOfLoops Docs에 따르면:
- 0: 사운드를 한 번만 재생 (기본값)
- 양수: 해당 값만큼 반복 재생 (예: 1 → 총 2번 재생)
- 음수: 무한 반복 재생 (stop() 호출 시까지 반복)
AVAudioPlayer! 왜 !를 썼을까?
AVAudioPlayer는 클래스이므로 참조 타입이며, 선언 시점에 인스턴스를 생성하지 않아도 된다.
하지만 여기서는 !를 붙여 IUO(Implicitly Unwrapped Optional)로 선언했다.
이유를 정리하면 다음과 같다:
- AVAudioPlayer()와 같은 빈 생성자는 존재하지 않음
- 생성 시 반드시 오디오 파일 경로가 필요
- 따라서 선언만 먼저 하고, 나중에 초기화하는 방식이 필요함
- 일반 옵셔널(?)을 사용하면 이후 코드에서 바인딩 또는 언래핑 코드가 필요해짐
- !을 사용하면 마치 일반 변수처럼 접근할 수 있어 코드가 간결해짐
즉, 초기에는 nil이지만 이후 반드시 값이 들어온다는 전제를 둘 수 있을 때 IUO 방식이 간결하고 실용적일 수 있다.
IUO (Implicitly Unwrapped Optional)
String!처럼 !가 붙은 타입은 초기에는 nil일 수 있지만, 나중에는 반드시 값이 있다고 가정하고 사용하는 타입이다.
옵셔널 바인딩 없이도 일반 변수처럼 사용할 수 있는 것이 특징이다.
예시:
1
2
3
4
5
6
var name: String! // IUO 선언
name = "Potter"
print("Welcome, \(name)") // Welcome, Optional("Potter")
print("Welcome, \(name!)") // Welcome, Potter
문자열 보간 시에는 여전히 옵셔널로 처리되기 때문에 .name!처럼 명시적으로 언래핑해야 Optional(…) 없이 출력된다.
만약 값을 할당하지 않으면?
1
2
3
4
var name: String!
print("Welcome, \(name)") // Welcome, nil
print("Welcome, \(name!)") // 런타임 에러: Unexpectedly found nil while unwrapping an Optional value
즉, 언래핑 시점에 값이 없다면 크래시 발생 가능성 있음.
일반 옵셔널(?)과 비교
1
2
3
4
5
6
7
8
9
var nameOptional: String? // 일반 옵셔널
nameOptional = "Harry"
print("Welcome, \(nameOptional)") // Welcome, Optional("Harry")
if let unwrapped = nameOptional {
print("Welcome, \(unwrapped)") // Welcome, Harry
}
일반 옵셔널은 안전성은 높지만 바인딩 코드가 필요하고, IUO는 간결하지만 안전하지 않을 수 있음
AVAudioPlayer 인스턴스 주입 확인
1
2
3
4
5
6
7
8
9
10
11
private func playAudio() {
let sound = Bundle.main.path(forResource: "magic-in-the-air", ofType: "mp3")
print("before: \(audioPlayer)")
print("before Type: \(type(of: audioPlayer))")
print("---")
audioPlayer = try! AVAudioPlayer(contentsOf: URL(filePath: sound!))
print("after: \(audioPlayer)")
print("after Type: \(type(of: audioPlayer))")
audioPlayer.numberOfLoops = -1
audioPlayer.play()
}
이때 콘솔 출력 결과는 다음과 같다:
1
2
3
4
5
before: nil
before Type: Optional<AVAudioPlayer>
---
after: Optional(<AVAudioPlayer: 0x6000002393a0>)
after Type: Optional<AVAudioPlayer>
즉, audioPlayer는 초기에는 nil이었지만, playAudio() 호출 이후에는 인스턴스가 할당되어 정상 작동한다.
이럴때 path 설정이 잘못된다면?
1
2
3
4
5
6
7
8
9
10
11
private func playAudio() {
let sound = Bundle.main.path(forResource: "magic-in-the-air", ofType: "mp4") // mp3 -> mp4로 변경
print("before: \(audioPlayer)")
print("before Type: \(type(of: audioPlayer))")
print("---")
audioPlayer = try? AVAudioPlayer(contentsOf: URL(filePath: sound ?? "/dev/null"))
print("after: \(audioPlayer)")
print("after Type: \(type(of: audioPlayer))")
//audioPlayer.numberOfLoops = -1
//audioPlayer.play()
}
이렇게 일부러 잘못된 값을 넣어준다. (?? 뒤에도 잘못된 경로를 일부러 넣어준다.)
1
2
3
4
5
before: nil
before Type: Optional<AVAudioPlayer>
---
after: nil
after Type: Optional<AVAudioPlayer>
하지만
1
2
audioPlayer.numberOfLoops = -1
audioPlayer.play()
둘중 하나라도 주석을 풀게되면
에러가 발생!
즉 !를 쓸때는 이렇게 오타같은 실수를 하게되면 바로 앱이 충돌이 발생하니 주의하자.
번외: 옵셔널 바인딩으로 앱 안정성 높이기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Exceptional Case
private func playAudio() {
let sound = Bundle.main.path(forResource: "magic-in-the-air", ofType: "mp4")
audioPlayer = try? AVAudioPlayer(contentsOf: URL(filePath: sound ?? "/dev/null"))
guard let audioPlayer = audioPlayer else {
print("audioPlayer가 nil입니다. 파일 경로를 확인하세요.")
return
}
audioPlayer.numberOfLoops = -1
audioPlayer.play()
}
// 출력
audioPlayer가 nil입니다. 파일 경로를 확인하세요. // 하지만 앱은 정상실행 (단지 소리만 나오지 않음.)
// Normal Case
private func playAudio() {
let sound = Bundle.main.path(forResource: "magic-in-the-air", ofType: "mp3")
audioPlayer = try? AVAudioPlayer(contentsOf: URL(filePath: sound ?? "/dev/null"))
guard let audioPlayer = audioPlayer else {
print("audioPlayer가 nil입니다. 파일 경로를 확인하세요.")
return
}
audioPlayer.numberOfLoops = -1
audioPlayer.play()
}
이렇게 해주면 조금 앱 개발에 있어 안정적인 처리가 가능하다. 즉,
- 실전에서는 아래처럼
guard let
으로 옵셔널 바인딩 처리 시- 경로 오류 발생 시 앱이 crash 나지 않고, 사용자에게 안내 메시지를 출력하거나 fallback 처리 가능
결론
@State private var audioPlayer: AVAudioPlayer!
는 초기에는nil
playAudio()
호출 시 정상적인AVAudioPlayer
인스턴스로 초기화됨- 이후에는 일반 변수처럼
audioPlayer.play()
등으로 직접 사용 가능 - SwiftUI의
@State
와 IUO의 결합은, 초기값 없이도 상태값을 나중에 안전하게 주입하는 패턴으로 자주 사용됨 - 단, 타입은 여전히
Optional<AVAudioPlayer>
형태로 유지되며,IUO는 내부적으로 옵셔널 타입으로 존재하고, 단지 문법상 편의를 제공할 뿐 실제로는 옵셔널 바인딩된 값이다
예를 들어, 콘솔에서 type(of: audioPlayer)
를 출력하면
Optional<AVAudioPlayer>
로 나오는 이유가 이 때문이다.
이미지는 참고(audioPlayer를 Option 누른상태에서 클릭했을때)