πŸ’₯μž„μ˜ src둜 λ―Έλ””μ–΄ μžλ™μž¬μƒ μš°νšŒν•˜κΈ°

AromahyangΒ·2024λ…„ 9μ›” 15일
0

Frontend

λͺ©λ‘ 보기
5/5
post-thumbnail

λ―Έλ””μ–΄ μžλ™μž¬μƒμ΄ μ•ˆλœλ‹€

μƒˆλ‘œμš΄ ν”„λ‘œμ νŠΈλ₯Ό λ§Œλ“œλŠ”λ°, μ˜€λ””μ˜€λ₯Ό μžλ™μž¬μƒν•˜λŠ” κΈ°λŠ₯이 λ“€μ–΄κ°„λ‹€.
즉, μŒμ†Œκ±°κ°€ μ•„λ‹Œ μƒνƒœμ—μ„œ μ˜€λ””μ˜€κ°€ μžλ™μž¬μƒλ˜μ–΄μ•Ό ν•œλ‹€.
λ°μŠ€ν¬νƒ‘ 크둬과 λͺ¨λ°”일 크둬, λ°μŠ€ν¬νƒ‘ μ‚¬νŒŒλ¦¬μ—μ„œλŠ” 잘 λ™μž‘ν•˜λŠ”λ°, λͺ¨λ°”일 μ‚¬νŒŒλ¦¬μ—μ„œ ν•΄λ‹Ή κΈ°λŠ₯이 λ™μž‘ν•˜μ§€ μ•Šμ•˜λ‹€.
λͺ¨λ°”일 μ‚¬νŒŒλ¦¬μ—μ„œ λ„μš΄ μ—λŸ¬λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

Unhandled Promise Rejection: NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.

이번 ν”„λ‘œμ νŠΈμ—μ„œ κ°€μž₯ μ€‘μš”ν•œ κΈ°λŠ₯μ΄λΌμ„œ(😱) μ–΄λ–»κ²Œλ“  λŒμ•„κ°€κ²Œ ν•΄μ•Ό ν•΄μ„œ 꼼수 λΆ€λ¦΄μš°νšŒν•  방법에 λŒ€ν•΄ κ³ λ―Όν•΄λ³΄μ•˜λ‹€.

πŸ’‘ tl;dr
1. λΈŒλΌμš°μ €λ§ˆλ‹€ λ―Έλ””μ–΄ μžλ™μž¬μƒ 정책이 λ‹€λ₯΄λ―€λ‘œ 정책을 잘 μ•Œμ•„λ³΄μž.
2. μ‚¬μš©μž 제슀처의 직접적인 결과둜 λ―Έλ””μ–΄ μžλ™μž¬μƒν•˜λŠ” 것은 κ°€λŠ₯ν•˜λ‹€.
3. 클릭 ν˜Ήμ€ submit μ΄λ²€νŠΈκ°€ λ°œμƒν•˜μžλ§ˆμž μž„μ˜ μ˜€λ””μ˜€λ₯Ό μŒμ†Œκ±°λ‘œ μž¬μƒν•˜κ³ , μ‹€μ œ μž¬μƒλ˜μ–΄μ•Ό ν•  srcλ₯Ό λ°›μœΌλ©΄ μ˜€λ””μ˜€μ˜ srcλ₯Ό ꡐ체 및 μŒμ†Œκ±° ν•΄μ œν•œ ν›„ μž¬μƒν•˜μ—¬ ν•΄κ²°ν–ˆλ‹€.

λ―Έλ””μ–΄ μžλ™μž¬μƒ 상황

