개발 기술/사소하지만 놓치기 쉬운 개발 지식

iOS 모바일의 보안과 이벤트 유실(feat. 사용자 활성화, 이벤트 루프)

by GicoMomg 2024. 10. 3.

0. 들어가며

얼마 전, 모바일 환경에서 카메라를 구동시키는 코드를 수정하고 있었다.
그런데 Vue 파일에서는 파일 업로드와 카메라 구동 코드, 팝업창 등 여러 기능이 혼재되어 있어 하나의 컴포넌트가 여러 책임을 지고 있었다.
이를 해결하기 위해 팝업창과 파일 업로드 창을 각각의 컴포넌트로 분리했는데…
컴포넌트를 리팩토링한 후, 왜인지 iOS 모바일에서 카메라 구동 코드가 작동하지 않았다… ;;

 

문제는 생각보다 간단했는데…
promise await 후에 카메라 구동 코드를 실행했기 때문이었다.

  • 아래 예시는 유사한 현상이 발생했던 코드이다.

See the Pen ios await 후 이벤트 유실 by KumJungMin (@kumjungmin) on CodePen.

 

 

 

  • 의도상, [즉시 실행 클릭]이나 [2초 wait 후 실행 클릭]을 했을 때 파일 입력창(모바일은 카메라)이 열려야 한다.
  • MAC과 안드로이드에서는 파일 입력창(모바일은 카메라)이 작동하지만, IOS 모바일에선 작동하지 않았다!

<mac의 예시 - [즉시 실행 클릭], [2초 wait 후 실행 클릭] 버튼 클릭시 파일 입력창 열림>



<IOS mobile 예시 - [2초 wait 후 실행 클릭] 버튼 클릭시 카메라 안열림>

 

왜 이런 현상이 발생한걸까?

  • 원인을 찾아보니 브라우저 보안 정책 때문이었는데...
  • IOS에서는 사용자 활성화 상태가 아니면 보안 및 사용자 경험을 이유로 일부 동작을 제한한다고 한다.
  • 그래서 결과적으로 IOS 모바일에서는 카메라가 열리지 않았다.
  • 아니 도대체 사용자 활성화가 뭐길래 이런 현상이 일어난 걸까??
  • 이번 시간에는 “사용자 활성화와 IOS 모바일 환경에서의 이벤트 유실”에 대해 알아보겠다.
  • 이 글은 아래 순서대로 진행된다.
목차

1. 사용자 활성화와 이벤트 유실
  1) 사용자 활성화란?
    (1) isTrusted의 개념과 예시
 
  2) 사용자 활성화의 종류
    (1) Transient Activation (일시적 활성화)
    (2) Persistent Activation (고정 활성화)
 
  3) 비동기 작업과 사용자 활성화 문제

2. 마치며



 

 

1. 사용자 활성화와 이벤트 유실

1) 사용자 활성화란?

  • 사용자 활성화(User Activation)란, 사용자가 현재 페이지에서 클릭하거나 터치하는 등, 직접 상호작용을 하고 있는 상태를 의미한다.
  • 이 상태는 보안사용자 경험을 보호하기 위해, 사용자의 명시적인 동의 없이 실행되는 동작을 제한하는 역할을 한다.
  • 다만, 비동기 작업이 개입하면 사용자 활성화 상태가 비활성화될 수 있다.
  • 사용자 활성화 상태가 되는 경우는 두 가지이다:
    1. isTrusted 속성이 true인 경우
    2. keydown, mousedown 등 사용자 이벤트가 발생한 경우

<MDN 문서의 사용자 활성화 설명 일부>

 

어? 그런데 사용자 이벤트는 익숙한데, isTrusted 속성은 뭘까?

 

(1) isTrusted의 개념과 예시

  • isTrusted는 웹 브라우저의 이벤트 객체에 있는 읽기 전용 속성이다.
  • 이 속성은 해당 이벤트가 사용자의 상호작용에 의해 발생했는지, 아니면 스크립트에 의해 생성되었는지 나타낸다.
  • 이 속성을 사용하면, 사용자 인증이나 미디어 재생 등 보안상 민감한 동작을 수행할 때 신뢰성 있는 이벤트인지 확인할 수 있다.
  • 일부 브라우저는 isTrusted 값이 false인 경우 보안을 위해 이벤트를 막기도 한다.
isTrusted 설명
true - 이벤트가 사용자의 실제 상호작용(예: 클릭, 키 입력 등)에 의해 발생한 경우

예시: 사용자가 실제로 버튼을 클릭해서 발생한 click 이벤트인 경우
false - 이벤트가 스크립트에 의해 생성된 경우

