교차 출처 서비스 워커 - 외부 가져오기 실험

제프 포스닉
제프 포스닉

배경

서비스 워커는 웹 개발자가 웹 애플리케이션의 네트워크 요청에 응답할 수 있는 기능을 제공하여 오프라인 상태에서도 작업을 계속하고, lie-fi에 맞서며, 비활성 재검증과 같은 복잡한 캐시 상호작용을 구현할 수 있도록 합니다. 그러나 서비스 워커는 예전부터 특정 출처에 연결되어 있었습니다. 웹 앱의 모든 소유자는 서비스 워커를 작성하고 배포하여 웹 앱이 수행하는 모든 네트워크 요청을 가로챌 책임이 있습니다. 이 모델에서는 각 서비스 워커가 서드 파티 API 또는 웹 글꼴에 대한 교차 출처 요청까지도 처리해야 합니다.

API, 웹 글꼴 또는 흔히 사용되는 기타 서비스의 서드 파티 제공업체가 자체 서비스 워커를 배포하여 다른 출처의 요청을 원본으로 처리할 수 있다면 어떻게 해야 할까요? 제공업체는 자체 커스텀 네트워킹 로직을 구현하고 응답을 저장하는 데 신뢰할 수 있는 단일 캐시 인스턴스를 활용할 수 있습니다. 이제 외부 가져오기 덕분에 이러한 유형의 서드 파티 서비스 워커 배포가 실현되었습니다.

외부 가져오기를 구현하는 서비스 워커를 배포하는 것은 브라우저에서 HTTPS 요청을 통해 액세스하는 모든 서비스 제공업체에 적합합니다. 하지만 브라우저가 공통 리소스 캐시를 활용할 수 있는 네트워크 독립 버전의 서비스를 제공할 수 있는 시나리오를 생각해 보세요. 여기에는 다음과 같은 서비스가 포함되며 이에 국한되지 않습니다.

  • RESTful 인터페이스를 사용하는 API 제공업체
  • 웹 글꼴 제공업체
  • 분석 제공업체
  • 이미지 호스팅 업체
  • 일반 콘텐츠 전송 네트워크

예를 들어 분석 서비스 제공업체 입장에서 외부 가져오기 서비스 워커를 배포하면 사용자가 오프라인 상태일 때 실패하는 모든 서비스 요청이 큐에 추가되어 연결이 복구된 후 다시 재생되도록 할 수 있습니다. 서비스의 클라이언트가 퍼스트 파티 서비스 워커를 통해 유사한 동작을 구현할 수도 있지만, 각 클라이언트가 서비스를 위한 맞춤형 로직을 작성하도록 하는 것은 개발자가 배포하는 공유된 외부 가져오기 서비스 워커를 사용하는 것만큼 확장 가능하지 않습니다.

기본 요건

오리진 트라이얼 토큰

외부 가져오기는 여전히 실험용으로 간주됩니다. 이 설계가 브라우저 공급업체에서 완전히 명시되고 동의되기 전에 조기에 이 설계를 적용하지 않도록, Chrome 54에서는 이 설계를 오리진 트라이얼로 구현했습니다. 외부 가져오기가 실험용으로 유지되는 한 호스팅하는 서비스에서 이 새로운 기능을 사용하려면 서비스의 특정 출처로 범위가 지정된 토큰을 요청해야 합니다. 외부 가져오기를 통해 처리하려는 리소스에 대한 모든 교차 출처 요청과 서비스 워커 JavaScript 리소스에 대한 응답에 토큰을 HTTP 응답 헤더로 포함해야 합니다.

Origin-Trial: token_obtained_from_signup

무료 체험은 2017년 3월에 종료됩니다. 이 시점이 되면 이 기능을 안정화하는 데 필요한 변경사항을 파악하고 이 기능을 기본적으로 사용할 수 있을 것으로 기대됩니다. 그때까지 외부 가져오기가 기본적으로 사용 설정되지 않으면 기존 오리진 트라이얼 토큰에 연결된 기능이 더 이상 작동하지 않습니다.

공식 오리진 트라이얼 토큰에 등록하기 전에 외부 가져오기 실험을 용이하게 하려면 chrome://flags/#enable-experimental-web-platform-features로 이동하여 '실험용 웹 플랫폼 기능' 플래그를 사용 설정하여 로컬 컴퓨터에 대한 Chrome 요구사항을 우회하세요. 오리진 트라이얼 토큰의 경우 이 기능을 모든 Chrome 사용자가 사용할 수 있는 반면, 로컬 실험에 사용하려는 모든 Chrome 인스턴스에서 이 작업을 수행해야 합니다.