이번 ν”„λ‘œμ νŠΈλŠ” μ‚¬μš©μžκ°€ νŠΉμ • λ²„νŠΌμ„ ν΄λ¦­ν•˜κ±°λ‚˜ μž…λ ₯값을 Submitν•˜λ©΄ API와 ν†΅μ‹ ν•˜κ³  μ‘λ‹΅μ˜ κ²°κ³Όλ₯Ό 보여쀀 ν›„ μ˜€λ””μ˜€κ°€ μžλ™μž¬μƒλ˜λŠ” ν”Œλ‘œμš°μ΄λ‹€.
즉, μ‚¬μš©μžκ°€ 직접 μ˜€λ””μ˜€λ₯Ό μž¬μƒν•˜λŠ” 상황이 μ•„λ‹ˆλ©° API의 κ²°κ³Όκ°€ λ Œλ”λ§λœ ν›„ μžλ™μž¬μƒλ˜λ„λ‘ μ½”λ“œλ₯Ό μž‘μ„±ν•˜μ˜€λ‹€.

λΈŒλΌμš°μ € μ •μ±…

λ―Έλ””μ–΄ μžλ™μž¬μƒμ— λŒ€ν•œ λΈŒλΌμš°μ € 정책을 μ°Έκ³ ν•΄μ„œ μš°νšŒν•  방법이 μžˆλŠ”μ§€ κ³ λ―Όν•΄λ³΄μž.

λ¨Όμ €Β MDN으둜 autoplay ν—ˆμš© 쑰건을 μ°Ύμ•„λ΄€μŠ΅λ‹ˆλ‹€. λ¬Έμ„œμ— λ”°λ₯΄λ©΄ μ•„λž˜ 쑰건 쀑 적어도 ν•˜λ‚˜λ₯Ό μΆ©μ‘±ν•΄μ•Ό autoplayλ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

  • μ˜€λ””μ˜€κ°€ μŒμ†Œκ±° μƒνƒœκ±°λ‚˜ λ³Όλ₯¨μ΄ 0으둜 섀정될 것
  • μ‚¬μš©μžκ°€ μ‚¬μ΄νŠΈμ™€ μƒν˜Έμž‘μš© (클릭, νƒ­, ν‚€ λˆ„λ₯΄κΈ° λ“±) ν•  것
  • μ‚¬μ΄νŠΈκ°€ autoplay ν—ˆμš© μ‚¬μ΄νŠΈμΌ 것 (λΈŒλΌμš°μ €κ°€ μžλ™μœΌλ‘œ λ“±λ‘ν•˜κ±°λ‚˜ μ‚¬μš©μžκ°€ μˆ˜λ™μœΌλ‘œ μ‚¬μ΄νŠΈμ— autoplayλ₯Ό ν—ˆμš©ν•˜λŠ” 경우)
  • Permissions Policyλ₯Ό μ‚¬μš©ν•΄ <iframe>κ³Ό λ„νλ¨ΌνŠΈμ— autoplay κΆŒν•œμ„ λΆ€μ—¬ν•˜λŠ” 경우

좜처: React Safari μ˜€λ””μ˜€ μž¬μƒ νŠΈλŸ¬λΈ” μŠˆνŒ…

MDN λ¬Έμ„œλŒ€λ‘œλΌλ©΄ 클릭과 submit처럼 μ‚¬μš©μžμ™€ μ‚¬μ΄νŠΈμ˜ μƒν˜Έμž‘μš©μ΄ 있기 λ•Œλ¬Έμ— μžλ™μž¬μƒμ΄ λ˜μ–΄μ•Ό ν•œλ‹€.
κ·Έλž˜μ„œ ν¬λ‘¬μ—μ„œλŠ” λ¬Έμ œκ°€ μ—†μ—ˆλ˜ κ²ƒμœΌλ‘œ 보인닀.
λΈŒλΌμš°μ €λ§ˆλ‹€ λ―Έλ””μ–΄ μžλ™μž¬μƒ 정책이 λ‹€λ₯Έ κ²ƒμœΌλ‘œ μΆ”μ •λ˜μ–΄ 쑰금 더 μ°Ύμ•„λ³΄μ•˜λ‹€.

