[SwiftUI] AVKit - VideoPlayer로 앱에 동영상 넣기

양재현·2026년 2월 25일

오늘은 앱에 동영상을 넣기 위해 VideoPlayer를 사용해 볼 것이다.
추가로 나는 온보딩 화면에 영상을 넣고 싶어서 그 부분도 구현해 볼 것이다.

애플 공식문서에 아주 간단한 예제가 있어서 바로 이해 가능할 것이다.

VideoPlayer 예제

import SwiftUI
import AVKit

struct VideoView: View {
    @State private var player: AVPlayer? //1
    @State private var isPlaying = false

    var body: some View {
        VStack {
            if let player {
           		 //2
                VideoPlayer(player: player)
                    .frame(width: 320, height: 180, alignment: .center)

				//3
                Button {
                    isPlaying ? player.pause() : player.play()
                    isPlaying.toggle()
                    player.seek(to: .zero)
                } label: {
                    Image(systemName: isPlaying ? "stop" : "play")
                        .padding()
                }
            }
        }
        //4
        .task {
            let url = URL(string: "여기에 url 넣기")!
            player = AVPlayer(url: url)
        }
    }
}


#Preview {
    VideoView()
}

주요 포인트는 4가지다.

  1. AVPlayer를 옵셔널로 뒀는데 AVPlayer 인스턴스를 선언 시점이 아닌 task에서 간접적으로 생성하는 방식을 사용한다.
    그 이유는 성능 문제나 기타 예기치 않은 부작용(Side effects)을 방지하는 데 도움이 되기 때문이다.

  2. VideoPlayer 객체를 통해 비디오를 띄울 곳이다. frame 모디파이어를 통해 크기 조절이 가능하다.

  3. pause()play() 는 각각 영상을 멈추고 재생하는 메서드고, seek(to: .zero) 이 친구는 영상을 0초 지점으로 되돌리는 메서드다.

  4. 비디오 플레이어 생성을 뒤로 미루기 위해 task 모디파이어를 사용한다. SwiftUI가 해당 뷰를 처음으로 나타낼 때, 딱 한 번만 플레이어를 생성하도록 보장하기 위함이다.
    그리고 넣고 싶은 영상은 로컬 or 리모트 둘 다 url로 넣어주면 된다.

온보딩에서 VideoPlayer

앞에서 동영상을 뷰에 구현하는 것은 간단했다. 그러나 나는 온보딩 뷰에서 각각의 튜토리얼 동영상을 넣고싶고, 각각의 영상은 계속 반복되게 하고 싶다.

그러기 위해서는 3가지 객체를 알아야 한다.
바로 AVPlayerItem, AVQueuePlayer, AVPlayerLooper 이다.

1. AVPlayerItem : 이 친구는 영상(item) 하나 하나를 담고 있는 객체다.
2. AVQueuePlayer : 이 친구는 여러 영상(item)을 재생하는 플레이어다.
3. AVPlayerLooper : 특정 영상(item)을 계속 queue에 추가해 반복 구현 해주는 객체다.

비유하자면

AVPlayerItem → 재생할 노래파일 (MP3 파일)
 ↓
AVQueuePlayer → 플레이 리스트 (노래가 끝나면 다음 노래로 자동으로 넘어감)
 ↓
AVPlayerLooper → "한 곡 반복" 버튼 (재생 목록에 노래가 끝나면, 알아서 그 노래를 다시 재생 목록에 추가해줌)

음악 앱을 생각해보면 된다.

온보딩 예제

import SwiftUI
import AVKit

struct OnboardingView: View {
    @State private var currentPage = 0
    
    // 제미나이가 추천해준 url 샘플
    let videoUrls = [
        // 1. 토끼 애니메이션 (Big Buck Bunny)
        URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"),
        
        // 2. 코끼리 꿈 애니메이션 (Elephants Dream)
        URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"),
        
        // 3. 오픈 소스 영화 (For Bigger Blazes)
        URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4")
    ].compactMap { $0 }
    
