Search
audio 태그와 자동 재생
9 min read

audio 태그와 자동 재생

브라우저의 Autoplay 정책으로 인해 발생하는 오디오 재생 차단 문제의 원인을 알아본다


서론

최근 모의고사 서비스에서 '듣기 평가 자동 재생' 기능을 구현할 일이 있었다. 시험 시작을 누르면 문제가 순차적으로 넘어가면서 오디오 트랙도 자연스럽게 자동 재생되어야 하는 요구사항이었다.

"단순히 <audio> 태그에 autoplay 속성을 주거나, 컴포넌트가 마운트될 때 play()를 호출하면 되겠지?"

하지만 생각처럼 동작하지 않았다. 브라우저 콘솔에는 붉은색 에러가 출력되며 첫 재생이 강제로 차단되었다. 오디오나 비디오 다루어 본 프론트엔드 개발자라면 한 번쯤 마주쳤을 브라우저 권한(Autoplay Policy) 문제였다.

이 글에서는 이 문제가 왜 발생하는지 알아보고, 어떻게 권한을 우회하고 문제를 해결했는지 정리해 본다.

문제 상황

가장 일반적인 형태의 실수부터 살펴보자. 페이지 접속 직후나 렌더링 직후에 스크립트를 통해 오디오를 자동 재생하려고 하면 십중팔구 에러를 만나게 된다.

<audio id="myAudio" src="listen.mp3"></audio>
<script>
  //  페이지 로드 직후 재생 시도
  document.getElementById("myAudio").play();
</script>

위 코드를 실행하면 재생은 되지 않고, 콘솔에 다음과 같은 에러가 발생한다.

NotAllowedError: play() failed because the user didn't interact with the document first.

문장을 해석해보면 사용자가 document와 상호작용 하지 않아 실패했다고 한다. 도대체 왜 이러는 걸까?

브라우저의 Autoplay 정책

이 이슈의 핵심은 브라우저의 자동 재생 정책에 있다.

MDN 문서를 보면 그 이유가 명확히 적혀 있다.

"Automatically starting the playback of audio (or videos with audio tracks) immediately upon page load can be an unwelcome surprise to users." (페이지가 로드되자마자 오디오(또는 오디오 트랙이 있는 비디오)의 재생을 자동으로 시작하는 것은 사용자에게 불쾌한 놀라움이 될 수 있습니다.)

과거, 페이지에 접속하자마자 사용자의 의도와 무관하게 광고 소리나 배경음악이 울려 퍼져 불편함을 겪는 일이 잦았다. 이를 방지하기 위해 모던 브라우저들은 엄격한 정책을 도입했다.

실제 Chrome 개발자 문서에 명시된 자동 재생 정책(Autoplay policies)은 다음과 같다.

Muted autoplay is always allowed. Autoplay with sound is allowed if: The user has interacted with the domain (click, tap, etc.). (음소거된 자동 재생은 항상 허용됩니다. / 소리가 있는 자동 재생은 사용자가 도메인과 상호작용(클릭, 탭 등)한 경우에 허용됩니다.)

즉, 사용자의 명시적인 클릭이나 터치 이벤트 흐름 안에서 재생 함수가 호출되어야만 브라우저가 이를 '정당한 재생 요청'으로 인정해 준다. 듣기 평가에서 음소거(muted) 상태로 자동 재생을 할 수는 없으니, 소리가 포함된 미디어를 재생하려면 반드시 사용자 상호작용(User Interaction)이 선행되어야 했던 것이다.

해결

해결의 핵심은 재생 권한 획득 타이밍이다. 임의의 타이밍(로드 직후, useEffect 등)에 강제로 재생하는 대신, 사용자가 화면의 무언가를 '클릭'하는 순간의 흐름 속에서 play()를 호출해야 한다.

1. 기본 HTML/JS에서의 해결책

가장 단순하고 확실한 방법은, 페이지 내에 "시작" 버튼을 두고 그 버튼의 클릭 이벤트 리스너 안에서 play()를 호출하는 것이다.

<audio id="myAudio" src="listen.mp3"></audio>
<button id="startBtn">듣기 평가 시작하기</button>
 
<script>
  const audio = document.getElementById("myAudio");
  const btn = document.getElementById("startBtn");
 
  //  사용자의 '클릭' 이벤트 흐름 안에서 직접 play() 호출 (권한 획득)
  btn.addEventListener("click", () => {
    audio.play().catch((error) => {
      console.warn("재생 실패:", error);
    });
  });
</script>

2. React에서의 해결책 (권한 위임하기)

React 환경에서도 마찬가지로 Ref를 이용하여 사용자 상호작용과 재생을 연결하면 된다. 이를테면 시험 시작 버튼을 누름과 동시에 재생시키는 방법이 있겠다.

"use client";
 
import {
  forwardRef,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from "react";
 
export interface AudioPlayerHandler {
  play: () => Promise<void> | undefined;
  pause: () => void;
}
 
const MockExamAudioPlayer = forwardRef<AudioPlayerHandler, Props>(
  ({ questions, isExamStarted }, ref) => {
    const audioRef = useRef<HTMLAudioElement | null>(null);
    const [currentIdx, setCurrentIdx] = useState<number>(0);
 
    // 부모 컴포넌트에서 사용자 클릭 시 직접 호출할 수 있도록 메서드 노출
    useImperativeHandle(ref, () => ({
      play: () => audioRef.current?.play(),
      pause: () => audioRef.current?.pause(),
    }));
 
    // ...
 
    return (
      <audio
        ref={audioRef}
        src={questions[currentIdx].audio}
        onEnded={() => handleNextAudio()}
      />
    );
  }
);
 
export default MockExamAudioPlayer;

위와 같이 실행과 관련된 함수를 외부로 노출시키고 부모 컴포넌트에서 사용자의 클릭 이벤트와 직접 연결해주면 된다.

function ExamPage() {
  const playerRef = useRef<AudioPlayerHandler>(null);
 
  const handleStartExam = () => {
    startExamLogic(); // 시험 시작 상태 변경 등
    // 💡 사용자의 '클릭' 이벤트 흐름 안에서 직접 play() 호출!
    playerRef.current?.play();
  };
 
  return (
    <div>
      <button onClick={handleStartExam}>시험 시작하기</button>
      <MockExamAudioPlayer
        ref={playerRef}
        questions={questions}
        isExamStarted={isStarted}
      />
    </div>
  );
}

이렇게 처리하면 브라우저는 오디오 재생 요청이 사용자의 자발적인 상호작용에서 비롯되었다고 판단하여 차단하지 않는다. 또 한 번 재생 권한을 획득하고 나면, 이후 오디오가 끝나고(onEnded) 다음 트랙으로 넘어가며 재생되는 동작은 제약 없이 매끄럽게 이루어진다.

주의점

참고자료