A note about the user gesture requirement: when we say that an action must have happened β€œas a result of a user gesture”, we mean that the JavaScript which resulted in the call to video.play(), for example, must have directly resulted from a handler for a touchend, click, doubleclick, or keydown event. So, button.addEventListener(β€˜click’, () => { video.play(); }) would satisfy the user gesture requirement. video.addEventListener(β€˜canplaythrough’, () => { video.play(); }) would not.
μ‚¬μš©μž 제슀처 μš”κ±΄μ— λŒ€ν•œ μ°Έκ³  사항: "μ‚¬μš©μž 제슀처의 κ²°κ³Ό"둜 λ™μž‘μ΄ λ°œμƒν•΄μ•Ό ν•œλ‹€λŠ” 것은, 예λ₯Ό λ“€μ–΄ video.play()λ₯Ό ν˜ΈμΆœν•œ μžλ°”μŠ€ν¬λ¦½νŠΈκ°€ touchend, click, doubleclick λ˜λŠ” keydown 이벀트의 ν•Έλ“€λŸ¬μ—μ„œ 직접 λ°œμƒν–ˆμ–΄μ•Ό ν•œλ‹€λŠ” μ˜λ―Έμž…λ‹ˆλ‹€. λ”°λΌμ„œ button.addEventListener('click', () => { video.play(); })λŠ” μ‚¬μš©μž 제슀처 μš”κ΅¬ 사항을 μΆ©μ‘±ν•˜μ§€λ§Œ, video.addEventListener('canplaythrough', () => { video.play(); })λŠ” μΆ©μ‘±ν•˜μ§€ λͺ»ν•©λ‹ˆλ‹€.
좜처: New <video> Policies for iOS

Webkit λ¬Έμ„œμ—μ„œ directly resulted라고 λͺ…μ‹œλ˜μ–΄ μžˆλ‹€.
즉, μ‚¬μš©μž 제슀처의 직접적인 결과둜 μž¬μƒκ³Ό 같은 JavaScript ν˜ΈμΆœμ€ ν—ˆμš©ν•œλ‹€λŠ” λœ»μ΄λ‹€.
λ‚΄ 생각엔 ν˜„μž¬ clickκ³Ό submit의 직접적인 결과둜 λ Œλ”λ§ν•˜κ³  μžˆμ§€, μ˜€λ””μ˜€λ₯Ό μž¬μƒν•˜λŠ” 것이 μ•„λ‹ˆλ―€λ‘œ λΈŒλΌμš°μ €(λͺ¨λ°”일 μ‚¬νŒŒλ¦¬)μ—μ„œ μ—λŸ¬λ₯Ό 좜λ ₯ν•œ 게 μ•„λ‹Œκ°€ μ‹Άμ—ˆλ‹€.
μ—¬κΈ°μ„œ 아이디어λ₯Ό μ°©μ•ˆν–ˆλ‹€!

clickκ³Ό submit이 λ°œμƒν•˜μžλ§ˆμž 아무 μ˜€λ””μ˜€λ₯Ό μŒμ†Œκ±°λ‘œ μž¬μƒν•˜λ‹€κ°€ μž¬μƒν•˜λ €λ˜ μ˜€λ””μ˜€ μ†ŒμŠ€λ₯Ό λ°›μœΌλ©΄ κ·Έλ•Œ μ˜€λ””μ˜€λ₯Ό μž¬μƒν•˜λ©΄ λ˜μ§€ μ•Šμ„κΉŒ?
결과적으둜만 보면 μ‚¬μš©μžμ˜ 직접적인 μƒν˜Έμž‘μš©μœΌλ‘œ μ˜€λ””μ˜€κ°€ μž¬μƒλ˜κ³ , 심지어 μŒμ†Œκ±°λ‘œ μž¬μƒλ˜κ³  μžˆμ—ˆμœΌλ‹ˆκΉŒ λΈŒλΌμš°μ €μ—μ„œ μ•ˆ 막을 κ±° 같은데??

μ½”λ“œ κ΅¬ν˜„

κΈ°μ‘΄ μ½”λ“œ

