---
layout: post
title: "자잘한 도움말 #14 웹 페이지의 화면 절전 방지 구현하기"
subtitle: "Screen Wake Lock API"
description: "JavaScript, 웹 페이지 화면 절전, 화면 절전 막기, 절전 모드, 슬립 모드, 슬립 모드 진입 방지, Wake Lock, wakeLock, Screen Wake Lock API, 기기를 켜진 상태로 유지, 화면 자동 꺼짐 방지, PWA, PWA 화면 절전, PWA 슬립 모드, 프로그레시브 웹 앱, 프로그레시브 웹 어플리케이션, 프로그레시브 웹 애플리케이션, Progressive Web Application"
date: 2024-02-28 17:40:00 +0900
categories: [Micro]
---

## TL;DR

- 화면 절전 방지는 **Wake Lock**이라고 부른다.
- Web API [**Screen Wake Lock API**](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API)를 사용한다.
  ```javascript
  // Screen Wake Lock API는 Promise 기반으로 동작한다
  // 상황에 맞게 async/await으로 사용하거나 Promise 형태 그대로 사용하자

  // wake lock 활성화
  let wakeLock = await navigator.wakeLock.request("screen");

  // wake lock 비활성화
  await wakeLock.release();
  wakeLock = null; // 필요시 null로 초기화해 주자
  ```
- 2024년 2월 기준 아직 해당 API를 지원하지 않는 브라우저들이 있기 때문에 기능 구현 시 지원 여부를 확인해야 한다.
  ```javascript
  isSupported = "wakeLock" in navigator;
  ```