예시 1: dispatchEvent()를 사용하여 생성한 커스텀 이벤트
예시 2: HTMLElement.click()으로 다른 요소의 이벤트를 트리거한 경우

 

  • 첫 번째 코드 예시는 isTrusted 값이 true인 경우이다.
  • 사용자가 myButton을 클릭하면 isTrustedtrue로 설정된다.
document.getElementById('myButton').addEventListener('click', (e) => {
  console.log(e.isTrusted); // 출력: true
});

 

  • 다음 코드 예시는 isTrusted 값이 false인 경우이다.
  • 아래 예시에선 HTMLElement.click()을 사용하여 다른 요소의 클릭 이벤트를 트리거했다.
  • 이 경우, 클릭 이벤트가 사용자에 의해 발생한 것이 아니기에 isTrustedfalse된다.
document.getElementById('scriptButton').addEventListener('click', (event) => {
  console.log(event.isTrusted); // 출력: false
});

// 스크립트를 통한 클릭 트리거
document.getElementById('scriptButton').click();

 

  • 세 번째는 dispatchEvent()를 사용하여 커스텀 이벤트를 생성했다.
  • 커스텀 이벤트 또한 스크립트에 의해 인위적으로 생성된 동작이기에 isTrustedfalse된다.
// 커스텀 이벤트 생성
const customEvent = new Event('custom-event');

// 이벤트 리스너 등록
document.getElementById('myElement').addEventListener('custom-event', (event) => {
  console.log(event.isTrusted); // 출력: false
});

// 커스텀 이벤트 디스패치
document.getElementById('myElement').dispatchEvent(customEvent);

 

아하! 사용자 활성화 상태는 (1) 사용자에 의해 직접 발생한 이벤트이거나(isTrusted = true),
(2) keydown, mousedown 등 이벤트가 발생할 때구나!

  • 그럼, 사용자 활성화 상태가 되는 경우를 알았으니 사용자 활성화에 대해 더 알아보겠다.



2) 사용자 활성화의 종류

  • 사용자 활성화는 Transient Activation (일시적 활성화)Persistent Activation (고정 활성화)로 분류된다.

(1) Transient Activation (일시적 활성화)

  • 사용자가 버튼을 클릭하거나 마우스를 움직이는 등의 직접 상호작용이 발생한 상태이다.
  • 이 상태는 짧게 유지되며, 추가 상호작용이 없으면 자동으로 종료된다.
  • 브라우저는 보안과 사용자 경험을 위해 이 일시적 활성화 동안 특정 API 사용(아래 표)을 허용한다.
API 종류 설명
파일 및 디렉토리 선택 - 파일 선택기나 새로운 창을 열 때
- 사용자의 직접적인 요청 없이 실행될 경우 보안상의 이유로 제한됨

예시: Window.open(), Window.showOpenFilePicker(), Window.showSaveFilePicker()
클립보드 - 클립보드에 접근하거나 데이터를 읽고 쓸 때
- 사용자의 개인정보 보호를 위해 제한됨

예시: Clipboard.read(), Clipboard.readText(), Clipboard.writeText()
미디어 및 오디오 - 비디오의 픽처 인 픽처 모드 요청, 오디오 출력 선택, 화면 공유 등 미디어 관련 기능
예시: HTMLVideoElement.requestPictureInPicture(), MediaDevices.getDisplayMedia()
화면 제어 - 전체 화면 모드 요청이나 포인터 락(마우스 커서 고정)과 같은 화면 제어 기능

예시: Element.requestFullscreen(), Element.requestPointerLock()
권한 및 저장소 - 저장소 접근 요청이나 사용자의 활동 상태를 감지하기 위한 권한 요청

예시: Document.requestStorageAccess(), IdleDetector.requestPermission()
하드웨어 및 장치 접근 - USB 장치 접근, 확장 현실(XR) 세션 요청 등 하드웨어와 직접적으로 상호작용하는 API

예시: HID.requestDevice(), USB.requestDevice()
공유 및 결제 예시: Navigator.share(), PaymentRequest.show()
입력 및 선택 예시: HTMLInputElement.showPicker()
기타 예시: Window.getScreenDetails(), Window.queryLocalFonts()

 

(2) Persistent Activation (고정 활성화)

  • 고정 활성화는 한 번의 사용자 상호작용으로 특정 기능이 계속 활성화되는 상태이다.
  • 추가적인 상호작용 없이도 유지되며, 일시적 활성화와 달리 한 번 설정되면 다시 활성화할 필요가 없다.
  • 예를 들어, 사용자가 자동 재생을 허용하면 이후에는 별도의 동작 없이도 비디오가 자동으로 재생될 수 있다.