HTTPS

모든 서비스 워커 배포와 마찬가지로 리소스와 서비스 워커 스크립트를 모두 제공하는 데 사용하는 웹 서버는 HTTPS를 통해 액세스해야 합니다. 또한 외부 가져오기 가로채기는 보안 출처에서 호스팅되는 페이지에서 시작된 요청에만 적용되므로 서비스의 클라이언트가 외부 가져오기 구현을 활용하려면 HTTPS를 사용해야 합니다.

외부 가져오기 사용

기본 요건을 갖추지 않았으므로 외부 가져오기 서비스 워커를 준비하고 실행하는 데 필요한 기술적 세부정보를 알아보겠습니다.

서비스 워커 등록

가장 먼저 접하게 될 문제는 서비스 워커를 등록하는 방법입니다. 이전에 서비스 워커를 사용한 적이 있다면 다음 항목에 익숙할 것입니다.

// You can't do this!
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js');
}

퍼스트 파티 서비스 워커 등록을 위한 이 JavaScript 코드는 개발자가 제어하는 URL로 이동하는 사용자에 의해 트리거되는 웹 앱의 컨텍스트에서 적합합니다. 하지만 타사 서비스 워커를 등록하는 것은 실행 가능한 방법이 아닙니다. 이 방법은 전체 탐색이 아닌 특정 하위 리소스를 요청하는 상호작용이 서버와의 유일한 상호작용 브라우저인 경우에 발생합니다. 브라우저가 유지 관리하는 CDN 서버의 이미지를 요청하는 경우, 응답 앞에 JavaScript 스니펫을 추가하여 실행될 것을 예상할 수 없습니다. 일반 JavaScript 실행 컨텍스트 외에 다른 서비스 워커 등록 방법이 필요합니다.

이 솔루션은 서버가 모든 응답에 포함할 수 있는 HTTP 헤더의 형식으로 제공됩니다.

Link: </service-worker.js>; rel="serviceworker"; scope="/"

이 예시 헤더를 구성요소로 세분화해 보겠습니다. 각 구성요소는 ; 문자로 구분됩니다.

  • </service-worker.js>는 필수 항목이며 서비스 워커 파일의 경로를 지정하는 데 사용됩니다 (/service-worker.js를 스크립트의 적절한 경로로 바꿈). 이는 navigator.serviceWorker.register()에 첫 번째 매개변수로 전달되는 scriptURL 문자열에 직접 상응합니다. 값은 Link 헤더 사양에 따라 <> 문자로 묶어야 하며, 절대 URL이 아닌 상대 URL을 입력하면 응답 위치를 기준으로 해석됩니다.
  • rel="serviceworker"도 필수이며 맞춤설정할 필요 없이 포함되어야 합니다.
  • scope=/은 선택적 범위 선언으로 navigator.serviceWorker.register()에 두 번째 매개변수로 전달할 수 있는 options.scope 문자열과 같습니다. 대부분의 사용 사례에서는 기본 범위 사용만으로도 충분하므로 이 범위가 필요하다는 것을 모르고 지나칠 수 있습니다. Service-Worker-Allowed 헤더를 통해 이러한 제한을 완화하는 기능과 함께 허용되는 최대 범위에 대한 동일한 제한사항이 Link 헤더 등록에 적용됩니다.

'기존' 서비스 워커 등록과 마찬가지로 Link 헤더를 사용하면 등록된 범위에 대해 다음 요청에 사용될 서비스 워커가 설치됩니다. 특수 헤더가 포함된 응답 본문은 그대로 사용되며 외부 서비스 워커가 설치를 완료할 때까지 기다리지 않고 페이지에서 즉시 사용할 수 있습니다.

외부 가져오기는 현재 오리진 트라이얼로 구현되므로 링크 응답 헤더와 함께 유효한 Origin-Trial 헤더도 포함해야 합니다. 외부 가져오기 서비스 워커를 등록하기 위해 추가해야 하는 최소 응답 헤더 세트는 다음과 같습니다.

Link: </service-worker.js>; rel="serviceworker"
Origin-Trial: token_obtained_from_signup

디버깅 등록