- Screen Wake Lock API는 [보안 컨텍스트(Secure contexts)](https://developer.mozilla.org/ko/docs/Web/Security/Secure_Contexts)에서만 제공된다.
  - HTTPS 혹은 로컬 호스트를 나타내는 URL에 접속할 때만 제공된다.
  - 테스트 시 장치가 서버에 보안 컨텍스트 상황에 맞게 접속하고 있는지 확인해야 한다.
    - 특정 브라우저에서는 로컬 호스트를 보안 컨텍스트로 인식하지 못할 수도 있다.

---

## 이하 그리 중요하진 않은 내용들

![PWA](https://i.postimg.cc/bwJN0nqD/image.png)  
{: .center}

오늘 주제는 PWA의 일부분을 다룬다. PWA라고 하니 뭔가 거창한 이야기를 해야 할 것 같지만, 사실 나도 PWA에 대해서는 잘 모른다. 웹과 네이티브 모두의 장점을 가지는 웹 어플리케이션이라는 개념 정도? 근데 이걸 달리 말하면, PWA를 위해선 웹 개발자가 웹과 네이티브 모두의 장점을 신경 써가며 기능을 구현해야 한다는 말이기도 하겠지. 이번 주제 화면 절전 방지처럼, 웹 앱인데 네이티브 앱인 것 마냥 장치를 직접 제어할 수 있도록 만들어야 한다던가. 문득 PWA는 특정 기술이나 도구를 칭한다기보다는, 개발 지침 혹은 패러다임에 가깝다는 생각이 든다. 아무튼, 글을 시작하기에 앞서 딱히 할 말이 없어서 생각나는 대로 적어봤다. PWA에 대해서 이 글에서는 더 이상 언급할 일은 없을 것 같으니 자세한 정보를 원한다면 다른 글을 찾아보도록 하자. 좋은 글이 많더라고.

### 화면 절전

[![도마도](https://raw.githubusercontent.com/anteater333/domado/main/docs/screen.png)  
도마도 뽀모도로 타이머](https://domado.vercel.app/)
{: .center .rounded-edge-16}

최근 열심히 여기저기 끼워 팔고 있는 나의 토이 프로젝트 도마도. 작년 말부터 뽀모도로 타이머 어플 하나 잘 쓰고 있었는데 갑자기 그게 나보고 쓰던 기능들 계속 쓰고 싶으면 결제하라고 하더라고. "그깟 타이머 만드는 거 뭐가 어렵다고 내가 만들고 말지." 라는 생각으로 시작했고 벌써 한 달 넘게 이걸 붙잡고 있네.. 아니 사실은 한 달 전에 이미 어느 정도 혼자 만족해하며 쓸 수 있는 수준까진 만들어놨고, 공개된 서버에 배포도 해서 주변인들 여럿에게 알려놓기도 했다. 그냥 손 털고 다시 할 거 하러 가도 되는 상황인데, 디자인이 이쁘게 나와서 그런가, 미련을 떨쳐내지 못하고 기능을 덧붙이고 있다.

아무튼 이렇게 쪼잔하고 옹졸한 마인드에서 출발한 프로젝트였기에 PC를 제외한 다른 장치에 대한 고려가 부족했던 게 사실이다. 뒤늦게 페이지에 반응형 디자인을 적용하고 내 휴대폰에서 테스트를 시작했더니, 25분 타이머가 다 가기도 전에 갑자기 꺼지는 내 휴대폰의 화면. 모바일 디바이스는 배터리가 소중해요. 쓰지도 않는 디스플레이 뭐 하러 켜두니. 하지만 애석하게도 화면이 꺼진다는 것은 웹 브라우저가 백그라운드로 넘어간다는 뜻이고, 아직 나의 도마도는 기술적인 문제로 인해 백그라운드에서 타이머가 제대로 동작하지 않았다. 백그라운드 기능을 구현하는 게 더 좋은 방법이겠지만 일단 더 쉬워 보이는 길부터 가보자. 그냥 화면이 꺼지지 않게 만들면 되잖아. [(물론 글을 쓰고 있는 지금은 백그라운드에서도 돌아가도록 구현해놨다)](https://github.com/anteater333/domado/commit/58f39a0674ff65d189de1e65b307995935e48a23){:target="_blank"}{: .small .a-not-colored}

![옵션](https://i.postimg.cc/NFJWkTwB/image.png)  
이 옵션을 추가하는 게 이번 목표  
{: .center .w-3-quarter .rounded-edge-16}

### Screen Wake Lock API

문제를 해결하는 일에서 가장 핵심은 그 문제를 한 단어로 정리해 줄 키워드를 찾는 거라고 생각한다. 이 문제를 설명하는 나의 장황한 언어를 어떻게 고정해 검색어로 사용할 것인가. 몇 년 전에는 이걸 정말 어려워했는데, 요즘엔 GPT가 있어 너무나 행복하구나.

![GPT](https://i.postimg.cc/3xYCgwqB/image.png)  
{: .center .w-3-quarter .rounded-edge-16}

**Web API인 [Screen Wake Lock API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API){:target="_blank"}를 사용하자.** 문제 해결, 끝.

물론 이렇게 글을 마치려는 건 아니고, 저게 뭔지 한 번 살펴는 봐야겠지. 디바이스의 화면이 자동으로 꺼지지 않게 하는 기능을 **Wake Lock**이라고 부른다. 깨어있는 상태로 잠가놓겠다는 의미다. 디바이스를 직접 제어하는 기능이다 보니 검색해 보면 웹보다는 안드로이드 같은 네이티브 환경에 대한 레퍼런스가 많이 나온다. 앞서 잠깐 설명했던 PWA 기조 덕분에 웹 환경에도 이런 기능을 수행하는 API를 추가한 것이 아닐까. 아무튼 API를 사용하는 코드는 다음과 같다.

```javascript
// Screen Wake Lock API는 Promise 기반으로 동작한다
// 상황에 맞게 async/await으로 사용하거나 Promise 형태 그대로 사용하자

// wake lock 활성화
let wakeLock = await navigator.wakeLock.request("screen");

// wake lock 비활성화
await wakeLock.release();
wakeLock = null; // 필요시 null로 초기화해 주자
```

```javascript
// 비활성화 시 동작을 Event Listener로 정의할 수도 있다.
wakeLock.addEventListener("release", () => {
  // DO SOMETHING
});
```

코드는 별로 어려울 것 없다. 기초적인 `Promise`다. 하지만 주의할 점, 2024년 기준 [아직 이 API를 지원하지 않는 브라우저도 있다](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API#browser_compatibility){:target="_blank"}. 특히 내가 애용하는 파이어폭스가 유독 이 기능을 늦게까지 추가하지 않고 있다. 아무튼, Screen Wake Lock API를 사용하기 전에 다음 코드로 지금 브라우저 환경에서 API를 사용할 수 있는지도 확인해 주자.

```javascript
// 사용할 수 있으면 true, 사용할 수 없으면 false
const isSupported = "wakeLock" in navigator;
```

### 보안 컨텍스트(Secure Context)

[![성](https://i.postimg.cc/kXwD85W1/image.jpg)  
웹 브라우저는 격리된 환경이다.](/micro/2022/05/05/micro-tip-5.html#wsl%EC%9D%80-vm%EC%9D%B4%EB%8B%A4){:target="_blank"}{: .a-not-colored}
{: .center}

웹 페이지는 웹 브라우저라는 철저히 제한된 환경 안에서만 동작한다. 파일 시스템 접근조차 불가능해서 사용자에게 "제발 파일 좀 골라주십쇼" 하고 파일 선택을 위임해야 한다. 그런 웹 페이지가 지금 사용자의 디바이스를 직접 조작하려고 한다. PWA는 웹 어플리케이션이 기존 웹을 초월한 능력을 보여주길 요구한다.

![Secure Context](https://i.postimg.cc/Gh5BjC4g/image.png)  
{: .rounded-edge-16 .center}

> 이 기능은 오직 **보안 컨텍스트(HTTPS)**에서만 사용할 수 있습니다.

[Screen Wake Lock API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API){:target="_blank"} 문서의 최상단엔 이런 안내 문구가 적혀 있다. 웹의 설계자들은 민감한 Web API는 기존보다 더 제한된 환경에서만 동작하도록 만들었다. 그것이 **[보안 컨텍스트(Secure Context)](https://developer.mozilla.org/ko/docs/Web/Security/Secure_Contexts){:target="_blank"}**이다. 위 문구에서 짐작했겠지만, 일반적으로 보안 컨텍스트는 HTTPS 환경을 의미한다. 개발 서버는 `http://localhost:5173/`로 쓰고 있는데 어떻게 하나요? 걱정할 필요 없이 로컬 호스트를 나타내는 URL도 보안 컨텍스트로 간주한다. 물론 파이어폭스는 84 버전 이상에서만 로컬 호스트를 보안 컨텍스트로 인식한다는 예외 사항이 있지만 일반적으론 괜찮다.**(또 너냐 파이어폭스)**{: .small} 자세한 명세사항은 [여기](https://w3c.github.io/webappsec-secure-contexts/){:target="_blank"}에서 확인할 수 있긴 한데, 이걸 다 읽는 순간 "자잘한 도움말"에서 벗어나는 거 같으니 알아만 두고 어디 가서 잘난척할 때 써먹자.

보안 컨텍스트의 요점은 너무 강력한 API를 쓸 때 그 힘이 폭주하지 않도록 하자는 것이다. 혹시라도 웹 페이지에 HTTP로 접속했다가 통신 중간에 위치한 공격자가 자바스크립트를 수정해서 몰래 Wake Lock을 켜버리면 안 되니까. 배터리가 얼마나 빨리 따르겠어.  

![권한](https://i.postimg.cc/KvNd5RKx/image.png)  
{: .rounded-edge-16 .center}

아무튼, 이런 API들은 섬세하게 다뤄져야 하다. 심한 경우 보안 컨텍스트로 제한할 뿐만 아니라 자바스크립트 입장에서 사용하기 불편하게 설계하는 경우도 있다. 예를 들어 앞서 소개한 도마도에서는 [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/Notification){:target="_blank"}도 사용되었다. 이 API는 웹 페이지에서 권한을 따로 얻어야 하고, 심지어 그 권한을 얻으려면 꼭 사용자 입력을 처리하는 이벤트 핸들러에서 API를 호출해야 한다.

### React Custom Hook으로 만들어보자

도마도는 React로 개발한 프로젝트다. 이런 기능은 훅(Hook)으로 묶어 쓰기 딱이지. 나는 아래 코드처럼 만들어봤다. 에러 처리 안 된 부분까지 가감 없이 공개해 본다. 이 코드가 별로라면 [이런 npm 패키지도 있으니](https://github.com/jorisre/react-screen-wake-lock/tree/main) 소스 코드를 참고해 보면 좋을 것 같다.

```typescript
import { useCallback, useEffect, useState } from 'react';

export const useWakeLock = () => {
  const [wakeLock, setWakeLock] = useState<WakeLockSentinel | null>(null);

  /** 각 callback들이 직접 조작하지 않고 useEffect 훅에서 WakeLock을 조작할 수 있도록 별도 상태 선언 */
  const [wakeLockStatus, setWakeLockStatus] = useState<
    'requested' | 'released' | 'idle'
  >('idle');

  const isSupported = 'wakeLock' in navigator;

  /** 경고 문구 출력 hook (개발자 참고용) */
  useEffect(() => {
    if (!isSupported)
      console.log(
        '[useWakeLock] 현재 브라우저는 Screen Wake Lock API를 지원하지 않습니다.\nhttps://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API',
      );
  }, [isSupported]);

  /** 실질적으로 Wake Lock을 변경하는 useEffect 훅 (core 로직) */
  useEffect(() => {
    // known issue.
    // requested case가 발생한 순간에 웹 페이지의 탭이 백그라운드에 위치할 시 DOMException을 발생시킴.
    // 에러 타입에 대한 조사 후 handling 필요.
    switch (wakeLockStatus) {
      case 'requested':
        if (!isSupported) setWakeLockStatus('idle');
        else
          navigator.wakeLock.request('screen').then((newWakeLock) => {
            setWakeLock((prevWakeLock) => {
              prevWakeLock?.release();
              setWakeLockStatus('idle');
              return newWakeLock;
            });
          });
        break;
      case 'released':
        if (!wakeLock) setWakeLockStatus('idle');
        else
          wakeLock.release().then(() => {
            setWakeLock(null);
            setWakeLockStatus('idle');
          });
        break;
    }
  }, [wakeLock, wakeLockStatus, isSupported]);

  /** Wake Lock을 사용하는 상태로 변경, API 형태로 포장 */
  const requestWakeLock = useCallback(() => {
    setWakeLockStatus('requested');
  }, []);

  /** Wake Lock을 사용하지 않는 상태로 변경, API 형태로 포장 */
  const releaseWakeLock = useCallback(() => {
    setWakeLockStatus('released');
  }, []);

  return {
    /** Wake Lock 사용 */
    requestWakeLock,
    /** Wake Lock 해제 */
    releaseWakeLock,
    /** 현재 웹 브라우저의 기능 지원 여부 */
    isSupported,
  };
};
```

## 내가 배운 것

- 화면 절전 방지 기능을 Wake Lock이라고 부른다.
  - 그리고 해당 기능은 Web API (Screen Wake Lock API)로 제공된다.
- 특정 Web API들은 보안 컨텍스트 (Secure Context) 상황에서만 사용할 수 있다.
  - 보안 컨텍스트는 HTTPS 환경 혹은 로컬 호스트 환경을 의미한다.
- PWA에 발끝 정도 담가본 것 같다.