API 종류 설명
진동 및 자동 재생 정책 - 기기의 진동을 제어하거나 자동 재생 정책을 확인하는 기능
- 사용자가 명확하게 요청한 경우에만 활성화되며, 이후 지속적으로 사용할 수 있음

예시: Navigator.vibrate(), navigator.getAutoplayPolicy()



3) 비동기 작업과 사용자 활성화 문제

  • 사용자 활성화 상태는 동기적인 흐름, 즉 사용자가 직접 상호작용한 DOM 이벤트가 처리되는 동안에만 유지된다.
  • 하지만 비동기 작업(예: setTimeout, Promise, async/await)이 실행되면, 이벤트 루프가 비동기 작업을 처리하기 위해 실행 흐름을 벗어나게 되고 이때 사용자 활성화 상태가 만료될 수 있다.
  • 즉, 비동기 작업이 개입되면 브라우저는 더 이상 해당 이벤트를 사용자가 직접 트리거한 것으로 간주하지 않는다.
그런데, 비동기 작업이 있으면 왜 사용자 활성화가 종료될까?
원인을 알기 위해서 자바스크립트의 이벤트 루프를 알아야 한다.
  • 우선 이벤트 루프에 대해 살펴보자.
  • 이벤트 루프는 자바스크립트에서 비동기 작업을 처리하는 방법으로, 동기 작업과 비동기 작업을 효율적으로 관리한다.
  • 이벤트 루프에는 두 가지 태스크 큐가 있는데, 그것은 매크로태스크 큐마이크로태스크 큐이다.
종류 매크로태스크(Macrotask) 마이크로태스크(Microtask)
설명 - 매크로태스크큰 작업 단위로, 일반적으로 오래 걸리는 작업들이 이 큐에 들어간다.
- 매크로태스크는 호출 스택이 비워진 후에 처리된다.
- 마이크로태스크짧은 작업 단위로, 매크로태스크보다 먼저 처리된다.
- 마이크로태스크는 매크로태스크가 종료된 직후에 처리되며, 현재 실행 중인 작업이 완료된 후 바로 실행된다.
예시 - setTimeout, setInterval
- DOM 이벤트• I/O 작업
- requestAnimationFrame
- Promise.then(), async/await 콜백
- Mutation Observer
  • 매크로태스크 큐는 상대적으로 더 큰 작업 단위들이 대기하는 곳이다.
  • 매크로태스크 큐에 있는 작업들은 자바스크립트의 기본 실행 흐름이 끝난 후 처리된다.
  • 마이크로태스크 큐는 더 작은 작업들을 처리하는 큐로, 비동기 처리의 즉각적인 후속 작업을 관리한다.
  • 마이크로태스크 큐의 작업들은 매크로태스크가 완료된 후 바로 처리된다.
  • 이벤트 루프는 현재 실행 중인 매크로태스크를 완료한 후, 마이크로태스크 큐에 있는 모든 작업을 처리한 다음에야 다시 매크로태스크로 넘어간다.
  • 만약 매크로태스크 큐에 작업이 남아있어도, 마이크로태스크 큐에 대기 중인 작업이 있다면 그 작업이 먼저 처리한다.

 

다시 돌아와서, 왜 이벤트 루프와 사용자 활성화가 연관이 있을까?
  • 사용자가 직접 상호작용하여 DOM 이벤트가 발생했다고 가정하자. 이때 사용자 활성화 상태가 된다.
  • DOM 이벤트는 자바스크립트의 매크로태스크 큐에서 처리되지만, await 구문은 마이크로태스크 큐에서 처리된다.
  • 여기서 중요한 건 이벤트 루프는 동기 작업을 모두 완료한 뒤, "마이크로태스크 큐의 작업을 끝내야 매크로태스크 큐의 다음 작업으로 넘어간다"는 점이다.
  • 따라서 await 구문으로 인해 실행 흐름이 마이크로태스크 큐로 넘어가면, 사용자 활성화 상태가 만료될 수 있다.

 

비동기 작업 후 사용자 활성화 상태의 만료 예시