개발 중에는 외부 가져오기 서비스 워커가 제대로 설치되어 요청을 처리하는지 확인하고 싶을 것입니다. 다음 몇 가지 사항을 Chrome의 개발자 도구에서 제대로 작동하는지 확인할 수 있습니다.

올바른 응답 헤더가 전송되고 있나요?

외부 가져오기 서비스 워커를 등록하려면 이 게시물의 앞부분에서 설명한 대로 도메인에서 호스팅되는 리소스에 대한 응답에 링크 헤더를 설정해야 합니다. 오리진 트라이얼 기간 동안 chrome://flags/#enable-experimental-web-platform-features를 설정하지 않았다면 Origin-Trial 응답 헤더도 설정해야 합니다. DevTools의 네트워크 패널에 있는 항목을 보면 웹 서버에서 이러한 헤더를 설정하고 있는지 확인할 수 있습니다.

네트워크 패널에 표시되는 헤더입니다.

외부 가져오기 서비스 워커가 올바르게 등록되어 있나요?

또한 DevTools의 Application 패널에 있는 서비스 워커의 전체 목록을 보고 기본 서비스 워커 등록을 확인할 수 있습니다(범위 포함). 기본적으로 현재 출처에 대한 서비스 워커만 표시되므로 '모두 표시' 옵션을 선택해야 합니다.

애플리케이션 패널의 외부 가져오기 서비스 워커.

설치 이벤트 핸들러

이제 서드 파티 서비스 워커를 등록했으므로 다른 서비스 워커와 마찬가지로 installactivate 이벤트에 응답할 수 있습니다. 예를 들어 이러한 이벤트를 활용하여 install 이벤트 중에 필수 리소스로 캐시를 채우거나 activate 이벤트에서 오래된 캐시를 프루닝할 수 있습니다.

일반적인 install 이벤트 캐싱 활동 외에도 서드 파티 서비스 워커의 install 이벤트 핸들러 내에 필수 추가 단계가 있습니다. 다음 예와 같이 코드는 registerForeignFetch()를 호출해야 합니다.

self.addEventListener('install', event => {
    event.registerForeignFetch({
    scopes: [self.registration.scope], // or some sub-scope
    origins: ['*'] // or ['https://example.com']
    });
});

두 가지 구성 옵션이 있으며 두 가지 모두 필요합니다.

  • scopes는 하나 이상의 문자열로 구성된 배열을 사용하며, 각 문자열은 foreignfetch 이벤트를 트리거하는 요청의 범위를 나타냅니다. 잠시만 기다려 주세요. 서비스 워커를 등록하는 동안 이미 범위를 정의했습니다라고 생각할 수도 있습니다. 이 사실은 사실이며 전체 범위는 여전히 관련이 있습니다. 여기서 지정하는 각 범위는 서비스 워커의 전체 범위의 하위 범위와 같거나 그 하위 범위여야 합니다. 여기에 추가 범위 지정 제한을 사용하면 퍼스트 파티 fetch 이벤트 (자체 사이트에서 발생한 요청의 경우)와 서드 파티 foreignfetch 이벤트 (다른 도메인에서 발생한 요청의 경우)를 모두 처리할 수 있는 다목적 서비스 워커를 배포할 수 있으며, 더 큰 범위의 하위 집합만 foreignfetch를 트리거하도록 명확히 할 수 있습니다. 실제로 서드 파티 foreignfetch 이벤트만 처리하는 전용 서비스 워커를 배포하는 경우 서비스 워커의 전체 범위와 동일한 명시적 단일 범위를 사용하는 것이 좋습니다. 위의 예시에서는 self.registration.scope 값을 사용하면 됩니다.
  • 또한 origins는 하나 이상의 문자열로 구성된 배열을 사용하며, 이를 통해 foreignfetch 핸들러가 특정 도메인의 요청에만 응답하도록 제한할 수 있습니다. 예를 들어 'https://example.com'을 명시적으로 허용하는 경우 https://example.com/path/to/page.html에서 호스팅되는 페이지에서 외부 가져오기 범위에서 제공되는 리소스를 요청하면 외부 가져오기 핸들러가 트리거되지만 https://random-domain.com/path/to/page.html에서 작성된 요청은 핸들러를 트리거하지 않습니다. 원격 출처의 하위 집합에 관해 외부 가져오기 로직만 트리거해야 하는 특별한 이유가 없다면 '*'을 배열의 유일한 값으로 지정하면 됩니다. 그러면 모든 출처가 허용됩니다.

Externalfetch 이벤트 핸들러