κΈ°μ‘΄μ—λŠ” μ˜€λ””μ˜€κ°€ μžλ™μž¬μƒλ˜μ–΄μ•Ό ν•  λ•Œ λ°”λ‘œ μž¬μƒν•˜λ„λ‘ μž‘μ„±ν–ˆλ‹€.
μ˜ˆμ‹œ μ½”λ“œμ΄λ―€λ‘œ 이런 λŠλ‚ŒμœΌλ‘œ μž‘μ„±ν–ˆκ΅¬λ‚˜ μ •λ„λ‘œλ§Œ μ°Έκ³ ν•˜λ©΄ μ’‹κ² λ‹€.

const audioRef = useRef<HTMLAudioElement>(null);
const [audioSrc, setAudioSrc] = useState<string | null>(null);

const fetchAudioSrc = useCallback(() => {
  // 3초 ν›„ μ˜€λ””μ˜€ μ†ŒμŠ€ κ°€μ Έμ˜€κΈ°
  setTiemout(() => {
	const src = "new audio source";
	  setAudioSrc(src);
  }, 3000);
}, []);

useEffect(() => {
  if (audioRef.current && audioSrc) {
	audioRef.current.src = audioSrc;
 	audioRef.current.onplay = () => { ... };
	audioRef.current.onended = () => { ... };
	audioRef.current.play();
  }
}, [audioSrc]);

return (
  ...
  <audio hidden ref={audioRef} />
  <button onClick={fetchAudioSrc}>Fetch Audio Source</button>
  ...
);

μ˜€λ””μ˜€ μž¬μƒ(audioRef.current.play())이 μ‚¬μš©μžμ˜ 직접적인 클릭으둜 λ°œμƒν•œ 게 μ•„λ‹ˆκ³  μƒνƒœ(audioSrc) λ³€κ²½μœΌλ‘œ 인해 λ°œμƒλœ 것이기 λ•Œλ¬Έμ— λͺ¨λ°”일 μ‚¬νŒŒλ¦¬μ—μ„œ μ—λŸ¬λ₯Ό λ‚΄λŠ” κ²ƒμœΌλ‘œ μ˜μ‹¬λ˜μ—ˆλ‹€.

바뀐 μ½”λ“œ

button의 click μ΄λ²€νŠΈμ— μ˜€λ””μ˜€λ₯Ό λ°”λ‘œ μž¬μƒν•˜λ„λ‘ μˆ˜μ •ν•˜μ˜€λ‹€.

const audioRef = useRef<HTMLAudioElement>(null);
const [audioSrc, setAudioSrc] = useState<string | null>(null);

// μΆ”κ°€
const playFakeAudio = useCallback(() => {
  if (audioRef.current) {
	audioRef.current.src = "아무 μ˜€λ””μ˜€ μ†ŒμŠ€";
	audioRef.current.muted = true;
	audioRef.current.onplay = null;
	audioRef.current.onended = null;
  }
}, []);

const fetchAudioSrc = useCallback(() => {
  playFakeAudio(); // μΆ”κ°€
  setTiemout(() => {
	const src = "new audio source";
	setAudioSrc(src);
  }, 3000);
}, []);

useEffect(() => {
  if (audioRef.current && audioSrc) {
	audioRef.current.src = audioSrc;
	audioRef.current.muted = false; // μΆ”κ°€
    audioRef.current.onplay = () => { ... };
    audioRef.current.onended = () => { ... };
  }
}, [audioSrc]);

return (
  ...
  <audio autoPlay hidden ref={audioRef} /> {/* autoPlay μΆ”κ°€ */}
  <button onClick={fetchAudioSrc}>Fetch Audio Source</button>
  ...
);