// 사용자 클릭 이벤트 핸들러
button.addEventListener('click', async function() {
  // 사용자 클릭으로 인한 이벤트 -> isTrusted = true

  await fetchData(); // 비동기 작업, 마이크로태스크 큐에 추가됨
  input.click();   
});
동작 설명
사용자 클릭 사용자가 버튼을 클릭하면 해당 이벤트는 매크로태스크 큐에 추가되고, isTrustedtrue로 설정된다.
비동기 작업 await fetchData()가 호출되면서, 함수의 실행이 일시 중단된다.
fetchData()는 비동기이므로 마이크로태스크 큐에 추가된다.
매크로태스크 완료 이벤트 핸들러 실행이 완료되면, 사용자 활성화 상태는 자동으로 해제된다.
마이크로태스크 실행 마이크로태스크 큐에 있는 input.click()이 실행되지만,
이 시점에서는 사용자 활성화가 해제되어 있다.
  • 사용자 상호작용으로 발생한 DOM 이벤트는 매크로태스크 큐에서 처리되며, 이때 사용자 활성화 상태가 유지된다.
  • 하지만 await 구문을 만나 비동기 작업이 마이크로태스크 큐로 넘어가면, 이벤트 루프는 매크로태스크 작업이 완료되었다 간주한다.
  • 그럼 이 시점에서 사용자 활성화 상태는 만료된다.

 

setTimeout 후 사용자 활성화 상태의 만료 예시

button.addEventListener('click', function() {
  // 사용자 클릭으로 인한 이벤트 -> isTrusted = true

  setTimeout(function() {
    input.click();
  }, 2000);
});
동작 설명
사용자 클릭 사용자가 버튼을 클릭하면 해당 이벤트는 매크로태스크 큐에 추가되고, isTrustedtrue로 설정된다.
setTimeout 호출 setTimeout을 사용하여 2000ms 후에 input.click()을 실행하도록 예약한다.
이 콜백은 매크로태스크 큐에 추가되며, 초기 매크로태스크(사용자 클릭 이벤트)와는 별개의 실행 흐름으로 처리된다.
매크로태스크 완료 초기 매크로태스크가 완료되면서, 사용자 활성화는 자동으로 해제된다.
setTimeout 매크로태스크 실행 setTimeout 콜백이 실행되지만,
이 시점에서는 사용자 활성화가 해제되어 있다.
이때 IOS 모바일에서는 콜백이 실행되지 않는데, 980ms까지는 콜백이 실행되는 케이스가 있다.
  • 사용자 활성화 상태는 단일 이벤트 흐름에서만 유효하며, 해당 이벤트가 처리되면 만료된다.
  • setTimeout의 콜백은 클릭 이벤트와 다른 매크로태스크로 처리되기에, 클릭 이벤트가 완료된 후 활성화 상태가 만료된다.
  • 이벤트 루프에서 초기 매크로태스크가 완료되면 사용자의 상호작용이 끝난 것으로 간주되어 활성화 상태가 해제된다.
  • 따라서 setTimeout 콜백이 실행될 때 사용자 활성화 상태는 만료되어있다.

 

그런데 왜 iOS 모바일에서는 이 현상이 발생하고, Android나 다른 웹 브라우저에서는 코드가 정상적으로 동작할까?
그리고 iOS 모바일에서는 왜 setTimeout 980ms까지는 정상 동작할까?

  • 그 이유는 iOS 모바일은 사용자의 보안을 최우선으로 고려한 보안 정책을 시행하기 때문이다.
  • 또한, 브라우저마다 동작이나 사용자 활성화 상태와 관련된 정책이 다르기 때문이기도 하다.
    (사용자 활성화는 짧은 시간 유지되는데, 그 시간은 브라우저 별로 다를 수 있음)
  • 예를 들어, iOS의 Safari는 Apple의 보안 및 프라이버시 정책을 준수하기 위해 사용자 활성화 관련 규정을 엄격하게 적용한다면,
    Android의 Chrome이나 Firefox는 약간 다른 정책을 채택하고 있어 동일한 코드가 다르게 동작할 수 있다.



2. 마치며…

이번 시간에는 iOS 모바일에서 await 및 Promise 호출 후 스크립트로 DOM 동작을 요청할 때, 해당 동작이 진행되지 않는 문제를 살펴봤다.

iOS 모바일 브라우저에서 스크립트로 발생한 이벤트가 차단되는 이유는 사용자 보안경험 보호를 위한 조치이다. 이러한 제한을 이해하고, 사용자 상호작용에 의존한 동기적인 이벤트 처리 방식으로 문제를 해결할 수 있다. 특히, 비동기 호출로 인한 이벤트 유실을 피하려면, 사용자의 직접적인 제스처와 연결된 이벤트 핸들러 내에서 동작을 수행하는 것이 중요하다.

비록 이 현상은 iOS 모바일에서 확인되었지만, 브라우저별로 유사한 현상이 발생할 수 있으므로, 이벤트 유실에 주의할 필요가 있다.




출처

반응형

댓글