    var body: some View {
        TabView(selection: $currentPage) {
            ForEach(0..<videoUrls.count, id: \.self) { index in
                OnboardingVideoCell(
                    videoURL: videoUrls[index],
                    isCurrentPage: currentPage == index
                )
                .tag(index)
            }
        }
        .tabViewStyle(.page(indexDisplayMode: .always))
        .ignoresSafeArea()
    }
}

먼저 온보딩 뷰에서는 제미나이한테 추천받은 url 샘플 3개와 함께 탭뷰로 3개의 페이지를 만들었다.

struct OnboardingVideoCell: View {
    let videoURL: URL
    let isCurrentPage: Bool
    
    //1
    @State private var player: AVQueuePlayer?
    @State private var looper: AVPlayerLooper?
    
    var body: some View {
        ZStack {
            if let player = player {
                VideoPlayer(player: player)
                    .disabled(true) // 재생 컨트롤 숨김
                    .aspectRatio(contentMode: .fill)
            }
        }
        //2
        .task {
            setupPlayer()
        }
        //3
        .onChange(of: isCurrentPage) { _, newValue in
            newValue ? player?.play() : player?.pause()
        }
        //4
        .onDisappear {
            player?.pause() // 화면에서 완전히 사라질 때 정지
        }
    }
    
    //5
    private func setupPlayer() {
        // 1) 중복 생성 방지: 이미 있으면 또 만들지 않고 재생만 확인
        guard player == nil else {
            if isCurrentPage { player?.play() }
            return
        }

        // 2) 플레이어 아이템(동영상) -> 큐플레이어(플레이리스트) -> 플레이어 루퍼(무한재생 도우미)
        let item = AVPlayerItem(url: videoURL)
        let queuePlayer = AVQueuePlayer(playerItem: item)
        looper = AVPlayerLooper(player: queuePlayer, templateItem: item)

        // 3) 할당 및 재생
        self.player = queuePlayer
        if isCurrentPage { queuePlayer.play() }
	}
}
  1. AVQueuePlayerAVPlayerLooper를 옵셔널로 둬서 .task에서 간접적으로 생성 해준다.
  2. 뷰가 뜨자마자 플레이어를 설정(setupPlayer)한다. 만약 영상을 다 불러오기 전에 사용자가 화면을 넘겨버리면, 진행 중이던 로딩 작업을 알아서 취소해 리소스를 아낀다.
  3. 페이지 번호를 지켜보고 있다가, 현재 페이지가 활성화되면(newValue == true) 재생, 옆으로 넘기면 즉시 정지시킨다.
  4. 온보딩이 끝나고 메인 화면으로 이동했을 때, 메모리엔 남아있던 플레이어가 갑자기 소리를 내는 버그를 방지한다.
  5. 1) 이미 현재 페이지에 비디오가 있으면 중복으로 생성하는 걸 방지한다.
    2) AVPlayerItem 로 동영상 하나를 담아두고 AVQueuePlayer 대기열에 추가시켜준다. 그 후 AVPlayerLooper로 플레이 리스트 안에 내가 지정한 영상 하나를 무한 반복시켜주는 것이다.
    3) 영상을 할당해주고, isCurrentPage일 때만 틀어서 다른페이지와 소리가 겹치는 걸 방지한다.

마무리

오늘은 VideoPlayer로 앱에 동영상 넣는 예제를 해보았는데, 기억에 남는거라면 AVQueuePlayer 부분이다. Queue(큐)는 CS에서 나오는 개념일텐데 아무래도 다들 CS를 중요하게 생각하는 이유가 이런부분에서 배경지식이 있다면 쉽게 원리를 이해할 수 있어서가 아닐까 싶다. CS를 개발과 구분짓지말고 엮어서 볼 수 있는 시각을 길러나가야겠다.

🍎 참고

https://developer.apple.com/documentation/avkit/videoplayer

0개의 댓글