서드 파티 서비스 워커를 설치했고 registerForeignFetch()를 통해 구성했으므로 외부 가져오기 범위에 속하는 교차 출처 하위 리소스 요청을 서버로 가로챌 수 있습니다.

기존의 퍼스트 파티 서비스 워커에서는 각 요청이 서비스 워커가 응답할 수 있는 fetch 이벤트를 트리거합니다. 타사 서비스 워커는 foreignfetch라는 약간 다른 이벤트를 처리할 수 있습니다. 개념적으로 두 이벤트는 매우 유사하며 수신되는 요청을 검사하고 선택적으로 respondWith()를 통해 응답을 제공할 수 있는 기회를 제공합니다.

self.addEventListener('foreignfetch', event => {
    // Assume that requestLogic() is a custom function that takes
    // a Request and returns a Promise which resolves with a Response.
    event.respondWith(
    requestLogic(event.request).then(response => {
        return {
        response: response,
        // Omit to origin to return an opaque response.
        // With this set, the client will receive a CORS response.
        origin: event.origin,
        // Omit headers unless you need additional header filtering.
        // With this set, only Content-Type will be exposed.
        headers: ['Content-Type']
        };
    })
    );
});

개념적 유사점에도 불구하고 실제로 ForeignFetchEvent에서 respondWith()를 호출할 때 몇 가지 차이점이 있습니다. FetchEvent에서처럼 Response (또는 Response로 확인되는 Promise)를 respondWith()에 제공하는 대신 특정 속성이 있는 객체로 확인되는 PromiseForeignFetchEventrespondWith()에 전달해야 합니다.

  • response는 필수 항목이며 요청한 클라이언트에 반환될 Response 객체로 설정해야 합니다. 유효한 Response 이외의 항목을 제공하면 클라이언트 요청이 네트워크 오류로 종료됩니다. fetch 이벤트 핸들러 내에서 respondWith()를 호출할 때와 달리, Response로 확인되는 Promise가 아니라 Response를 여기에 제공해야 합니다. 프로미스 체인을 통해 응답을 구성하고 이 체인을 매개변수로 foreignfetchrespondWith()에 전달할 수 있지만 체인은 Response 객체로 설정된 response 속성이 포함된 객체로 확인되어야 합니다. 위의 코드 샘플에서 이에 관한 데모를 볼 수 있습니다.
  • origin는 선택사항이며 반환되는 응답이 불투명한지 여부를 결정하는 데 사용됩니다. 이를 생략하면 응답이 불투명하게 되고 클라이언트가 응답의 본문과 헤더에 액세스할 수 없게 됩니다. mode: 'cors'로 요청한 경우 불투명한 응답을 반환하면 오류로 처리됩니다. 그러나 원격 클라이언트의 출처 (event.origin을 통해 가져올 수 있음)와 동일한 문자열 값을 지정하면 CORS가 사용 설정된 응답을 클라이언트에 제공하도록 명시적으로 선택됩니다.
  • headers도 선택사항이며, origin를 지정하고 CORS 응답을 반환하는 경우에만 유용합니다. 기본적으로 CORS 허용 목록에 있는 응답 헤더 목록에 있는 헤더만 응답에 포함됩니다. 반환되는 항목을 추가로 필터링해야 하는 경우 헤더는 하나 이상의 헤더 이름 목록을 취하고 이를 응답에 노출할 헤더의 허용 목록으로 사용합니다. 이렇게 하면 잠재적으로 민감한 응답 헤더가 원격 클라이언트에 직접 노출되는 것을 방지하면서 CORS를 선택할 수 있습니다.

foreignfetch 핸들러가 실행되면 서비스 워커를 호스팅하는 출처의 모든 사용자 인증 정보와 주변 권한에 액세스할 수 있습니다. 외부 가져오기 지원 서비스 워커를 배포하는 개발자는 이러한 사용자 인증 정보로는 사용할 수 없는 권한 있는 응답 데이터가 유출되지 않도록 할 책임이 있습니다. CORS 응답을 선택해야 하는 것은 의도치 않은 노출을 제한하기 위한 한 단계이지만, 개발자는 foreignfetch 핸들러 내에서 다음을 통해 묵시적 사용자 인증 정보를 사용하지 않는 fetch() 요청을 명시적으로 만들 수 있습니다.