audio νƒœκ·Έμ— autoPlay 속성을 μΆ”κ°€ν•˜μ—¬ λ―Έλ””μ–΄κ°€ μž¬μƒ κ°€λŠ₯ν•  λ•Œ μžλ™μœΌλ‘œ μž¬μƒλ˜κ²Œ ν•˜μ˜€λ‹€.
그리고 λ²„νŠΌμ„ ν΄λ¦­ν•˜λ©΄ playFakeAudio()λ₯Ό ν˜ΈμΆœν•˜μ—¬ 3초 μ „κΉŒμ§€ audioκ°€ μŒμ†Œκ±° μƒνƒœμ—μ„œ μž¬μƒλ˜κ²Œλ” ν•˜μ˜€λ‹€.(audioRef.current.muted = true;)
3초 ν›„μ—λŠ” μ‹€μ œ μž¬μƒλ˜μ–΄μ•Ό ν•˜λŠ” μ†ŒμŠ€λ₯Ό κ°€μ Έμ˜€κΈ° λ•Œλ¬Έμ— useEffectμ—μ„œ audio의 srcλ₯Ό λ°”κΎΈκ³  mutedλ₯Ό ν•΄μ œν•˜μ—¬, 클릭 이벀트 λ•Œλ¬Έμ— 이미 μž¬μƒ μ€‘μ΄λ˜ μ˜€λ””μ˜€κ°€ μ†Œλ¦¬μ™€ ν•¨κ»˜ μž¬μƒν•˜κ²Œ μ²˜λ¦¬ν•˜μ˜€λ‹€.

κ²°κ³Ό

κ³„νšλŒ€λ‘œ (ν…ŒμŠ€νŠΈ κ°€λŠ₯ν•œ..) λͺ¨λ“  λΈŒλΌμš°μ €μ—μ„œ μ˜€λ””μ˜€κ°€ μžλ™μž¬μƒλ˜μ—ˆλ‹€.
λͺ¨λ“  μ‚¬νŒŒλ¦¬ λͺ¨λ°”일 λ²„μ „μ—μ„œ ν…ŒμŠ€νŠΈν•΄λ³΄μ§€λŠ” λͺ»ν–ˆμœΌλ‚˜, iOS 15~17μ—μ„œλŠ” 문제 μ—†μ—ˆλ‹€.
κΈ°λŠ₯ κ΅¬ν˜„μ„ μœ„ν•΄ λΆˆν•„μš”ν•œ λ¦¬μ†ŒμŠ€λ₯Ό λ‹€μš΄λ°›μ•„μ•Ό ν•˜λŠ” 단점이 μžˆμ§€λ§Œ, 크기가 μž‘μ€ μ†ŒμŠ€λ₯Ό μ‚¬μš©ν•œλ‹€λ©΄ μ‚¬μš©μžμ—κ²Œ 뢀담을 덜 쀄 κ²ƒμœΌλ‘œ μ˜ˆμƒν•œλ‹€.
λ―Έλ””μ–΄ μžλ™μž¬μƒμ„ κ΅¬ν˜„ν•  λ•Œ λΈŒλΌμš°μ € 정책을 읽어보고 ν”„λ‘œμ νŠΈ 상황에 맞게 μš°νšŒν•  방법을 κ³ λ―Όν•˜λŠ” 것을 μΆ”μ²œν•œλ‹€.
그리고 μš°νšŒν•œ λ‹€λ₯Έ μ•„μ΄λ””μ–΄λ‚˜ κ²½ν—˜μ΄ μžˆλ‹€λ©΄ κ³΅μœ ν•΄μ£Όλ©΄ λ„ˆλ¬΄ κ°μ‚¬ν•˜κ² λ‹€..!

μ°Έκ³ 

React Safari μ˜€λ””μ˜€ μž¬μƒ νŠΈλŸ¬λΈ” μŠˆνŒ…
[JS] audio νƒœκ·Έλ‘œ bgm μž¬μƒ, play() failed because the user didn't interact with the document first μ—λŸ¬
New <video> Policies for iOS

profile
ν•œ 우물만 νŒŒλŠ” μ‚¬λžŒ

0개의 λŒ“κΈ€