self.addEventListener('foreignfetch', event => {
    // The new Request will have credentials omitted by default.
    const noCredentialsRequest = new Request(event.request.url);
    event.respondWith(
    // Replace with your own request logic as appropriate.
    fetch(noCredentialsRequest)
        .catch(() => caches.match(noCredentialsRequest))
        .then(response => ({response}))
    );
});

클라이언트 고려사항

외부 가져오기 서비스 워커가 서비스 클라이언트의 요청을 처리하는 방식에 영향을 미치는 추가 고려사항이 있습니다.

자체 퍼스트 파티 서비스 워커가 있는 클라이언트

서비스의 일부 클라이언트에는 이미 웹 앱에서 발생한 요청을 처리하는 자체 퍼스트 파티 서비스 워커가 있을 수 있습니다. 이는 서드 파티의 외부 가져오기 서비스 워커에 어떤 영향을 미치나요?

퍼스트 파티 서비스 워커의 fetch 핸들러는 요청을 처리하는 범위로 foreignfetch가 사용 설정된 서드 파티 서비스 워커가 있는 경우에도 웹 앱의 모든 요청에 응답할 수 있는 첫 번째 기회를 얻습니다. 하지만 퍼스트 파티 서비스 워커가 있는 클라이언트는 외부 가져오기 서비스 워커를 활용할 수 있습니다.

퍼스트 파티 서비스 워커 내에서 fetch()를 사용하여 교차 출처 리소스를 가져오면 적절한 외부 가져오기 서비스 워커가 트리거됩니다. 즉, 다음과 같은 코드는 foreignfetch 핸들러를 활용할 수 있습니다.

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    // If event.request is under your foreign fetch service worker's
    // scope, this will trigger your foreignfetch handler.
    event.respondWith(fetch(event.request));
});

마찬가지로 퍼스트 파티 가져오기 핸들러가 있지만 교차 출처 리소스의 요청을 처리할 때 event.respondWith()를 호출하지 않는 경우 요청이 자동으로 foreignfetch 핸들러로 '폴스루'됩니다.

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    if (event.request.mode === 'same-origin') {
    event.respondWith(localRequestLogic(event.request));
    }

    // Since event.respondWith() isn't called for cross-origin requests,
    // any foreignfetch handlers scoped to the request will get a chance
    // to provide a response.
});

퍼스트 파티 fetch 핸들러가 event.respondWith()를 호출하지만 fetch()를 사용하여 외부 가져오기 범위에서 리소스를 요청하지 않는 경우 외부 가져오기 서비스 워커는 요청을 처리할 기회를 얻지 못합니다.

자체 서비스 워커가 없는 클라이언트

서드 파티 서비스에 요청하는 모든 클라이언트는 자체 서비스 워커를 아직 사용하고 있지 않더라도 서비스가 외부 가져오기 서비스 워커를 배포할 때 이점을 얻을 수 있습니다. 클라이언트가 외부 가져오기 서비스 워커를 지원하는 브라우저를 사용하는 한 이러한 서비스 워커를 사용하도록 선택하기 위해 클라이언트가 취해야 할 특별한 조치는 없습니다. 즉, 외부 가져오기 서비스 워커를 배포하면 커스텀 요청 로직과 공유 캐시가 서비스의 많은 클라이언트가 추가 조치를 취하지 않아도 즉시 이점을 누릴 수 있습니다.

종합: 클라이언트가 응답을 찾는 위치

위의 정보를 고려하여 클라이언트가 교차 출처 요청에 대한 응답을 찾는 데 사용할 소스 계층 구조를 조합할 수 있습니다.

  1. 퍼스트 파티 서비스 워커의 fetch 핸들러 (있는 경우)
  2. 서드 파티 서비스 워커의 foreignfetch 핸들러 (있는 경우, 교차 출처 요청에만 해당)
  3. 브라우저의 HTTP 캐시 (새 응답이 있는 경우)
  4. 네트워크

브라우저는 맨 위에서부터 시작하며, 서비스 워커 구현에 따라 응답의 소스를 찾을 때까지 목록 아래로 계속 내려갑니다.

자세히 알아보기

최신 정보 확인

Chrome의 해외 가져오기 오리진 트라이얼 구현은 개발자의 의견을 반영하여 변경될 수 있습니다. 이 게시물은 인라인 변경사항을 통해 최신 상태로 유지되고 구체적인 변경사항이 생길 때마다 언급할 예정입니다. @chromiumdev Twitter 계정을 통해 주요 변경사항에 대한 정보도 공유할 예정입니다.